分布式事务比较

188 阅读8分钟

图片.png

xa和at的不同

1、XA规范主要定义了:

1、(全局)事务管理器(TM)

2、(局部)资源管理器(RM)之间的接口

本地的数据库如mysql在XA中扮演的是RM角色

第一阶段(prepare):即所有的参与者RM准备执行事务并锁住需要的资源。参与者ready时,向TM报告已准备就绪。

第二阶段 (commit/rollback):当事务管理者(TM)确认所有参与者(RM)都ready后,向所有参与者发送commit命令。

2、 AT的角色和XA一样分为3个

AT 这种事务模式是阿里开源的seata主推的事务模式

  • RM 资源管理器,是业务服务,负责本地数据库的管理,与XA中的RM一致
  • TC 事务协调器,是Seata服务器,负责全局事务的状态管理,负责协调各个事务分支的执行,相当于XA中的TM
  • TM 事务管理器,是业务服务,负责全局事务的发起,相当于XA中的APP

AT 的第一阶段为prepare,它在这一阶段会完成以下事情:

  1. RM 侧,用户开启本地事务

  2. RM 侧,用户每进行一次业务数据修改,假设是一个update语句,那么 AT 会做以下内容:

    1. 根据update的条件,查询出修改前的数据,该数据称为BeforeImage
    2. 执行update语句,根据BeforeImage中的主键,查询出修改后的数据,该数据称为AfterImage
    3. 将BeforeImage和AfterImage保存到一张undolog表
    4. 将BeforeImage中的主键以及表名,该数据称为lockKey,记录下来,留待后续使用
  3. RM 侧,用户提交本地事务时,AT 会做以下内容:

    1. 将2.4中记录的所有的lockKey,注册到 TC(即事务管理器seata)上
    2. 3.1中的注册处理会检查 TC 中,是否已存在冲突的主键+表名,如果有冲突,那么AT会睡眠等待后重试,没有冲突则保存
    3. 3.1成功完成后,提交本地事务

如果 AT 的第一阶段所有分支都没有错误,那么会进行第二阶段的commit,AT 会做以下内容:

  1. TC 会将当前这个全局事务所有相关的lockKey删除
  2. TC 通知与当前这个全局事务相关的所有业务服务,告知全局事务已成功,可以删除undolog中保存的数据
  3. RM 收到通知后,删除undolog中的数据

如果 AT 的第一阶段有分支出错,那么会进行第二阶段的rollback,AT 会做以下内容:

  1. TC 通知与当前这个全局事务相关的所有业务服务,告知全局事务失败,执行回滚

  2. RM 收到通知后,对本地数据的修改进行回滚,回滚原理如下:

    1. 从undolog中取出修改前后的BeforeImage和AfterImage
    2. 如果AfterImage与数据库中的当前记录校验一致,那么使用BeforeImage中的数据覆盖当前记录
    3. 如果AfterImage与数据库中的当前记录不一致,那么这个时候发生了脏回滚,此时需要人工介入解决
  3. TC 待全局事务所有的分支,都完成了回滚,TC 将此全局事务所有的lockKey删除

存在问题

NPC的挑战

我们以分布式事务中的TCC作为例子,看看NP带来的影响。

一般情况下,一个TCC回滚时的执行顺序是,先执行完Try,再执行Cancel,但是由于N,则有可能Try的网络延迟大,导致先执行Cancel,再执行Try。

这种情况就引入了分布式事务中的两个难题:

  • 空补偿:  Cancel执行时,Try未执行,事务分支的Cancel操作需要判断出Try未执行,这时需要忽略Cancel中的业务数据更新,直接返回
  • 悬挂:  Try执行时,Cancel已执行完成,事务分支的Try操作需要判断出Cancel已执行,这时需要忽略Try中的业务数据更新,直接返回

分布式事务还有一类需要处理的常见问题,就是重复请求

  • 幂等:  由于任何一个请求都可能出现网络异常,出现重复请求,所有的分布式事务分支操作,都需要保证幂等性

因为空补偿、悬挂、重复请求都跟NP有关,我们把他们统称为子事务乱序问题。在业务处理中,需要小心处理好这三种问题,否则会出现错误数据。

现有方案的问题

我们看到开源项目dtm之外,包括各云厂商,各开源项目,他们给出的业务实现建议大多类似如下(这也是大多数用户最容易想到的方案):

  • 空补偿:  “针对该问题,在服务设计时,需要允许空补偿,即在没有找到要补偿的业务主键时,返回补偿成功,并将原业务主键记录下来,标记该业务流水已补偿成功。”
  • 防悬挂:  “需要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝执行该笔服务,以免造成数据不一致。”

上述的这种实现,能够在大部分情况下正常运行,但是上述做法中的“先查后改”在并发情况下是容易掉坑里的,我们分析一下如下场景:

  • 正常执行顺序下,Try执行时,在查完没有空补偿记录的业务主键之后,事务提交之前,如果发生了进程暂停P,或者事务内部进行网络请求出现了拥塞,导致本地事务等待较久
  • 全局事务超时后,Cancel执行,因为没有查到要补偿的业务主键,因此判断是空补偿,返回
  • Try的进程暂停结束,最后提交本地事务
  • 全局事务回滚完成后,Try分支的业务操作没有被回滚,产生了悬挂

事实上,NPC里的P和C,以及P和C的组合,有很多种的场景,都可以导致上述竞态情况,就不一一赘述了。

虽然这种情况发生的概率不高,但是在金融领域,一旦涉及金钱账目,那么带来的影响可能是巨大的。

PS:幂等控制如果也采用“先查再改”,也是一样很容易出现类似的问题。解决这一类问题的关键点是要利用唯一索引,“以改代查”来避免竞态条件。

子事务屏障

子事务屏障技术的原理是,在本地数据库,建立分支操作状态表dtm_barrier,唯一键为全局事务id-分支id-分支操作(try|confirm|cancel)

  1. 开启本地事务
  2. 对于当前操作op(try|confirm|cancel),insert ignore一条数据gid-branchid-op,如果插入不成功,提交事务返回成功(常见的幂等控制方法)
  3. 如果当前操作是cancel,那么在insert ignore一条数据gid-branchid-try,如果插入成功(注意是成功),则提交事务返回成功
  4. 调用屏障内的业务逻辑,如果业务返回成功,则提交事务返回成功;如果业务返回失败,则回滚事务返回失败

在此机制下,解决了乱序相关的问题

  • 空补偿控制--如果Try没有执行,直接执行了Cancel,那么3中Cancel插入gid-branchid-try会成功,不走屏障内的逻辑,保证了空补偿控制
  • 幂等控制--2中任何一个操作都无法重复插入唯一键,保证了不会重复执行
  • 防悬挂控制--Try在Cancel之后执行,那么Cancel会在3中插入gid-branchid-try,导致Try在2中不成功,就不执行屏障内的逻辑,保证了防悬挂控制

对于SAGA、二阶段消息,也是类似的机制。

3 TCC方案 柔性事务

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

TCC方案是一种补偿型事务,该方案要求每个服务提供try、confirm、cancel三个接口。其核心思想是通过对于资源的预留(提供中间态,如资金的冻结、库存的预留等),尽早释放对于资源的锁定,如果Try阶段判断业务能够正常执行,则执行Confirm方法,完成对于资源的修改。如果不能够正常执行,则执行Cancel方法,释放对于资源的锁定。

  • Try:尝试执行业务,完成所有业务检查,进行业务资源的预留
  • Confirm:真正执行业务操作,不在做业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性
  • Cancel:取消执行业务,并释放Try阶段预留的业务资源

4、本地消息表方案

都是通过mq

5 可靠消息最终一致性方案

参考 :juejin.cn/post/716346…