分布式事务(三) 分布式事务解决方案

DTP和XA

分布式事务的解决方案之一就是两阶段提交协议(2PC:Two-Phase Commit)

1994年X/Open组织(现在的Open Group)定义了分布式事务处理的DTP模型,该模型包括这样几个角色:

  • 应用程序(AP):业务微服务
  • 事务管理器(TM):全局事务管理者
  • 资源管理者(RM):一般是数据库
  • 通信资源管理器(CRM):是TM和RM间的通信中间件

在该模型中,一个分布式事务(全局事务)可以被拆分成多个本地事务,运行在不同的AP和RM上,每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每个本地事务都能成功,若有一个失败,则所有其他事务都必须回滚。
问题是本地事务处理过程中,并不知道其他事务的运行状态,因此就需要通过CRM来通知各个本地事务,同步事务执行状态
因此各个本地事务的通信必须有一个统一的标准,否则不同数据库之间就无法通信,XA就是X/Open DTP中通信中间件和TM间联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。

2PC(两阶段提交)

参考:漫话分布式系统共识协议: 2PC/3PC篇

两阶提交协议就是根据这一思想衍生出来的,将全局事务拆分为两个阶段来执行。

  • 阶段一:准备阶段,各个本地事务完成本地事务的准备工作。
  • 阶段二:执行阶段,各个本地事务根据上一阶段执行结果,进行提交或回滚。
    这个过程中需要一个协调者(coordinator),还有事务的参与者(voter)。

正常情况

2pc

投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入undo和redo log,然后反馈事务执行成功。(agree)
提交阶段:协调组发现每个参与者都可以执行事务(agree),于是向各个事务参与者发出commit指令,各个事务参与者提交事务。

异常情况

2pc

投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入undo和redo log,然后反馈事务执行结果,但只要有一个参与者返回的是Disagree,就说明执行失败。
提交阶段:协调组发现每个有一个或多个参与者返回Disagree,认为执行失败,于是向各个事务参与者发出abort指令,各个事务参与者回滚事务。

缺陷

  1. 存在CRM单点故障问题
    协调者挂了之后就不知道别人什么情况。
    2pc

  2. 阻塞问题
    在准备阶段,提交阶段,每个事务参与者都会锁定本地资源,并等待其他事务的执行结果,阻塞时间较长。

总结

  1. 面对二阶段提交的缺点,后来又演变出来了三阶段提交,但是依旧没有解决阻塞和资源锁定的问题,而且引入了新的微,因此实际场景使用较少。
  2. 2PC拥有稳定成熟的框架支持,可以保证强一致,并且对代码侵入度不高,

使用场景

  • 适用于对效率要求不高,一致性要求高的场景
  • 代码侵入度小,不影响原有代码

3PC(三阶段提交)

TCC

TCC模式可以解决2PC中资源锁定和阻塞的问题,减少资源锁定时间。

基本原理

本质是一种补偿的机制,事务运行包括三个方法,

  • try:资源的检测和预留
  • confirm:执行业务操作提交;要求try成功confirm一定能成功
  • cancel:预留资源释放

执行分两个阶段:

  • 准备阶段(try):资源的检测和预留
  • 执行阶段(confirm/cancel):根据上一步的结果,判断下面的执行方法。如果上一步所有事务参与者都成功,这里执行confirm,否则执行cancel
tcc

看似与两阶段提交没有什么区别,其实差距很大。

  • try,confirm,cancel都是独立的事务,不受其他参与者的影响,不会阻塞等待他人。
  • try,confirm,cancel由程序员在业务层编写,锁粒度由代码控制。

实例

假如账户A原来有100元,需要从余额扣除30元。如图:
tcc

  • 一阶段(Try):余额检查,并冻结用户部分金额,此阶段执行完毕,事务已提交。
    • 检查用户余额是否足够,如果足够,冻结部分余额
    • 在账户表中添加冻结金额字段,值为30,余额不变
  • 二阶段
    • 提交(Confirm):真正的扣款,把冻结金额从余额中扣除,冻结金额清空。
      • 修改冻结金额=0,修改余额为100-30=79
    • 补偿(Cancel):释放之前冻结的金额,不是回滚
      • 余额不变,修改账户冻结金额=0

优势&缺点

优势

  • 避免资源长期锁定和阻塞等待操作。执行效率高。
    TCC执行的每个阶段都是独立的事务,不需要等待其他事务的结果。如果执行失败,不是执行的回滚而是执行的补偿

缺点

  • 代码侵入:需要人为编写try,confirm,cancel操作,对开发人员要求高
  • 开发成本高:一个业务需要拆分三个阶段,开发成本高
  • 安全性考虑:cancel动作如果执行失败,资源就无法释放。需要引入重试,但是引入重试又有可能造成重复执行问题,还需要考虑重试时的幂等问题。

使用场景

  • 对事务有一定的一致性要求(最终一致性)
  • 对性能要求较高
  • 开发人员具备较高的编码能力和幂等处理经验

可靠消息服务

源于eBay,设计思想是将远程分布式事务拆分成一系列的本地事务。

基本原理

事务分为发起者A参与者BCD

  • 事务发起者A执行本地事务
  • 事务发起者A通过MQ将需要执行的事务信息发送给事务参与者BCD
  • 事务参与者BCD接收到消息后执行本地事务
    mq

现实场景(老师给的场景,好像不恰当):

  1. 你点了一杯奶茶,付完钱。(发起者A完成)
  2. 服务员给了你一个号码牌。(参与者B)
  3. 你凭这张小票,一定能够领到一杯奶茶。(不断问服务员,重试)

本地消息表

为了避免消息发送失败或丢失,消息需要持久化到数据库。实现有简化版本和解耦合版本两种。

简化版本

mq
  • 事务发起者
    1. 开启本地事务
    2. 执行业务操作
    3. 发送消息到MQ
    4. 保存消息记录到数据库,记录为已发送
    5. 提交本地事务
  • 事务参与者
    1. 接收消息
    2. 开启本地事务
    3. 执行业务操作
    4. 修改数据库记录状态为已消费
    5. 提交本地事务
  • 额外的定时任务
    • 定时扫描数据库中超时未消费的,重新发送

优点&缺点

优点

  • 与TCC相比,不用写那么多代码。

缺点

  • 数据一致性完全依赖MQ,因此消息服务一定需要可靠。
  • 需要处理被动业务方的幂等问题。
  • 被动业务失败不会主动业务回滚,而是重试被动的业务。
  • 事务业务与消息业务耦合,发起者业务完成后需要写消息表

独立消息服务

通过引入独立的消息服务,完成对消息的持久化、发送、确认、失败重试等行为。
mq

RocketMQ事务消息

自带事务消息,可以保证消息的可靠性,原理就是自带了一个本地消息表。

RocketMQ-事务消息

RabbitMQ的消息确认

RabbitMQ确保消息不丢失使用的是ACK确认机制。

  • 生产者确认机制:确保从生产者 -> MQ不丢失
    • 生产者发送消息到mq,设置异步监听MQ的ACK
    • MQ接收到消息,返回ACK到生产者
      1. 消息到exchange但是route失败,返回失败ACK
      2. 消息route成功,持久化失败,返回失败ACK
      3. 消息route成功,持久化成功,返回成功ACK
    • 生产者提前编写好不同回调的处理方式
      1. 成功,记录日志
      2. 失败,一段时间后重新发送
  • 消费者确认机制:确保从MQ -> 消费者正确消费
    • 消费者开启监听时开启手动ACK模式
    • mq将消息发送到消费者后,会等待消费者ACK
      1. 收到消费者ACK才会删除消息
      2. 一直没有收到ACK则一直保存
      3. 消费者断开或异常后,发送给其它消费者(此处需要做好幂等)
    • 消费者处理完消息,提交事务,手动ACK。如果异常,则不会ACK,业务处理失败。等待处理下一条消息。

事务消息优点&缺点

  • 优点
    • 业务简单,不需要写三个阶段的代码
    • 多个本地事务,缩短资源锁定时间,性能高
  • 缺点
    • 代码侵入度高
    • 一致性依赖MQ的可靠性
    • 消息发起者可以回滚,参与者无法引起全局事务回滚
    • 时效性差,完全取决于MQ的消息发送是否及时,消费是否及时。

针对缺点3,无法回滚问题。有人提出可以让事务参与者失败后,利用MQ通知消息服务,由消息服务通知其他参与者回滚,可是这是在利用MQ重新实现了2PC,又是一个轮子。没必要

Seata

官网:http://seata.io/

Seata是阿里开源的框架,支持XA,AT,TCC,SAGA四种模式。

AT模式

2019年1月份,Seata开源了AT模式。AT模式是一种无侵入的分布式事务解决方案。是对TCC或两阶段提交模型的优化,解决了TCC模式代码侵入,编码复杂等问题。

在AT模式下,用户只需要关注自己业务的"sql",用户的业务SQL作为“一阶段”,Seata框架会解析SQL自动生成事务的“二阶段”提交或回滚操作。

Seata官方文档:seata.io

at-1

乍一看和TCC协议很像,都是分两阶段提交。

  • 一阶段:执行本地事务,并返回执行结果
  • 二阶段:根据一阶段的结果,决定执行提交或回滚
    但是AT模式底层完全不同,二阶段完全不需要我们编码,框架帮我们实现了。我们写的代码和本地事务代码完全一致,无需手动处理分布式事务。

一阶段

一阶段,Seata会拦截业务SQL,首先解析SQL语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存为before image,然后执行业务SQL,更新业务数据,在业务数据执行之后,再将其保存为after image,最后获取全局行锁,提交事务。以上所有操作都是在一个本地事务中完成,保证了一阶段的原子性。

before imageafter image类似于数据库的undo log和redo log。

at-2

二阶段提交

二阶段如果是提交的话,因为业务SQL在一阶段已经提交到数据库,所以Seata框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

二阶段回滚

回滚可直接使用before image还原业务数据,但在还原前首先要检查脏写,对比数据库当前业务数据after image,如果两份完全一致则没有脏写,可以还原;如果数据不一致,说明出现了脏写,需要人工处理。

详细架构和流程

基本概念

  • TC(Transaction Coordinator):事务协调者,维护全局和分支事务的状态,驱动全局事务提交或回滚(TM之间的协调者)
  • TM(Transaction Manager):事务管理器,定义全局事务的范围:开启全局事务,提交或回滚全局事务
  • RM(Resource Manager):资源管理器,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
at-3
  • TM:业务模块中全局事务的开启者
    • 向TC开启一个全局事务
    • 调用其它微服务
  • RM:业务模块执行者中,包含RM部分,负责向TC汇报事务执行状态
    • 执行本地事务
    • 向TC注册分支事务,并提交本地事务执行结果
  • TM:结束对微服务的调用,通知TC,全局事务执行完毕,一阶段结束
  • TC:汇总各个分支事务执行结果,决定分布式事务是提交还是回滚
  • TC:通知所有RM提交/回滚资源,二阶段结束

详细流程

  • 一阶段
    • TM开启全局事务,并向TC声明全局事务,包括全局事务XID信息
    • TM所在服务调用其他微服务
    • 微服务,主要由RM执行
      • 查询before image
      • 执行本地事务
      • 查询after image
      • 生成undo log写入数据库
      • 向TC注册分支事务,并告知事务执行结果
      • 获取全局锁(防止其他全局事务并发修改当前数据)
      • 释放本地锁(不影响其他业务对数据的操作)
  • 二阶段
    • TC 统计各个分支事务的执行情况,根据情况做对应的操作
      • 都成功:通知分支事务,提交事务
      • 有分支失败:通知执行成功的事务,回滚数据(做补偿)
    • 分支事务RM
      • 提交事务:直接清空before imageafter image信息,释放全局锁
      • 回滚事务
        • 校验after image,判断是否有脏写
        • 如果没有脏写,回滚到before image,清空before imageafter image
        • 如果有脏写,需要人工干预

优点&缺点

  • 优点
    1. 一阶段无锁,高性能
    2. 补偿服务易于实现
  • 缺点
    1. 解析,查询和保存undo log的blob字段徒增性能损耗,根据二八定理,80%的业务是不会出现问题,而需要接受这部分写入log的损耗
    2. 不保证隔离性,一阶段完成后仅维护着全局事务锁XID提交了本地事务。

适用场景

  • 业务流程长,业务逻辑多的地方。
  • 历史遗留代码无法修改的情况