0%

分布式事务-多种理论模式

2PC

第一阶段(提交事务)

  • 事务询问

    TM向所有的RM询问是否可以执行提交操作,并开始等待RM的响应。

  • 预执行事务

    RM预提交当前所有事务操作,并将相关信息写入Undo和Redo日志

  • 事务反馈

    RM提交事务后响应TM的询问。如果所有RM都提交成功,则返回一个“同意”消息;如果任一RM提交失败,则返回TM“终止”消息。

第二阶段(执行事务)

  • 提交

    当TM从所有RM获得的响应都为“同意”时,向所有RM发送“正式提交(commot)”的请求。

  • 回滚

    当TM接收到任一RM的“终止消息”时,向所有RM发送“回滚操作(rollback)”的请求;

    RM利用之前写入Undo Log的日志进行回滚,并释放整个事务占用的资源。

定义:TM:事务管理器、RM:资源管理器

总结:第一阶段预提交事务,当有RM出错后,TM于第二阶段通知所有RM回滚预提交事务。

2PC可能出现的问题
1、第一阶段持有连接未释放,占用资源高
2、第一阶段预提交成功之后,在第二阶段RM的执行失败,无法进行回滚

3PC

第一阶段(can commit)

  • 询问是否能够提交

    TM 向RM询问是否能够提交,RM全部响应“YES”后才进入第二阶段

第二阶段(pre commit)

  • 事务的预提交

    ​ 与2PC的第一阶段类似,进行事务的预提交

第三阶段(do commit )

  • 集体提交
  • 集体回滚

定义:TM:事务管理器、RM:资源管理器

总结:
1、增加了can commit阶段,降低锁定资源的概率和时长(当RM异常的时候,无需像2PC一样一开始就锁定,而是确认RM都正常的情况下才执行预提交,锁定资源)
2、增加超时机制
2.1、TM超时未抽到RM的反馈,主动给RM发送中断命令
2.2、RM在第三阶段超时未收到TM命令,默认提交

TCC

Try: 尝试执行业务

Confirm:确认执行业务

Cancel: 取消执行业务

具体实现参见LCNSEATA

问题

幂等性处理

因为网络抖动等原因,分布式事务框架可能会重复调用同一个分布式事务中的一个分支事务的二阶段接口(Confirm/Cancel)。所以分支事务的二阶段接口Confirm/Cancel需要能够保证幂等性。如果二阶段接口不能保证幂等性,则会产生严重的问题,造成资源的重复使用或者重复释放,进而导致业务故障。

空回滚

没有调用参与方Try方法的情况下,就调用了二阶段的Cancel方法,Cancel方法需要有办法识别出此时Try有没有执行。如果Try还没执行,表示这个Cancel操作是无效的,即本次Cancel属于空回滚;如果Try已经执行,那么执行的是正常的回滚逻辑

总而言之,Try方法没有执行成功,然而此时这笔分布式事务和分支事务已经落库。有两种情况会触发分布式事务的回滚:

  1. 发起方认为当前分布式事务无法成功,主动通知TC回滚
  2. TC发现分布式事务超时,被动触发回滚

资源悬挂

Cannel在Try之前执行

这种一阶段比二阶段执行的还晚的情况看似不可能,但是仔细考虑RPC调用的时序,其实这种情况在复杂多变的网络中是完全可能的,下面的时序展示了这种可能性:

  1. 发起方通过RPC调用参与者一阶段Try,但是发生网络阻塞导致RPC超时
  2. RPC超时后,TC会回滚分布式事务(可能是发起方主动通知TC回滚或者是TC发现事务超时后回滚),调用已注册的各个参与方的二阶段Cancel
  3. 参与方空回滚后,发起方对参与者的一阶段Try才开始执行,进行资源预留从而形成悬挂

处理方案

增加事件处理表,通过主事务ID分支事务ID分支事务处理状态(try、confirm、cancel三个状态)进行数据控制

幂等性处理:第二阶段进行事件表状态判断,第一阶段状态已执行的情况下,才执行第二阶段;第二阶段已经存在的情况下,不重复执行

空回滚:在执行第二阶段之前,对第一阶段是否执行进行判断

资源悬挂:通过事件状态进行控制,没有第一阶段状态时,第二阶段不执行具体业务逻辑;插入第二阶段记录,等待第一阶段到来时,判断到第二阶段状态时不处理

消息队列+事件表

当一个请求涉及到多个微服务系统时,可以采取消息队列加事件表的形式进行处理。

以接收第三方支付回调请求,更新支付流水记录、订单状态为例:

支付系统

  • 接收第三方请求,处理本地事务

  • 插入支付事件到事件表,状态为“新建”

    当前两个动作为一个事务,当处理失败一起回滚后,返回错误响应给第三方,等待第三方再次调起(支付回调接口有多次重试机制)

定时任务

  • 查询事件表,“新建”状态的支付事件

  • 更新时间表状态为“已发送”

  • 发送消息到消息队列

    当前三个动作为一个事务,消息发送失败可以让事务回滚,该“新建”时间等待下一次定时任务周期调起处理

订单系统

  • 消息监听者消费消息

  • 将消息插入事件表,状态为“已接收”,同时解决幂等性问题(业务key唯一索引)

    当前几个动作为一个事务,失败回滚,同时响应给mq,让其重试

    事件表唯一键冲突时,提交消息,不让mq重试

定时任务

  • 查询“已接收”状态的事件进行

  • 根据事件处理本地事务

  • 修改事件状态为“已处理”

    当前几个动作为一个事务,失败回滚,该“已接收”状态事件等待下一次定时任务周期调起处理

事务消息

参见rocketmq事务消息章节