分布式事务的这些常见用法都有坑,来看看正确姿势

处理NPC 分布式系统最大的敌人可能就是NPC了,在这里它是Network Delay, Process Pause, Clock Drift的首字母缩写。我们先看看具体的NPC问题是什么:

  • Network Delay,网络延迟。虽然网络在多数情况下工作的还可以,虽然TCP保证传输顺序和不会丢失,但它无法消除网络延迟问题。
  • Process Pause,进程暂停。有很多种原因可以导致进程暂停:比如编程语言中的GC(垃圾回收机制)会暂停所有正在运行的线程;再比如,我们有时会暂停云服务器,从而可以在不重启的情况下将云服务器从一台主机迁移到另一台主机。我们无法确定性预测进程暂停的时长,你以为持续几百毫秒已经很长了,但实际上持续数分钟之久进程暂停并不罕见。
  • Clock Drift,时钟漂移。现实生活中我们通常认为时间是平稳流逝,单调递增的,但在计算机中不是。计算机使用时钟硬件计时,通常是石英钟,计时精度有限,同时受机器温度影响。为了在一定程度上同步网络上多个机器之间的时间,通常使用NTP协议将本地设备的时间与专门的时间服务器对齐,这样做的一个直接结果是设备的本地时间可能会突然向前或向后跳跃。
分布式事务既然是分布式的系统,自然也有NPC。分布式事务可分为
  • 分布式数据库内部的分布式事务:NPC中的C给这类应用,带来了极大的挑战,例如Spanner采用了原子钟,并且增加了Commit-Wait来解决问题。TiDB采用单点授时,引入了一个单点。
  • 跨数据库的异构分布式事务:这是我们这篇文章讨论的主题,因为没有涉及时间戳,所以NPC带来的困扰主要是NP。
TCC的空补偿与悬挂 我们以分布式事务中的TCC(Try,Confirm,Cancel)作为例子,关于TCC模式的详细介绍可以参考:分布式事务最经典的七种解决方案
一般情况下,一个TCC回滚时的执行顺序是,先执行完Try,再执行Cancel,但是由于N,则有可能Try的网络延迟大,导致先执行Cancel,再执行Try。
这种情况就引入了分布式事务中的两个难题:
  1. 空补偿:Cancel执行时,Try未执行,事务分支的Cancel操作需要判断出Try未执行,这时需要忽略Cancel中的业务数据更新,直接返回
  2. 悬挂:Try执行时,Cancel已执行完成,事务分支的Try操作需要判断出Cancel一致性,这时需要忽略Try中的业务数据更新,直接返回
分布式事务还有一类需要处理的常见问题,就是重复请求,业务需要做幂等处理。因为空补偿、悬挂、重复请求都跟NP有关,我们把他们统称为子事务乱序问题。在业务处理中,需要小心处理好这三种问题,否则会出现错误数据。
现有方案的问题 我看到开源项目dtm之外,包括各云厂商,各开源项目,他们给出的业务实现建议大多类似如下:
  • 空补偿:“针对该问题,在服务设计时,需要允许空补偿,即在没有找到要补偿的业务主键时,返回补偿成功,并将原业务主键记录下来,标记该业务流水已补偿成功。”
  • 防悬挂:“需要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝执行该笔服务,以免造成数据不一致。”
上述的这种实现,能够在大部分情况下正常运行,但是上述做法中的“先查后改”在并发情况下是容易掉坑里的,我们分析一下如下场景:
  • 正常执行顺序下,Try执行时,在查完没有空补偿记录的业务主键之后,事务提交之前,如果发生了进程暂停P,或者事务内部进行网络请求出现了拥塞,导致本地事务等待较久
  • 全局事务超时后,Cancel执行,因为没有查到要补偿的业务主键,因此判断是空补偿,直接返回
  • Try的进程暂停结束,最后提交本地事务
  • 全局事务回滚完成后,Try分支的业务操作没有被回滚,产生了悬挂
事实上,NPC里的P和C,以及P和C的组合,有很多种的场景,都可以导致上述竞态情况,就不一一赘述了。
【分布式事务的这些常见用法都有坑,来看看正确姿势】虽然这种情况发生的概率不高,但是在金融领域,一旦涉及金钱账目,那么带来的影响可能是巨大的。
PS:幂等控制如果也采用“先查再改”,也是一样很容易出现类似的问题。解决这一类问题的关键点是要利用唯一索引,“以改代查”来避免竞态条件。
子事务屏障技术 下面我们来详解dtm是如何解决这个问题的。
dtm首创了子事务屏障技术,用于同时解决空补偿、防悬挂、幂等这三个问题,对于TCC事务,他的详细工作过程如下:
  1. 在本地数据库中创建好子事务屏障表dtm_barrier.barrier,唯一索引为gid-branchid-branchop
  2. 对于Try、Confirm、Cancel操作,insert ignore一条记录gid-branchid-try|confirm|cancel,如果影响行数为0(重复请求、悬挂),直接提交返回
  3. 对于Cancel操作额外再insert ingore一条记录 gid-branchid-try,如果影响行数为1(空补偿),直接提交返回
  4. 执行业务逻辑并提交返回,如果业务发生错误则回滚
假如Try和Cancel的执行时间没有重叠,那么读者容易分析出上述过程能够解决空补偿和悬挂问题。如果出现了Try和Cancel执行时间重叠的情况,我们看看会发生什么。
假设Try和Cancel并发执行,Cancel和Try都会插入同一条记录gid-branchid-try,由于唯一索引冲突,那么两个操作中只有一个能够成功,而另一个则会等持有锁的事务完成后返回。
  • 情况1,Try插入gid-branchid-try失败,Cancel操作插入gid-branchid-try成功,此时就是典型的空补偿和悬挂场景,按照子事务屏障算法,Try和Cancel都会直接返回
  • 情况2,Try插入gid-branchid-try成功,Cancel操作插入gid-branchid-try失败,按照上述子事务屏障算法,会正常执行业务,而且业务执行的顺序是Try在Cancel前
  • 情况3,Try和Cancel的操作在重叠期间又遇见宕机等情况,那么至少Cancel会被dtm重试,那么最终会走到情况1或2。
综上各种情况的详细论述,子事务屏障能够在各种NP情况下,保证最终结果的正确性。
事实上,子事务屏障有大量优点,包括:
  • 两个insert判断解决空补偿、防悬挂、幂等这三个问题,比其他方案的三种情况分别判断,逻辑复杂度大幅降低
  • dtm的子事务屏障是SDK层解决这三个问题,业务完全不需要关心
  • 性能高,对于正常完成的事务(一般失败的事务不超过1%),子事务屏障的额外开销是每个分支操作一个SQL,比其他方案代价更小。
上述的理论与分析过程也同样适用于SAGA。dtm里面的子事务屏障同时支持了TCC和SAGA两种事务模式。
Java接入 dtm是首个支持自动处理子事务乱序问题的开源框架,极大的减轻了业务负担,降低了分布式事务的使用门槛。
dtm已经提供了Java的SDK,包含了子事务屏障功能,帮助用户自动处理子事务乱序问题。DTM提供的是一套SDK形式(非注解形式)的接口,易于理解和使用。
我们编写了一个非常完整的示例,该示例的主要功能模拟了一个跨行转账(跨服务转账)的业务场景,包含的内容有:如何组织子事务、如何做冻结资金、如何处理失败、如何使用子事务屏障。您在这个例子的基础上改一改,就能够完成您实际的TCC分布式事务。
这篇文章用Java轻松完成一个分布式事务TCC,自动处理空补偿、悬挂、幂等给出了详细的Java TCC事务原理与接入过程,方便用户快速上手。
小结 阅读完这篇干货文,希望这里介绍的子事务屏障技术能够帮助你更好更正确的完成你的分布式事务。
我们不仅提供了前面Java的子事务屏障SDK,还提供了go、python版本的子事务屏障,有需要可以自取:dtm
如果您有分布式事务相关的业务需求,dtm能够支持多种语言,并且简单易上手。
如果您想要学习分布式事务相关的知识,dtm的文档备受好评,能够让读者快速入门分布式事务,理论结合实践,让读者逐步深入。
欢迎大家访问dtm,欢迎Issue、PR、Star

    推荐阅读