用Java轻松完成一个分布式事务TCC,自动处理空补偿、悬挂、幂等
作者:叶东富
来源:SegmentFault 思否社区
处理NPC
分布式系统最大的敌人可能就是NPC了,在这里它是Network Delay, Process Pause, Clock Drift的首字母缩写。我们先看看具体的NPC问题是什么:
Network Delay,网络延迟。虽然网络在多数情况下工作的还可以,虽然TCP保证传输顺序和不会丢失,但它无法消除网络延迟问题。
Process Pause,进程暂停。有很多种原因可以导致进程暂停:比如编程语言中的GC(垃圾回收机制)会暂停所有正在运行的线程;再比如,我们有时会暂停云服务器,从而可以在不重启的情况下将云服务器从一台主机迁移到另一台主机。我们无法确定性预测进程暂停的时长,你以为持续几百毫秒已经很长了,但实际上持续数分钟之久进程暂停并不罕见。
Clock Drift,时钟漂移。现实生活中我们通常认为时间是平稳流逝,单调递增的,但在计算机中不是。计算机使用时钟硬件计时,通常是石英钟,计时精度有限,同时受机器温度影响。为了在一定程度上同步网络上多个机器之间的时间,通常使用NTP协议将本地设备的时间与专门的时间服务器对齐,这样做的一个直接结果是设备的本地时间可能会突然向前或向后跳跃。
分布式数据库内部的分布式事务:NPC中的C给这类应用,带来了极大的挑战,例如Spanner采用了原子钟,并且增加了Commit-Wait来解决问题。TiDB采用单点授时,引入了一个单点。
跨数据库的异构分布式事务:这是我们这篇文章讨论的主题,因为没有涉及时间戳,所以NPC带来的困扰主要是NP。
TCC的空补偿与悬挂
空补偿:Cancel执行时,Try未执行,事务分支的Cancel操作需要判断出Try未执行,这时需要忽略Cancel中的业务数据更新,直接返回
悬挂:Try执行时,Cancel已执行完成,事务分支的Try操作需要判断出Cancel一致性,这时需要忽略Try中的业务数据更新,直接返回
现有方案的问题
开源项目dtm地址:
https://github.com/yedf/dtm
空补偿:“针对该问题,在服务设计时,需要允许空补偿,即在没有找到要补偿的业务主键时,返回补偿成功,并将原业务主键记录下来,标记该业务流水已补偿成功。”
防悬挂:“需要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝执行该笔服务,以免造成数据不一致。”
正常执行顺序下,Try执行时,在查完没有空补偿记录的业务主键之后,事务提交之前,如果发生了进程暂停P,或者事务内部进行网络请求出现了拥塞,导致本地事务等待较久
全局事务超时后,Cancel执行,因为没有查到要补偿的业务主键,因此判断是空补偿,直接返回
Try的进程暂停结束,最后提交本地事务
全局事务回滚完成后,Try分支的业务操作没有被回滚,产生了悬挂
子事务屏障技术
在本地数据库中创建好子事务屏障表dtm_barrier.barrier,唯一索引为gid-branchid-branchop
对于Try、Confirm、Cancel操作,insert ignore一条记录gid-branchid-try|confirm|cancel,如果影响行数为0(重复请求、悬挂),直接提交返回
对于Cancel操作额外再insert ingore一条记录 gid-branchid-try,如果影响行数为1(空补偿),直接提交返回
执行业务逻辑并提交返回,如果业务发生错误则回滚
情况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。
两个insert判断解决空补偿、防悬挂、幂等这三个问题,比其他方案的三种情况分别判断,逻辑复杂度大幅降低
dtm的子事务屏障是SDK层解决这三个问题,业务完全不需要关心
性能高,对于正常完成的事务(一般失败的事务不超过1%),子事务屏障的额外开销是每个分支操作一个SQL,比其他方案代价更小。