空谈发件箱模式(outbox pattern)

时间:2022-07-26
本文章向大家介绍空谈发件箱模式(outbox pattern),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

之前聊过很多次分布式事务这个话题,本文继续聊一聊如何用发件箱模式实现分布式事务。

1

基于微服务架构模式(当然不限于)的应用系统,常常会利用消息中间件(kafka,rabbitmq等)来实现各个微服务之间的通信。对于用户的某个操作,一个微服务可能需要执行“存数据库”和“发送event”两个步骤。

举个例子,用户创建一个订单,订单服务需要存数据库和发送订单created event,如下:

begin transactionsave(order) to dbsend(order_created_event) to kafkacommit/rollback

对于上面这个例子,由于牵扯到两个系统,数据库事务和kafka事务并不能保证整个操作的事务性(ACID),至多能保证各个子系统的事务性,最终可能导致出现如下数据不一致的情况:

  1. 发送event成功,然后commit到数据库失败(原因可能是数据库不available,或者是违反数据库表的constraints)。
  2. 发送event成功到commit到数据库之间有一定延迟,如果consumer消费到event,接着call订单服务查找这个order,结果就可能会查不到。

有同学会说,可以把发送event放在存数据库transaction后面,如下:

begin transactionsave(order) to dbcommit/rollbacksend(order_created_event) to kafka

即使这样,又会出现另外一种不一致的情况,即:存数据库成功,发event失败,比如由于kafka临时不available。这直接导致用户的操作失败,必须要重新提交请求,在某些场景下对用户来说可能是不可接受的;并且数据库里面还有一条脏数据存在。

那么,如何保证存数据库和发event是一个transaction呢?

其实,抽象这个问题,其本质就是一个分布式事务问题,如何实现分布式事务,网上有很多文章介绍,可能最常被提到的就是两阶段提交2PC(2 phase commit),但其需要各个子系统都支持两阶段提交协议(比如XA),很多数据库都有支持,但是很多消息中间件都不支持,所以2PC不适用这里的场景,并且由于2PC的一些缺点(参见我的另外一篇文章:关于分布式系统数据一致性的那些事(二)),它并不是实现分布式事务的常规选择。

基于此,这里介绍的发件箱模式可以有效地解决这个问题。

2

发件箱模式,简单讲就是在数据库里面额外增加一个outbox表用于存储需要发送的event,把直接发送event的步骤换成先把event存储到数据库outbox表;另外,程序启动一个scheduler job不断去抓取outbox表里面的记录,发送给Kafka,最后删除发送成功的记录。如下:

主逻辑:

begin transactionsave(order) to dbsave(order_created_event) to db outboxcommit/rollback

scheduler job:

begin transactionget(order_created_event) from db outboxsend(order_created_event) to kafkadelete(order_created_event) to db outboxcommit/rollback

基于这样的实现,存order和event就可以通过数据库的transaction来保障,这样即使是Kafka不available,也不影响用户的行为,只是说后续处理可能会有一些延迟而已;此外,如果scheduler job删除event失败,最坏的行为也就是重新再发一次这个event,即是重发消息的问题,这就需要消费端实现幂等性处理。

3

发件箱模式主要有两种实现方式:

  • Transaction log tailing
  • Polling publisher

Transaction log tailing,又称CDC(change data capture),就是依赖于数据库的transaction log,一般数据库在成功执行完一条语句之后就会记录一条log,那么这个方式就是不断地增量抓取数据库log,然后发送到消息系统。目前,已经有一些支持这种方式的open source可以直接使用,比如:Debezium。另外,采用这种方式,可能需要数据库装相关的插件以支持CDC。

Polling publisher,其实就是自己实现一个轮询的sheduler job,不断抓取outbox表里面的event,然后发送到消息系统,最后删除event。在实现层面,可能需要注意下面几点:

  1. 一般来说,为了保证event的顺序,在抓取event的时候,需要把event按时间排序。
  2. 如果是分布式的环境,为了保证scheduler job同时只在一个instance上run,需要实现分布式锁;或者借助于成熟的scheduling的library,比如:quartz。

相关阅读

References

  • https://microservices.io/patterns/data/transactional-outbox.html
  • https://mp.weixin.qq.com/s/PMIWr8j3cN-K0FbS6qGW0A