XA 模式
XA 规范
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的 TM 与局部的 RM 之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
Seata 的 XA 模式
Seata 的 XA 模式做了一些调整,但大体相似:
RM 一阶段的工作:
- 注册分支事务到 TC
- 执行分支业务 SQL 但不提交
- 报告执行状态到 TC
TC 二阶段的工作:
- TC 检测各分支事务执行状态
- 如果都成功,通知所有 RM 提交事务
- 如果有失败,通知所有 RM 回滚事务
RM 二阶段的工作:
- 接收 TC 指令,提交或回滚事务
XA 模式的优点:
- 事务的强一致性,满足 ACID 原则。
- 常用数据库都支持,实现简单,并且没有代码侵入
XA 模式的缺点:
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
Seata 实现 XA 模式
Seata 的 starter 已经完成了 XA 模式的自动装配,实现非常简单,步骤如下:
修改 application.yml 文件(每个参与事务的微服务),开启 XA 模式:
seata:data-source-proxy-mode: XA # 开启数据源代理的XA模式
给发起全局事务的入口方法添加 @GlobalTransactional 注解,本例中是SeataOrderServiceImpl 中的 create 方法:
@Override@Transactional@GlobalTransactionalpublic Long create(SeataOrder order) {// 创建订单orderMapper.insert(order);try {// 扣用户余额accountClient.deduct(order.getUserId(), order.getMoney());// 扣库存storageClient.deduct(order.getCommodityCode(), order.getCount());} catch (FeignException e) {log.error("下单失败,原因:{}", e.contentUTF8(), e);throw new RuntimeException(e.contentUTF8(), e);}return order.getId();}
重启服务并测试
AT 模式
AT 模式介绍
AT 模式同样是分阶段提交的事务模型,不过缺弥补了 XA 模型中资源锁定周期过长的缺陷。
阶段一 RM 的工作:
- 注册分支事务
- 记录 undo-log(数据快照)
- 执行业务 SQL 并提交
- 报告事务状态
阶段二提交时 RM 的工作:
- 删除 undo-log 即可
阶段二回滚时 RM 的工作:
- 根据 undo-log 恢复数据到更新前
简述 AT 模式与 XA 模式最大的区别是什么?
- XA 模式一阶段不提交事务,锁定资源;AT 模式一阶段直接提交,不锁定资源。
- XA 模式依赖数据库机制实现回滚;AT 模式利用数据快照实现数据回滚。
- XA 模式强一致;AT 模式最终一致
AT 模式的脏写问题
因为事务之间没有隔离,存在脏写问题
解决方法:
全局锁:由 TC 记录当前正在操作某行数据的事务,该事务持有全局锁,具备执行权。
对于非 Seata 管理的业务
AT 模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT 模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比 XA 模式要好很多
Seata 实现 AT 模式
AT 模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。
- 导入 SQL 文件:seata-at.sql,其中 lock_table 导入到 TC 服务关联的数据库,undo_log 表导入到微服务关联的数据库
修改 application.yml 文件,将事务模式修改为 AT 模式即可
seata:data-source-proxy-mode: AT # 开启数据源代理的AT模式
服务添加
@GlobalTransactional注解- 重启服务并测试
TCC 模式
TCC 模式介绍
TCC 模式与 AT 模式非常相似,每阶段都是独立事务,不同的是 TCC 通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留;
- Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
- Cancel:预留资源释放,可以理解为 Try 的反向操作。
举例,一个扣减用户余额的业务。假设账户 A 原来余额是 100,需要余额扣减 30 元。
- 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加 30 元,可用余额扣除 30
- 阶段二:假如要提交(Confirm),则冻结金额扣减 30
- 阶段二:如果要回滚(Cancel),则冻结金额扣减 30,可用余额增加 30
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 和执行状态,为此我们设计了一张表:
CREATE TABLE `account_freeze_tbl` (`xid` varchar(128) NOT NULL,`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',`state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',PRIMARY KEY (`xid`) USING BTREE) 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 方法都需要在接口中基于注解来声明,语法如下
@LocalTCCpublic interface TCCService {/*** Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法*/@TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel")void prepare(@BusinessActionContextParameter(paramName = "param") String param);/*** 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致** @param context 上下文,可以传递try方法的参数* @return boolean 执行是否成功*/boolean confirm (BusinessActionContext context);/*** 二阶段回滚方法,要保证与rollbackMethod一致*/boolean cancel (BusinessActionContext context);}
seata-demo 数据库中引入 account_freeze_tbl.sql 数据表,创建对应实体类和Mapper
@Data@TableName("account_freeze_tbl")public class AccountFreeze {@TableId(type = IdType.INPUT)private String xid;private String userId;private Integer freezeMoney;private Integer state;public static abstract class State {public final static int TRY = 0;public final static int CONFIRM = 1;public final static int CANCEL = 2;}}
创建接口,使用 @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 模式要求的三个接口 |
