本地消息表
Last updated
Last updated
本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。
角色:
事务主动方
事务被动方
如上图所示,通过在事务主动方中额外创建一个事务消息表,事务主动方处理业务逻辑和记录事务消息在本地事务中完成,轮询事务消息表的数据从而发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
整体的流程如下:
事务主动方在同一个本地事务中处理业务逻辑和记录事务消息
事务主动方通过消息中间件,通知事务被动方处理事务消息。消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
事务主动方接收中间件的消息,更新消息表的状态为已处理。
一些必要的容错处理如下:
当 1 中出错,由于还在事务主动方的本地事务中,直接回滚即可。
当 2、3 中出错,由于事务主动方在本地保存了消息,只需要查询事务消息表,并重新通过消息中间件发送事务消息,事务被动方重新读取消息处理业务即可。
如果事务被动方处理业务失败,事务被动方可以发消息给事务主动方回滚事务。
如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要发消息通知事务主动方进行回滚事务。
这样可以避免以下两种情况导致的数据不一致性:
业务处理成功、事务消息发送失败
业务处理失败、事务消息发送成功
以订单服务、库存服务为例。当客户下单后,需要先通过订单服务在订单表中插入一条订单记录,再通过库存服务实现对库存表中库存记录的扣减。但这里即会存在一个问题,由于订单表、库存表分别位于订单服务、库存服务的数据库。传统的本地事务显然无法解决这种跨服务、跨数据库的场景。而基于本地消息表的分布式事务方案则可以在对业务改动尽可能小的前提下保障数据的最终一致性。
具体地,在事务发起方即订单服务的数据库中再增加一张本地消息表。向订单表中插入订单记录的同时,在本地消息表中也插入一条表示订单创建成功的记录。由于此时订单表、本地消息表位于同一数据库当中,可以直接通过一个本地事务来保证对这两张表操作的原子性。
与此同时,在订单服务中添加一个定时任务,不停轮询、处理本地消息表:
将消息表中未被成功处理的记录通过 MQ 投递至库存服务。
库存服务在从 MQ 中接收到订单创建成功的消息后,对库存表进行库存扣减操作。
在库存服务完成扣减后,通过某种方式告诉订单服务该条消息已经被成功消费、处理。这样订单服务即可将本地消息表中相关记录标记为成功处理的状态,以避免定时任务重复投递。这里库存服务确认消息消费成功的实现方式,可以直接通过 MQ 的 Ack 消息确认机制实现,也可以让库存服务再向订单服务发送一个处理完毕的消息来完成。
注意:
库存服务的库存扣减需要保证幂等性。一方面由于 MQ 存在自动重试机制,另一方面,当订单服务未收到库存服务对本次消息的消费确认时,则可能会导致定时任务下一次继续投递该消息至库存服务。
根据实际业务需要,本地消息表中记录还应该设置一个合理的最大处理等待时间,以及时发现长时间无法得到有效处理的本地消息记录。
优点:
从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
方案轻量,容易实现。
缺点:
与具体的业务场景绑定,耦合性强,不可公用。
消息数据与业务数据同库,占用业务系统资源。
业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。