数据库事务--分布式事务(4)

谈及分布式事务,就必须涉及多个服务,多个数据源。既然还是属于事务,那么目的还是尽可能保证数据一致性,只不过手段上,不能像传统事务那么简单的在单数据源上解决问题。

例子引入

下面笔者尝试根据自己有限的理论介绍清楚分布式事务,到底解决什么问题,各个解决方案有什么问题。
首先引入,分布式事务中常用例子,一个电商场景中用户购买商品,涉及仓储服务,订单服务,账户服务。

用代码来表示就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderServiceImpl{
@Resource
public StockService stockService;
@Resource
public UserService userService;
@Resource
public OrderMapper orderMapper;

public boolean order(){
// 保存订单
orderMapper.save();
// 预扣库存
stockService.save();
// 扣减资金
userService.save();
}
}

上面的执行顺序还有待考量,在实践中尽可能将本地操作,和最容易出错的操作先执行。如果我们可以保证每个 sql 都可以执行成功,那么确实也不需要什么额外措施。然而,物理机器和网络是不可靠的,任意的 sql 执行失败,都会导致数据的不一致。
比如,我们很自然想到为方法加上本地事务(可以简单的为方法加上@Transaction 注解),当订单服务,库存服务的 sql 执行失败,都可以正确的回滚。而当账户服务的 sql 执行失败了,抛出了异常,订单服务可以正常回滚,然而库存服务无法感知到这一点,数据不一致了。
这里存在问题的根因是,不同服务之间的状态无法感知,那么,让服务之间互相感知一般有两种方式,一种是服务之间互相通信,另一种有一个中心管理者来管理所有服务,显然第一种方案的通信成本会随着服务数量增多而显著增加。所以,工程上,一般都是添加第三方管理者

2PC

引入一个中心管理者,其核心思想其实就是二阶段提交。
第一阶段:各个微服务提交事务,并且将执行状态上报给中心管理者
第二阶段:中心管理者根据各个微服务的执行情况,给与反馈,只有所有微服务都成功了,才能提交分布式事务,否则就回滚

3PC

  1. 3pc 比 2pc 多了一个 can commit 阶段,减少了不必要的资源浪费。因为 2pc 在第一阶段会占用资源,而 3pc 在这个阶段不占用资源,只是校验一下 sql,如果不能执行,就直接返回,减少了资源占用。(这里资源占用是指对资源进行加锁了)
  2. 引入超时机制。同时在协调者和参与者中都引入超时机制。
    2pc: 只有协调者有超时机制,超时后,发送回滚指令。
    3pc: 协调者和参与者都有超时机制。
    协调者超时: can commit,pre commit 中,如果收不到参与者的反馈,则协调者向参与者发送中断指令。
    参与者超时: pre commit 阶段,参与者进行中断; do commit 阶段,参与者进行提交。

实践

基于上述的理论,工程上已经有多种模式可以使用了。

XA 模式

XA 其实可以理解为分布式事务处理(DTP,distribute transaction process)的一个处理规范协议,有很多数据库已经支持了。
XA 模式将组件分为三种角色
AP(application):开启全局事务的应用程序
TM(transaction manager):全局事务管理器,管理全局事务和各个子事务的状态
RM(resource manager):资源管理器,参与者需要实现 XA 协议,因此一般都是数据库来承担。可见这个模式对代码侵入性比较小
执行过程:

  1. 准备阶段:rm 本地提交本地子事务,但不写入最后的 commit record(即:不释放锁资源)。将执行结果通知给 tm
  2. 执行阶段:tm 根据各个子事务的结果,让 rm 们进行 commit/rollback
    特点:
  3. 强一致性
  4. 性能差劲:整个过程有三次持久化(准备阶段 rm 写 redo 日志,tm 做状态持久化,提交阶段 rm 写入提交日志),两次远程调用。而且具有木桶效应,最慢的 rm 不完成任务,其他 rm 不能释放锁资源
  5. 单点效应:rm 宕机了,可以在 rm 的提交结果过程引入超时机制,但 tm 的回复过程宕机了,无法处理。需要做好 tm 集群的高可用

AT 模式

在 AT 模式下,业务基本无感知的,都是通过 seate 做了一层代理来实现功能的。
事务执行过程:

  1. 准备阶段:Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成框架行锁。以上操作全部在一个数据库事务内完成。
  2. 执行阶段:
    1. commit:将“before image”和“after image”删除,释放框架行锁
    2. rollback:将对应的数据和“after image”进行对比,如果数据一致,说明这个过程没有发生脏写(虽然有行锁,但是这是框架层面的,不能排除有其他数据库连接修改了数据,还是可能会脏写),直接把数据还原为“before image”。如果数据不一致,则必须人工介入
      可以看出 AT 模式和 XA 模式特别像,都是 2PC 的典型应用,不过有两点不同:
  3. AT 模式的 RM 是业务应用,不是数据库处理的。
  4. AT 模式在准备阶段会直接提交本地事务,不会占用锁资源,避免了木桶效应。(这里值得一提的是,seata 虽然释放了本地锁资源,但是会保存一个框架级别的“全局锁”,这个锁是行锁,用于做写隔离效果的)
    这个模式是一个使用比较广泛的分布式事务解决方案

TCC 模式

上面介绍的两种模式都是业务侵入性比较小的方案。也正是因为它的业务侵入性比较小,导致它的灵活性不够,只能基于 sql 维度解决问题。
TCC 主要有三个步骤,也是两个阶段:

  • Try:预留资源
  • Confirm:执行的业务操作提交
  • Cancel:预留的资源释放
    这三个步骤的划分都是基于业务上的,我们在使用这个模式的时候首先就得把业务模型拆为两阶段。
    举一个扣 100 元的例子,try 阶段就得冻结用户 100 元,confirm 阶段就直接提交,cancel 阶段就撤销冻结操作。但是,“冻结”操作我们有很多实现方式,可以新建一张金钱冻结数据表,或者依据什么高可靠的消息队列,不需要像 XA 模式保留锁资源,或者像 AT 模式保留一个框架全局行锁。这种模式的灵活性高很多,带来的性能也会高很多。就是对业务侵入比较强。
    在此基础上再提出几个只有再业务层面才能实现的技术点:
  1. 允许空回滚:try 操作失败了,rm 收到了 cancel 命令,此时 rm 没有发现事务 id 时也需要能回滚成功
  2. 防悬挂设计:悬挂的意思是 cancel 比 try 更先执行。比如,try 操作超时了,cancel 回滚成功,此时 try 命令又来了,rm 就不应该可以再执行 try 命令了,否则就会数据不一致
  3. 幂等性:其实,分布式系统的场景下,尽可能所有接口都设计成幂等的。因为网络是不稳定的,我们会通过重试操作来减少这种不稳定带来的影响,术语叫做“最大努力交付”。
    这些技术点,基本都需要业务的参与。

SAGA 模式

这个模式的核心思想是将一个长的分布式事务,分解为一个个子事务,并且为子事务设计补偿操作。只要所有子事务可以正确提交,分布式事务就算正确提交了。如果有的子事务无法正确提交,那么有两种操作:

  1. 正向恢复:重复尝试失败的子事务,做到“最大努力交付”,这种适用于事务必须要成功的场景
  2. 反向恢复:执行目前已经执行了的子事务的补偿操作。如果补偿操作失败,也得做到“最大努力交付”。否则让人工介入
    SAGA 必须保证所有子事务都得提交或者补偿,但 SAGA 系统本身也可能崩溃,所以系统本身也得做到高可用。
    这个模式的业务侵入性也很强,因为补偿是在业务层面做的。这个和回滚不太一样,它需要业务的参与。比如扣 100 元钱,补偿是指加上 100 元,而回滚是撤销扣钱的操作。

总结

事务的目的是保证数据一致性,本地事务的执行过程保证了无论系统在什么场景下,数据都是一致的。
但是在分布式的场景下,可以看到,这四种模式有各自的优缺点,没有一种模式适合所有的场景,因地制宜才是最好的选择。
分布式系统的引入,带来了机遇,也带来了技术挑战,分布式系统在极端场景下需要人工的介入才能保证数据一致性。
当然,除了分布式事务这种“很重”的操作,在很多业务场景中,我们还是会选择使用“最终一致性”这种性能更高的方案,还是要因“场景”制宜的。

参考链接

seata 官网: Seata AT 模式
AT 模式的行锁:Seata AT模式原来是这样实现行锁的 - 掘金 (juejin.cn)
《凤凰架构》:凤凰架构:构建可靠的大型分布式系统 | 凤凰架构 (icyfenix.cn)


数据库事务--分布式事务(4)
http://ttoobbyyy.github.io/2024/08/14/数据库事务--分布式事务(4)/
作者
jianren.xiao
发布于
2024年8月14日
许可协议