之前我们已经讲过了基础的CAP、BASE理论,然后介绍了强一致性方案XA、2PC和3PC,然后详细讲述了TCC在生产中的应用场景和原理。本文继续讲解终一致性方案——本地事务状态表方案。
本地事务状态表
本地事务状态表的方案中主要有三种角色:调用方、被调用方、定时任务。
我们都知道,分布式事务之所以很难保证一致性,但是本地事务却可以,就是因为本地事务是存储引擎层面去保证的。比如 MySQL 的InnoDB 引擎中,他的底层用了 RedoLog 和 UndoLog 以及锁等机制确保了这一点。这里对此不再深入作答,具体可以看往期文章。
所以现在思路是这样:我们要利用存储引擎可以保证事务一致性,这就让我们想到了为每一次远程服务的调用生成一条数据,在数据库落盘,利用本地事务的特点来保证一致性。每一条数据都代表一次RPC调用,然后利用重试机制来保证终一致性。
用一个案例思考
我先不给详细过程,而是带你实际推理一下,掌握学习方法更重要。
这里使用支付和订单项目来举例,本地服务是支付项目,在支付完成后需要使用RPC通知订单支付结果成功,订单更新订单状态。
正常调用流程如下:
看起来流程没有毛病,余额如果通知订单失败,本地事务回滚就是了。余额会加上去,订单也没有支付成功。
订单接口超时怎么办?
但是,一个远程连接有有来和去两个过程,如果余额扣减成功,并且订单服务已经收到支付成功的信息,但是订单本地状态更新为支付成功后,接口响应超时,没有成功返回给支付服务怎么办?
这就造成了余额本地事务回退,金额没有扣减,但是订单状态成功的情况。
本地事务状态表核心流程
为了解决数据不一致的情况,根据文章一开始我们提到的BASE理论,以及利用本地事务来保证数据一致的思路。
因为订单接口有可能超时,网络问题我们无法从编码层面解决,但是可以做到有效的补偿。根据BASE理论,一次调用不能一致,那我多次调用不断尝试不就可以了嘛,总不能每次都超时。
所以这就涉及到两个问题:
订单接口(被调用方)需要支持幂等操作。 需要有一种机制让系统知道该重试哪些操作,并且这个机制需要保证和余额(被调用方)数据的一致性。
个问题很好解决,可以通过订单ID利用键或者Redis来保证支付行为不会被重复处理。
第二个问题提到了一致性和数据记录,那我们直接保存一条本地事务数据到表里就可以了,保存时候记录订单接口的必要参数信息,以及调用成功状态,默认是未成功。
因为要保证本地事务状态表中的数据和余额(被调用方)一致,那我们就需要把事务提交的时机提前到RPC调用前。
因为此时还没有真正发起RPC调用,所以对于表中记录的数据,可以单独启动一个定时任务不断重试。如果通知订单(被调用方)成功就更改状态为已通知,这样以后定时任务就不会再读取这条记录了。如果还不成功就保持状态不变,继续等待下一次尝试。
这里的定时任务不断重试体现了终一致性的思想。
优缺点
当然,本地事务状态表也优缺点并存,具体使用需要根据场景。
优点:
性能还不错,业务之间没有强依赖。 实现了终一致性,适合对数据不一致有一定容忍度的系统。
缺点:
有一定的业务入侵性。 每个系统需要有自己的本地事务状态表,难通用。 实效性并不是很好。
但是,如上这些缺点其实有一些解决的办法。
定时任务不及时怎么办
笔者自己实现了一套简单的本地事务表框架,主要解决了两个问题:业务入侵度高和实效性。
这里不说具体代码了,核心思想是:
将每个需要利用本地事务表方案的接口和参数保存在ThreadLocal中,并且保存状态表数据到MySQL。 将每个需要利用本地事务表方案的方法用一个自定义注解去做切面。 利用TransactionSynchronization接口自己实现了commit,afterCompletion等方法,在事务结束的时候读取ThreadLocal中的内容,以做到及时调用对应RPC。
但是还有问题,每次都需要根据不同的事件类型,写不同的业务逻辑,并且还需要在手动写保存到任务表的逻辑,依旧有业务入侵性,还是有点繁琐。
总结
简单总结下流程:
调用方在本地事务中保存一条待调用状态的数据到本地事务状态表中。 本地事务提交后马上调用RPC,如果成功就更新状态,如果不成功那就不管。 在后台启动一个定时任务,定期扫描本地事务状态表中未调用完成的数据,不断尝试调用。如果成功就更新状态,如果失败那就等待下次调用。在失败重试一定次数后触发告警人工介入处理。
对于本地事务表方案,你能做出自己设计的框架吗?如何解决他存在的问题?