XA 模式

XA 规范

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的 TM 与局部的 RM 之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。

使用 Seata - 图1

Seata 的 XA 模式

Seata 的 XA 模式做了一些调整,但大体相似:

RM 一阶段的工作:

  • 注册分支事务到 TC
  • 执行分支业务 SQL 但不提交
  • 报告执行状态到 TC

TC 二阶段的工作:

  • TC 检测各分支事务执行状态
  • 如果都成功,通知所有 RM 提交事务
  • 如果有失败,通知所有 RM 回滚事务

RM 二阶段的工作:

  • 接收 TC 指令,提交或回滚事务

使用 Seata - 图2

XA 模式的优点:

  • 事务的强一致性,满足 ACID 原则。
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA 模式的缺点:

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

Seata 实现 XA 模式

Seata 的 starter 已经完成了 XA 模式的自动装配,实现非常简单,步骤如下:

修改 application.yml 文件(每个参与事务的微服务),开启 XA 模式:

  1. seata:
  2. data-source-proxy-mode: XA # 开启数据源代理的XA模式

给发起全局事务的入口方法添加 @GlobalTransactional 注解,本例中是SeataOrderServiceImpl 中的 create 方法:

  1. @Override
  2. @Transactional
  3. @GlobalTransactional
  4. public Long create(SeataOrder order) {
  5. // 创建订单
  6. orderMapper.insert(order);
  7. try {
  8. // 扣用户余额
  9. accountClient.deduct(order.getUserId(), order.getMoney());
  10. // 扣库存
  11. storageClient.deduct(order.getCommodityCode(), order.getCount());
  12. } catch (FeignException e) {
  13. log.error("下单失败,原因:{}", e.contentUTF8(), e);
  14. throw new RuntimeException(e.contentUTF8(), e);
  15. }
  16. return order.getId();
  17. }

重启服务并测试

AT 模式

AT 模式介绍

AT 模式同样是分阶段提交的事务模型,不过缺弥补了 XA 模型中资源锁定周期过长的缺陷。

使用 Seata - 图3

阶段一 RM 的工作:

  • 注册分支事务
  • 记录 undo-log(数据快照)
  • 执行业务 SQL 并提交
  • 报告事务状态

阶段二提交时 RM 的工作:

  • 删除 undo-log 即可

阶段二回滚时 RM 的工作:

  • 根据 undo-log 恢复数据到更新前

简述 AT 模式与 XA 模式最大的区别是什么?

  • XA 模式一阶段不提交事务,锁定资源;AT 模式一阶段直接提交,不锁定资源。
  • XA 模式依赖数据库机制实现回滚;AT 模式利用数据快照实现数据回滚。
  • XA 模式强一致;AT 模式最终一致

AT 模式的脏写问题

因为事务之间没有隔离,存在脏写问题

使用 Seata - 图4

解决方法:

全局锁:由 TC 记录当前正在操作某行数据的事务,该事务持有全局锁,具备执行权。

使用 Seata - 图5

对于非 Seata 管理的业务

使用 Seata - 图6

AT 模式的优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能比较好
  • 利用全局锁实现读写隔离
  • 没有代码侵入,框架自动完成回滚和提交

AT 模式的缺点:

  • 两阶段之间属于软状态,属于最终一致
  • 框架的快照功能会影响性能,但比 XA 模式要好很多

Seata 实现 AT 模式

AT 模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。

  1. 导入 SQL 文件:seata-at.sql,其中 lock_table 导入到 TC 服务关联的数据库,undo_log 表导入到微服务关联的数据库
  2. 修改 application.yml 文件,将事务模式修改为 AT 模式即可

    1. seata:
    2. data-source-proxy-mode: AT # 开启数据源代理的AT模式
  3. 服务添加 @GlobalTransactional 注解

  4. 重启服务并测试

TCC 模式

TCC 模式介绍

TCC 模式与 AT 模式非常相似,每阶段都是独立事务,不同的是 TCC 通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留;
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为 Try 的反向操作。

举例,一个扣减用户余额的业务。假设账户 A 原来余额是 100,需要余额扣减 30 元。

  • 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加 30 元,可用余额扣除 30
  • 阶段二:假如要提交(Confirm),则冻结金额扣减 30
  • 阶段二:如果要回滚(Cancel),则冻结金额扣减 30,可用余额增加 30

使用 Seata - 图7

TCC 模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC 的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比 AT 模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC 的缺点是什么?

  • 有代码侵入,需要人为编写 Try、Confirm 和 Cancel 接口,太麻烦
  • 软状态,事务是最终一致

需要考虑 Confirm 和 Cancel 的失败情况,做好幂等处理

实现 TCC 模式

改造 account-service 服务,利用 TCC 实现分布式事务

需求如下:

  • 修改 account-service,编写 try、confirm、cancel 逻辑
  • try 业务:添加冻结金额,扣减可用金额
  • confirm 业务:删除冻结金额
  • cancel 业务:删除冻结金额,恢复可用金额
  • 保证 confirm、cancel 接口的幂等性(重复调用效果相同)
  • 允许空回滚
  • 拒绝业务悬挂

TCC 的空回滚和业务悬挂

  • 当某分支事务的 try 阶段阻塞时,可能导致全局事务超时而触发二阶段的 cancel 操作。在未执行 try 操作时先执行了 cancel 操作,这时 cancel 不能做回滚,就是空回滚。
  • 对于已经空回滚的业务,如果以后继续执行 try,就永远不可能 confirm 或 cancel,这就是业务悬挂。应当阻止执行空回滚后的 try 操作,避免悬挂。

为了实现空回滚、防止业务悬挂,以及幂等性要求。我们必须在数据库记录冻结金额的同时,记录当前事务 id 和执行状态,为此我们设计了一张表:

  1. CREATE TABLE `account_freeze_tbl` (
  2. `xid` varchar(128) NOT NULL,
  3. `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
  4. `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
  5. `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  6. PRIMARY KEY (`xid`) USING BTREE
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
  • Try 业务:记录冻结金额和事务状态到 account_freeze 表,扣减 account 表可用金额
  • Confirm 业务:根据 xid 删除 account_freeze 表的冻结记录
  • Cancel 业务:修改 account_freeze 表,冻结金额为 0,state 为 2;修改 account 表,恢复可用金额
  • 如何判断是否空回滚:Cancel 业务中,根据 xid 查询 account_freeze,如果为 null 则说明 Try 还没做,需要空回滚
  • 如何避免业务悬挂:Try 业务中,根据 xid 查询 account_freeze ,如果已经存在则证明Cancel 已经执行,拒绝执行 Try 业务

声明 TCC 接口:Try、Confirm、Cancel 方法都需要在接口中基于注解来声明,语法如下

  1. @LocalTCC public interface TCCService {
  2. /**
  3. * Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法
  4. */
  5. @TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel")
  6. void prepare(@BusinessActionContextParameter(paramName = "param") String param);
  7. /**
  8. * 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致
  9. *
  10. * @param context 上下文,可以传递try方法的参数
  11. * @return boolean 执行是否成功
  12. */
  13. boolean confirm (BusinessActionContext context);
  14. /**
  15. * 二阶段回滚方法,要保证与rollbackMethod一致
  16. */
  17. boolean cancel (BusinessActionContext context);
  18. }

seata-demo 数据库中引入 account_freeze_tbl.sql 数据表,创建对应实体类和Mapper

  1. @Data
  2. @TableName("account_freeze_tbl")
  3. public class AccountFreeze {
  4. @TableId(type = IdType.INPUT)
  5. private String xid;
  6. private String userId;
  7. private Integer freezeMoney;
  8. private Integer state;
  9. public static abstract class State {
  10. public final static int TRY = 0;
  11. public final static int CONFIRM = 1;
  12. public final static int CANCEL = 2;
  13. }
  14. }

创建接口,使用 @TwoPhaseBusinessAction 注解

@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 context);
    boolean cancel (BusinessActionContext context);
}

实现接口和业务

@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 获取事务id
        String xid = RootContext.getXID();
        // 判断 freeze 是否有冻结记录
        AccountFreeze oldFreeze = freezeMapper.selectById(xid);
        if (oldFreeze != null) {
            // 已经处理过一次 CANCEL,无需重复处理
            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);
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        // 获取事务id
        String xid = context.getXid();
        // 根据 id 删除冻结记录
        return freezeMapper.deleteById(xid) == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        // 查询冻结记录
        String xid = context.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);
        // 空回滚判断
        if (freeze == null) {
            // 需要空回滚
            freeze.setUserId(context.getActionContext("userId").toString());
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);
            return true;
        }

        // 幂等判断
        if (freeze.getState() == AccountFreeze.State.CANCEL) {
            // 已经处理过了
            return true;
        }

        // 恢复可用余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 将冻结余额清零
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        return freezeMapper.updateById(freeze) == 1;
    }
}

修改 Controller 中使用的 Service

@Autowired
private AccountTCCService accountService;

SAGA 模式

Saga 模式是 Seata 提供的长事务解决方案。也分为两个阶段:

  • 一阶段:直接提交本地事务
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

Saga 模式优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高
  • 一阶段直接提交事务,无锁,性能好
  • 不用编写TCC中的三个阶段,实现简单

缺点:

  • 软状态持续时间不确定,时效性差
  • 没有锁,没有事务隔离,会有脏写

四种模式对比

对比内容 XA AT TCC SAGA
一致性 强一致 弱一致 弱一致 最终一致
隔离性 完全隔离 基于全局锁隔离 基于资源预留隔离 无隔离
代码侵入 有,要编写三个接口 有,要编写状态机和补偿业务
性能 非常好 非常好
场景 对一致性、隔离性有高要求的业务 基于关系型数据库的大多数分布式事务场景都可以 对性能要求较高的事务。有非关系型数据库要参与的事务。 业务流程长、业务流程多。参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口