之前聊过很多次分布式事务这个话题,本文继续聊一聊如何用发件箱模式实现分布式事务。
1
基于微服务架构模式(当然不限于)的应用系统,常常会利用消息中间件(kafka,rabbitmq等)来实现各个微服务之间的通信。对于用户的某个操作,一个微服务可能需要执行“存数据库”和“发送event”两个步骤。
举个例子,用户创建一个订单,订单服务需要存数据库和发送订单created event,如下:
begin transactionsave(order) to dbsend(order_created_event) to kafkacommit/rollback
对于上面这个例子,由于牵扯到两个系统,数据库事务和kafka事务并不能保证整个操作的事务性(ACID),至多能保证各个子系统的事务性,最终可能导致出现如下数据不一致的情况:
有同学会说,可以把发送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,又称CDC(change data capture),就是依赖于数据库的transaction log,一般数据库在成功执行完一条语句之后就会记录一条log,那么这个方式就是不断地增量抓取数据库log,然后发送到消息系统。目前,已经有一些支持这种方式的open source可以直接使用,比如:Debezium。另外,采用这种方式,可能需要数据库装相关的插件以支持CDC。
Polling publisher,其实就是自己实现一个轮询的sheduler job,不断抓取outbox表里面的event,然后发送到消息系统,最后删除event。在实现层面,可能需要注意下面几点:
相关阅读
References