分布式事务是计算机处理中比较复杂的场景,经常让人很头疼。
首先我们考虑单机情况下的事务实现,例如mysql,这类事务实现在本地可以通过redolog,undolog来保证同时成功,同时失败(虽然这里的实现也挺复杂的),因为他只涉及到了内部的业务逻辑,或者说在单机情况下,成功和失败都是属于可控范围,因为整个事件的完成提交只涉及到本地事务的成功与否。
为什么分布式事务就复杂了呢?因为他不可控。主要体现在以下几点:
下面,我们讨论下基于db的分布式事务实现。首先先介绍下场景,业务场景抽象出来比较简单,也就是当收到请求之后我们需要按顺序调用A->B->C三个系统(注意,这里三个系统全部都是写操作,并且写入的值都依赖于前一个系统的处理结果)返回给上游处理结果。乍一看是不是感觉很简单?三次RPC调用搞定。事情肯定没有这么简单,下面我列举几种异常情况来看看:
看到这里是不是感觉自己还是大意了,没想到有这么多问题要解决,下面给出一种基于db解决分布式事务的思路
首先我们需要有一个分布式事务管理器来分派事件ID,标识整个事务的进展状态,表结构大致设计如下:
并且我们通过一个状态机来管理事务状态
该状态机可以标识当前事务的进展状态,并且可以为事务幂等提供状态标识,也就是查询到成功之后就可以组装结果直接返回了
事件任务表关联了这个事务id下即将要执行的任务,注意这里是"即将",这意味着我们是先写入任务再执行操作的,这么做是为了防止接口调用成功再写表时如果失败了,操作记录就丢失了,这对于事务调用是不可以容忍的。
同样的,我们也需要用一个状态机来管理任务的状态
该状态机可以表示当前任务的进展状态,在幂等/回滚控制,以及事务进展标识中起到作用。
如图所示
注意这里其实是先在任务管理器注册为ready,然后调用完成之后再去更新为success的
在调用过程中的任何一个步骤都有可能出现失败,这个失败可能是接口调用失败,也有可能系统宕机直接终止了,这些类型的失败都是我们回滚中需要解决。
当调用接口出现失败或者超时就会触发系统的回滚逻辑,如下图所示可以看到此时A任务执行成功,B任务执行成功,C任务执行超时,此时触发了回滚。因为我们并不知道C任务是否执行成功了,所以A,B,C任务都需要执行回滚,执行完成后更新事务任务管理器为回滚成功状态。
但是这里问题又来了,如果我回滚到一半失败了呢?我们需要依赖上游的重试来继续完成回滚的流程,那么此时又有一个新的问题来了,如何确认回滚点?
回滚点是一个需要分类讨论然后再总结归纳的过程:
1、 A,B执行成功,C执行明确失败更新状态为失败状态
2、 A,B执行成功,C执行明确失败但更新状态失败,还是初始状态
3、 A,B执行成功,C执行失败;接口A,B回滚成功,C回滚失败
4、 A,B执行成功,C执行失败;接口A,B,C回滚成功,但更新事务管理器记录失败
也就是说当我们开始做回滚的时候,遇到这些情况都需要能够处理到。把上述四种情况统一一下得出结论,只要任务状态为初始化的都需要重新触发回滚。
细心的小伙伴应该发现这么处理会导致已经回滚了的单又继续回滚,因此下游系统需要能够支持回滚的幂等,上游可以通过加订单号来确保回滚的幂等性。
但是这里有一种状态是有歧义的,也就是回滚的时候发现任务状态记录为初始化的时候,因为此时任务的状态可能是任意的,既可以调用成功/失败,也可以是调用成功/失败,还可以是回滚成功/失败,我们为了简单处理,统一让这个记录回滚,或者业务可以根据需求做特定的处理。
不知道有小伙伴注意到没有,当我们在遇到调用接口超时的时候触发了回滚,可能由于网络波动的原因造成回滚请求先于调用请求的情况,也就是悬挂,此时可能会导致先收到取消接口之后又收到正常的系统调用,这就会导致出现了回滚失效的情况,所以这个时候就需要有对账机制能够及时发现相关问题;或者这里可以约束下游的业务接口需要能够拒绝已经调用回滚的请求。
同时这里也可能因为网络的原因导致出现没有接口在还未收到调用请求后直接收到了回滚请求,所以业务接口需要能够兼容空回滚的情况
分布式事务没有完美解法,只能在现有的业务场景下做出局部最优解,因此要意识到该方案也是有缺陷或者待优化的点:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。