分布式事务-seata详解

分布式事务名词解释
在分布式系统下,一个业务跨越多个服务或者数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致,这样的事务就是分布式事务。

理论基础
CAP理论
Consistency(一致性): 用户访问分布式系统中的任意节点,得到的数据必须一致
Availability(可用性): 用户访问集群中任意健康的节点,必须能得到响应,而不是超时或拒绝
Partition tolerance(分区容错性): 
                         
Partition(分区): 因为网络故障或者其他原因导致分布式系统中的部分节点与其他节点失去连接,形成独立分区
tolerance(容错): 在集群出现分区时,整个系统也要持续对外提供服务

结论: 但凡是分布式系统,分区一定会出现,因为分布式系统都是通过网络连接的,不可能保证网络百分之百可用。所以分区一定会出现。

BASE理论
Basically Available(基本可用) : 分布式系统在出现故障时,允许损失部分可用性,即保证核心可用
Soft State(软状态) : 在一定时间内,允许出现中间状态,比如临时的不一致状态
Eventually Consistent(最终一致性) : 虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致

AP模式: 各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致
CP模式: 各子事务执行后相互等待,同时提交,同时回滚,达成强一致性,但事务等待过程中,处于弱可用状态


解决分布式事务的思想和模型
全局事务: 整个分布式事务
分支事务: 分布式事务中包含的每个子系统的事务

seata框架
TC(Transaction Coordinator)-事务协调者: 维护全局和分支事务的状态,协调全局事务提交或回滚
TM(Transaction Manager)-事务管理器: 定义全局事务的范围,开始全局事务,提交或回滚全局事务
RM(Resource Manager)-资源管理器: 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事物的提交或回滚

搭建TC服务与springboot整合seata这里暂时不做介绍

seata解决分布式事务有4种模式
XA模式   : XA规范描述了全局的TM与局部的RM直接的接口,几乎所有主流数据库都对XA规范提供了支持
           两段提交第一阶段 1.注册事务到TC 2.执行分支业务sql但是不提交 3.报告执行状态到TC
           两段提交第二阶段 1.TC检查各分支事务执行状态,如果都成功通知所有RM提交,如果有失败的通知所有RM回滚 2.接收TC指令,提交或者回滚事务
           优势: 强一致性,常用数据库都支持,实现简单,并且没有代码侵入
           缺点: 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差,依赖关系型数据库实现事务
           XA实现步骤:
           1.修改配置文件增加配置seata.data-source-proxy-mode: XA
           2.给发起全局事务的入口方法添加@GlobalTransactional注解
             @GlobalTransactional
             public void test() {
                 //调用a服务中的方法
                 //调用b服务中的方法
             }
           3.重启服务
           
AT模式   : 同样是分阶段提交的事务模型,不过弥补了XA模型中资源锁定周期过长的缺陷
           两段提交第一阶段: 1.注册分支事务 2.记录undo-log数据快照 3.执行业务sql并且提交 4.报告事务状态
           两段提交第二阶段: 1.TC发现如果事务都成功了就删除undo-log,如果有失败的事务根据undo-log回复数据,异步删除undo-log
           AT模式与XA模式的区别: XA模式一阶段不提交事务,锁定资源. AT模式一阶段直接提交,不锁定资源
                                 XA模式依赖数据库机制实现回滚,AT模式利用数据快照实现数据回滚
                                 XA模式为强一致,AT模式为最终一致
           
           AT模式可能出现的问题,就是第一阶段提交了数据,之后有必别的线程修改了这条数据。第二阶段回滚就出现了问题。
           比如: 原始数据为100,第一阶段将数据修改为90,undo-log中的记录为100,此时锁已经释放。别的线程获取了当前的锁,将数据修改为80,然后释放锁。
                 此时如果第一个事务发生了回滚,将回滚到undo-log中的100。
           为了解决这个问题退出了全局锁: 由TC记录当前正在操作某行数据的事务,该事务持有全局锁,具备执行权。
           AT模式实现步骤:
           1. 导入解决全局锁问题的表seata-at.sql到TC的数据库中,导入AT模式实现的回滚日志表undo-log表到各个需要的微服务中(表结构在下面)
           2. 修改配置文件seata.data-source-proxy-mode: AT
           3. 重启服务并测试
           DROP TABLE IF EXISTS `undo_log`;
            CREATE TABLE `undo_log`  (
              `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
              `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
              `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
              `rollback_info` longblob NOT NULL COMMENT 'rollback info',
              `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
              `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
              `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
              UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
            ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
           
           DROP TABLE IF EXISTS `lock_table`;
            CREATE TABLE `lock_table`  (
              `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
              `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
              `transaction_id` bigint(20) NULL DEFAULT NULL,
              `branch_id` bigint(20) NOT NULL,
              `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
              `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
              `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
              `gmt_create` datetime NULL DEFAULT NULL,
              `gmt_modified` datetime NULL DEFAULT NULL,
              PRIMARY KEY (`row_key`) USING BTREE,
              INDEX `idx_branch_id`(`branch_id`) USING BTREE
            ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
            
            
           
TCC模式  : TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC是通过人工编码来实现数据恢复。需要实现三个方法:
           Try: 资源的检测和预留
           Confirm: 完成资源操作业务; 要求try成功,Confirm一定能成功
           Cancel: 预留资源释放,可以理解为try的反向操作
           
           案例: 一个账户扣款的需求,如果账户原来有100元,需要扣款30元。
                 阶段一(Try):     检查余额是否充足,如果充足则冻结金额增加30元(冻结金额变成了30元),可用余额扣除30元(可用余额变成了70元)
                 阶段二(Confirm): 假如要提交,则冻结金额扣减30元(冻结金额变成0元)  注意:此时可用余额不做变化
                 阶段三(Cancel):  如果要回滚,则冻结金额扣减30元(冻结金额变成了0元),可用余额增加增加30元(可用余额变成了100元)
           
           TCC模式的优点: 一阶段完成直接提交事务,释放数据库资源,性能好
                          相比AT模式,无需生成快照(undo_log),无需使用全局锁,性能最强
                          不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库比如redis,mongodb等
                          
           TCC模式的缺点: 有代码侵入,需要人为编写Try,Confirm,Cancel接口,太麻烦
                          软状态,事务是最终一致
                          需要考虑Confirm和Cancel的失败情况,做好幂等处理
                          
           TCC模式实现步骤(注意: TCC模式并不是使用所有的事务场景,比如新建订单操作,就是一个新增的过程,没有什么是可以实现冻结的,直接使用AT模式就可以解决)
           1. 为了防止空回滚(由于阻塞原因try没有执行,TC通知所有TM进行回滚,就会出现没有执行try操作,就进行Cancel操作),
              防止业务悬挂(这个时候阻塞结束了,事务也结束了。又去执行try操作,就会出现try执行了,但是事务已经结束了,没有后续阶段了),以及幂等性要求。
              所以在编写逻辑的时候必须进行判断,执行try的时候判断事务是否已经结束。执行Cancel的时候判断是否已经执行了try。
              保证幂等性必须判断当前逻辑是否已经执行,如果执行了就不继续执行。
              为了保证这些必须在数据库记录冻结金额的同时,记录当前事务的id和执行状态。
              DROP TABLE IF EXISTS `account_freeze_tbl`;
              CREATE TABLE `account_freeze_tbl`  (
               `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '事务id',  
               `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id',
               `freeze_money` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '冻结金额',
               `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
               PRIMARY KEY (`xid`) USING BTREE
              ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
            2. Try业务: 记录冻结金额和事务状态到account_freeze_tbl表中,扣减余额表中的可用余额数据(判断业务悬挂:根据xid查询account_freeze_tbl表如果已经存在了说明已经业务悬挂了,就避免业务执行 )
            3. Confirm业务: 根据xid删除account_freeze_tbl表中的冻结记录. 
            4. Cancel业务: 修改account_freeze_tbl表中冻结金额为0,状态为2。回复余额表中的可用金额(判断空回滚: 根据xid查询account_freeze_tbl表如果为null证明try还没做,需要空回滚 )
            5. 在业务接口上标注@LocalTCC注解, @TwoPhaseBusinessAction注解加在哪个方法上,哪个方法就是Try, commitMethod对应Confirm, rollbackMethod对应Cancel, 需要用到的参数
               使用@BusinessActionContextParameter进行标注,这样的话在Confirm和Cancel中都可以通过上下文获取到这些参数
               @LocalTCC
                public interface AccountTCCService {

                    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
                    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money")int money);
                    
                    boolean confirm(BusinessActionContext ctx);

                    boolean cancel(BusinessActionContext ctx);
                }
                
                Try代码:
                @Override
                @Transactional
                public void deduct(String userId, int money) {
                    // 0.获取事务id
                    String xid = RootContext.getXID();
                    //检查是否存在业务悬挂
                    AccountFreeze accountFreeze = freezeMapper.selectById(xid);
                    if(accountFreeze != null){
                        return;
                    }
                    // 1.扣减可用余额
                    accountMapper.deduct(userId, money);
                    // 2.记录冻结金额,事务状态
                    AccountFreeze freeze = new AccountFreeze();
                    freeze.setUserId(userId);
                    freeze.setFreezeMoney(money);
                    freeze.setState(AccountFreeze.State.TRY);
                    freeze.setXid(xid);
                    freezeMapper.insert(freeze);
                }
                
                Confirm代码:
                @Override
                public boolean confirm(BusinessActionContext ctx) {
                    // 1.获取事务id
                    String xid = ctx.getXid();
                    // 2.根据id删除冻结记录
                    int count = freezeMapper.deleteById(xid);
                    return count == 1;
                }
                
                Cancel代码:
                @Override
                public boolean cancel(BusinessActionContext ctx) {
                    // 0.查询冻结记录
                    String xid = ctx.getXid();
                    AccountFreeze freeze = freezeMapper.selectById(xid);
                    //空回滚判断,如果是null证明需要空回滚
                    if(freeze == null){
                        AccountFreeze freeze2 = new AccountFreeze();
                        String userId = (String) ctx.getActionContext("userId");
                        freeze2.setUserId(userId);
                        freeze2.setFreezeMoney(0);
                        freeze2.setState(AccountFreeze.State.CANCEL);
                        freeze2.setXid(xid);
                        freezeMapper.insert(freeze2);
                        return true;
                    }
                    //判断幂等性
                    if(freeze.getState() == AccountFreeze.State.CANCEL){
                        //已经处理过一次CANCEL了 无需重复处理
                        return true;
                    }
                    // 1.恢复可用余额
                    accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
                    // 2.将冻结金额清零,状态改为CANCEL
                    freeze.setFreezeMoney(0);
                    freeze.setState(AccountFreeze.State.CANCEL);
                    int count = freezeMapper.updateById(freeze);
                    return count == 1;
                }
                          
                          
SAGA模式 : 属于长事务解决方案,也是分为两阶段提交
           第一阶段直接提交本地事务
           第二阶段成功则什么都不做,如果失败了则通过编写补偿业务来回滚
           
           优点:
           事务参与者可以基于事件驱动实现异步调用,吞吐高
           一阶段直接提交事务,无锁,性能好
           不用编写TCC的三个阶段,实现简单
           缺点:
           软状态持续时间不确定,时效性差
           没有锁,没有事务隔离,会有脏写
           
           
           
           
           
           

上一篇:Seata分布式事务失效,不生效(事务不回滚)的常见场景


下一篇:大数据_学习_01_Hadoop 2.x及hbase常用端口及查看方法