当前位置: 首页 > 后端技术 > Java

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

时间:2023-04-01 14:06:13 Java

随着微服务架构的流行,难免会遇到跨服务分布式事务的问题。分布式事务之所以难,主要是因为在分布式系统的各个节点上都可能发生各种意想不到的情况。本文首先介绍了分布式系统中存在的异常问题,然后介绍了这些问题给分布式事务带来的挑战,然后指出了各种常见用法的问题,最后给出了正确的解决方案。NPC挑战分布式系统最大的敌人可能就是NPC了,这里是NetworkDelay,ProcessPause,ClockDrift的缩写。我们先看看具体的NPC问题是什么:NetworkDelay,网络延迟。虽然网络在大多数情况下都运行良好,但是TCP虽然保证了传输的顺序和不丢包,但是并不能消除网络延迟的问题。ProcessPause,进程暂停。导致进程暂停的原因有很多:比如编程语言中的GC(垃圾回收机制)会暂停所有正在运行的线程;再比如,我们有时候会把云服务器挂起,让云服务器不用重启就可以重启。从一台主机迁移到另一台主机。我们无法确定地预测进程暂停的长度。您可能认为它会持续几百毫秒,但实际上进程暂停几分钟并不罕见。时钟漂移,时钟漂移。在现实生活中,我们通常认为时间是平稳且单调递增的,但在计算机中却不是这样。计算机使用时钟硬件来计时,通常是石英钟,其计时精度有限且受机器温度影响。为了在一定程度上同步网络上多台机器之间的时间,通常使用NTP协议将本地设备的时间与专用的时间服务器对齐。这样做的一个直接结果是设备的本地时间可能在跳跃后突然向前或向后移动。分布式事务既然是分布式系统,自然就有NPC问题。因为不涉及时间戳,所以麻烦主要是NP。TCC的空补偿和暂停我们在分布式事务中使用TCC(如果你不知道TCC,可以参考这篇分布式事务最经典的七大解决方案,了解分布式事务基础知识)为例,看在NP的影响下。一般情况下,TCC回滚的执行顺序是先执行Try,再执行Cancel。但是由于N的原因,Try的网络延迟可能会很大,导致先执行Cancel,再执行Try。这种情况在分布式事务中引入了两个问题:空补偿:执行Cancel时,Try没有执行,事务分支的Cancel操作需要判断Try没有执行。这时候需要忽略Cancel中的业务数据更新,直接Returnsuspend:执行Try时,Cancel已经执行完毕。事务分支的Try操作需要判断Cancel的一致性。这时需要忽略Try中的业务数据更新,直接返回分布式事务。还有一个通病需要处理,就是对于重复的请求,业务需要做到幂等。因为null补偿、暂停、重复请求都与NP有关,所以我们统称为子事务乱序问题。在业务处理中,这三个问题都需要慎重处理,否则会出现错误的数据。现有方案存在的问题我们看到,除了开源项目https://github.com/yedf/dtm,包括各个云厂商和开源项目,他们给出的业务实施建议大多类似如下:空补:》对于这个问题,在设计服务的时候,需要允许空补,即当没有找到要补偿的业务主键时,补偿成功,原来的业务主键为记录下来,标志着业务流量补偿成功。”反挂:“需要检查空补记录的业务主键中是否已经存在当前业务主键,如果存在则拒绝执行服务,避免数据不一致。”上述实现在大多数情况下可以正常工作,但是上述做法中的“先查后改”在并发情况下很容易掉坑。下面分析一下场景:正常执行顺序下,执行Try时,检查业务主键无空补偿记录后,事务提交前,如果进程暂停P,或者事务内网络请求拥塞,导致全局事务超时后本地事务等待时间过长,执行Cancel,因为没有找到要补偿的业务主键,所以判断为空补偿,直接返回Try的流程进程被挂起,最后提交本地事务。全局事务回滚完成后,Try分支的业务操作没有回滚,导致挂起。其实NPC中P和C的组合有很多,P和C的组合场景会导致上述的raceconditions,就不一一细说了。虽然这种情况发生的概率不高,但是在金融领域,一旦涉及到金钱账户,影响可能是巨大的。PS:如果幂等控件也采用“先检查后修改”的方式,也容易出现类似的问题。解决此类问题的关键是使用唯一索引,通过“以查找代替更改”来避免竞争条件。正确的姿势下面详细解释一下yedf/dtm是如何解决这个问题的。dtm首创了子事务屏障技术,用于同时解决空值补偿、防挂、幂等三大问题。对于TCC事务,其详细工作过程如下:在本地数据库中创建子事务屏障表dtm_barrier.barrier,唯一索引为gid-branchid-branchop。对于Try、Confirm和Cancel操作,插入忽略一条记录gid-branchid-try|confirm|cancel。如果受影响的行数为0(重复请求,暂停),直接提交返回。对于Cancel操作,追加Theninsertingorearecordgid-branchid-try,如果受影响的行数为1(null补偿),直接提交返回执行业务逻辑并提交返回,如果业务出现错误,rollback如果Try和Cancel的执行时间没有重叠,那么读者很容易分析出上面的过程可以解决null补偿和挂起的问题。让我们看看如果Try和Cancel的执行时间重叠会发生什么。假设Try和Cancel并发执行,那么Cancel和Try都会插入同一条记录gid-branchid-try。由于唯一索引冲突,这两个操作只有一个可以成功,另一个会在持有锁的事务完成后返回。.情况一:Try插入gid-branchid-try失败,Cancel操作插入gid-branchid-try成功。这是一个典型的空补和暂停场景。根据子事务屏障算法,Try和Cancel都会直接回到情况2,Try插入gid-branchid-try成功,Cancel操作插入gid-branchid-try失败。根据上面的子事务屏障算法,业务会正常执行,业务执行顺序为TrybeforeCancel。Case3,Try和Cancel的操作重叠如果遇到宕机等情况,至少Cancel会被dtm重试,最终会走到情况1或2。总结各种情况的详细讨论,子-transactionbarrier可以保证各种NP情况下最终结果的正确性。子事务屏障其实有很多优点,包括:两个insert判断解决空补、反挂、幂等三个问题,相对于其他方案的三个case分别判断,以及逻辑复杂度大大降低。DTM子事务barrier就是解决这三个问题的SDK层,业务根本不需要关心高性能。对于正常完成的事务(一般失败的事务不超过1%),子事务barrier的额外开销是每个分支操作一条SQL,高于其他方案。它的成本更低。以上理论和分析过程同样适用于SAGA分布式事务。dtm中的子事务barrier同时支持TCC和SAGA两种事务模式。完整解决方案DTM是golang开发的分布式事务管理器,解决了跨数据库、跨服务、跨语言栈更新数据的一致性问题。以下是dtm与阿里开源的seata的主要特性对比:特性DTMSEATA备注支持语言Go,Java,python,php,c#...Javadtm可轻松接入一门新语言异常处理子事务屏障自动processingmanualprocessingdtmsolution幂等、挂起、空值补偿TCC事务??XA事务??AT事务推荐使用XA?AT与XA类似,性能更好,但带脏回滚SAGA事务支持并发状态机模式事务消息rocketmq的事务消息单服务多数据源??通信协议HTTP、gRPCdubbo等协议dtm对云原生更友好如果你的语言栈包括Java以外的语言,那么dtm是你的首选。如果你的语言栈是Java,你也可以选择接入dtm,使用子事务屏障技术来简化你的业务编写。可以参考使用Java轻松完成一个TCC分布式事务,自动处理空值补偿、挂起和幂等。如果想学习分布式事务,dtm的文档大受好评,让读者快速上手分布式事务,理论与实践相结合,让读者逐步深入。欢迎大家访问https://github.com/yedf/dtm,欢迎Issue、PR、Star