06_基于本地消息表实现分布式事务
基于本地消息表常用的分布式事务解决方案
文章来源:基于本地消息表的分布式事务解决方案
上面提到了分布式系统中要实现强一致性比较困难,往往很多业务场景不要求强一致性,允许有个临时的业务中间状态。因此就可以采用最终一致性的分布式事务方案。
常用的分布式解决方案有实现XA事务的Atomikos,本地消息表方案,基于消息中间件的最终一致性方案,TCC方案,阿里的SEATA,SAGA方案和最大努力通知。下面主要对基于本地消息表实现最终一致性的分布式事务方案进行介绍。
本地消息表方案最初是ebay提出的,其实也是BASE理论的应用,属于可靠消息最终一致性的范畴。这里以支付服务和会计服务为例展开介绍本地消息表方案,大概流程是这样子:用户在支付服务完成了支付订单支付成功后,此时会调用会计服务的接口生成一条原始的会计凭证到数据库中,如图1所示。这里必须明确:支付服务处理完订单支付等逻辑后,此时若直接调用会计服务生成会计凭证数据的接口肯定会遇到分布式事务的问题。

因为用户完成支付后,此时得立马给用户一个支付的反馈,要做的就是提醒用户支付成功。因为会计服务生成的会计凭证保存到数据库的过程中可以对用户透明,用户也无需知道有这么一个流程,为了提高响应速度和解耦,因此可以引入mq来做到异步生成会计凭证,即用户完成支付订单支付后,此时可以将消息投递到mq中,然后会计服务再去监听mq消息去处理消费逻辑。此时如图2:

在支付服务和会计服务之间引入mq后,此时又引入了新的问题。大概列举如下:
- 若支付服务完成支付逻辑后,在投递消息到mq中间件的过程中由于网络抖动等原因,没有投递到mq中导致消息丢失了怎么办?
- mq接收到消息后,由于内部原因导致消息丢失了怎么办?
- 会计服务在监听消息的过程中,由于网络原因没有接收到消息或消费过程中遇到异常,此时也会导致消息丢失,测试怎么办?
经过以上分析,mq可能会丢失消息,传统的mq没有实现分布式事务(注意rocketmq的某些版本有实现分布式事务功能),因此这里可以引入本地消息表结合mq的方式来解决分布式事务的问题,保证消息的可靠投递。
图3是由图2细化后的图,其中红框处引入了一个本地消息表。

根据图3,正向流程步骤大概如下:
- 在支付库中引入一张消息表来记录支付消息,即用户支付成功后同时往这张消息表插入一条支付成功的消息,状态为“发送中”。注意支付逻辑和插入消息表的代码要包裹在一个事务里面,这里保证了本地事务的强一致性。即支付逻辑和插入消息表的消息组成了一个强一致性的事务,要么同时成功,要么同时失败。
- 完成 1)步的逻辑后,此时再向mq的PAY_QUEUE队列中投递一条支付消息,这条支付消息的内容跟保存在支付库消息表的消息内容一致。
- mq接收到消息后,此时会计服务也监听到这条消息了,此时会计服务处理消费逻辑即开始生成会计凭证。
- 会计凭证生成后,再反向向mq投递一条消费成功的消息到ACC_QUEUE队列
- 同时支付服务又来监听这个会计服务消费成功的消息,当支付服务监听到这个消费成功的消息后,此时再将本地消息表的消息状态改为“已发送”。
- 经过前面5步后,整个业务就已经完成了。
以上是引入本地消息表后的正常的业务流程,前文分析过生产者,mq和消费者三个环节中都可能弄丢消息,即图4中的红框处可能会造成消息丢失。

此时可能你会有个疑问:用户支付成功后,若消息在投递过程中丢失了就丢失了,会计服务那边也消费不到了,此时同样也会造成支付服务(生产者)和会计服务(消费者)之间的数据不一致。
之前增加的本地消息表好像也没起作用啊?
那此时怎么办呢?如何来解决消息丢失的问题,做到消息的可靠投递呢?
其实解决方案就是消息重复投递,但消费者的消费接口要实现幂等性。
怎么来让消息重复投递呢?此时本地消息表就派上用场了,刚才我们在支付库中新增加了一张本地消息表,即支付等逻辑处理成功,这张本地消息表也会记录一条记录,此时的消息状态是“发送中”。若第一次生产者投递的消息丢失后,此时我们只要将这张本地消息表状态为“发送中”的消息重新投递即可,直到消费者消费成功为止,消费者消费成功后将这条消息的状态改为“已发送”即可。
因此为了能将丢失后的消息重发,此时我们引入一个定时任务好了,暂且叫它“消息恢复系统”吧,如下图所示。这个消息恢复系统就是每隔一段时间去本地消息表中捞取状态为“发送中”的消息,然后重新投递到mq中间件中,然后消费者就会重新消费了。若消费者已经消费过了,此时就不再处理消费业务逻辑,直接反向投递一条消费成功的消息到mq中,此时原来的生产者此时也会监听这条消费成功的消息,将本地消息表的消息状态改为“已发送”,此时消息恢复系统就不会再去捞取这条状态为“已发送”的消息,然后进行重新投递了。

此时若消息丢失后且消息恢复系统在重新投递过程中,也可能会再次投递失败。此时我们一般会指定最大重试次数,重试间隔时间根据重试次数而线性增长。若达到最大重试次数后,同时记录日志,我们可以根据记录的日志来通过邮件或短信来发送告警通知,接收到告警通知后及时介入人工处理即可。
基于本地消息表的分布式事务方案就介绍到这里了,本地消息表的方案的优点是建设成本比较低,其虽然实现了可靠消息的传递确保了分布式事务的最终一致性,其实它也有一些缺陷:
- 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
- 本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的
优缺点
优点:
- 可靠性高:基于本地消息表实现分布式事务,可以将本地消息的持久化和本地业务逻辑操作,放到一个事务中执行进行原子性的提交,从而保证了消息的可靠性。
- 可扩展性好:基于本地消息表实现分布式事务,可以将消息的发送和本地事务的执行分开处理,从而提高了系统的可扩展性。
- 适用范围广:基于本地消息表实现分布式事务,可以适用于多种不同的业务场景,可以满足不同业务场景下的需求。
缺点:
- 实现复杂度高:基于本地消息表实现分布式事务,需要设计复杂的事务协议和消息发送机制,并且需要进行相应的异常处理和补偿操作,因此实现复杂度较高
- 系统性能受限:基于本地消息表实现分布式事务,需要将消息写入本地消息表,并且需要定时扫描本地消息表进行消息发送,因此对系统性能有一定影响。
- 会带来消息堆积扫表慢、集中式扫表会影响正常业务、定时扫表存在延迟问题等问题。
补充
具体的实现思路大致如下:
- 在生产者(发送方)的数据库中,创建一张本地消息表,用来记录要发送的消息,以及消息的状态(发送中,已发送,已消费等)。
- 在生产者的业务逻辑中,同时执行本地事务和插入本地消息表的操作,保证本地事务的强一致性。
- 在本地事务和本地消息表的操作成功后,向消息队列发送一条消息,这条消息的内容和本地消息表的内容一致。
- 在消费者(接收方)的业务逻辑中,监听消息队列,接收到消息后,执行相应的业务操作,并向消息队列发送一条消费成功的消息。
- 在生产者的业务逻辑中,监听消息队列,接收到消费成功的消息后,更新本地消息表的状态为已消费。
为了防止消息的丢失或重复,还需要考虑以下几点:
- 在生产者发送消息到消息队列的过程中,如果发生网络异常或其他原因导致消息丢失,需要有一个定时任务,定期扫描本地消息表中状态为发送中的消息,重新发送到消息队列。
- 在消费者接收消息的过程中,如果发生网络异常或其他原因导致消息丢失,需要有一个重试机制,重新拉取消息队列中的消息。
- 在消费者执行业务操作的过程中,如果发生异常或其他原因导致业务失败,需要有一个补偿机制,回滚已经执行的操作,或者发送一条消费失败的消息到消息队列。
- 在消费者执行业务操作的过程中,如果发生重复消费的情况,需要保证业务操作的幂等性,即多次执行相同的操作,不会影响数据的一致性。