原文链接https://www.confluent.io/blog/dual-write-problem/
双写问题发生于当两个外部系统必须以原子的方式更新时。
问题
说有人到银行存了一笔钱,触发 DepositFunds 命令,DepositFunds 命令被发送到Account microservice。
Account microservice需要做两件事,修改过数据库 和 发送消息到kafka。
Figure 1: A microservice writes to two separate systems.
如果在修改数据成功之后, 系统崩溃,发送kafka 消息失败,这样就造成了相关kafka事件丢失。
Figure 2: A failure results in a missing event.
同样的问题也会发生于任何系统,包括 monolithic和microservice 系统, 只要是尝试写两入两个独立的系统,却只有其中一个操作成功。
解决办法
事务发件箱模式 (transactional outbox pattern )
同时利用了数据库事务和重试机制。
这种模式其实就是在 Account microservice 写数据库表的同时,把要发到kafka的消息先写入一张outbox表。这里写入的两张表在同一个事务(transaction)里完成。
至于outbox 表中的数据,可以由专门的程序负责读取和发送到kafka,也可以利用CDC(change data capture)完成。对于发送失败可以加入retry。在确定发送成功之后,可以删除outbox表中的数据。
Figure 9: The transactional outbox pattern.
Event sourcing
事务发件箱模式不合适于不支持事务的数据库,而不依赖于数据库事务的就是 event sourcing。
这种模式不另外写一张表来保存要发送的数据,而是在原来表上(这里原来的表是存放的event,而不是事物发件箱模式中的数据)加一个flag,来过滤那些已经发送过了。
(😂是不是觉得很。。。 反正我看到这里,哎!😂)
The listen-to-yourself pattern
和 event sourcing很像,只不过event sourcing是先写表,在发送至kafka。
而 listen-to-yourself pattern 是直接写入kafka。在从kafka接收消息来修改数据库,同时写表需要retry跟上。
只不过就是这个先后顺序问题,写表虽会最终一致,但若在未写表之时,访问数据库则问题出现。
若系统可接纳,则无所谓。
其它解决办法
还有其他解决双写问题的方法,我们在这里没有介绍。其目的不是暗示这些是解决问题的唯一方法,而是强调在事件驱动系统中特别有用的具体解决方案。
在某些情况下,可以使用两阶段提交( two-phase commit aka 2PC)、扩展架构(extended architecture aka XA)事务和传奇模式(saga pattern)等技术来规避(circumvent)这个问题。然而,它们具有复杂性,可能不适用于所有技术,因此在实施它们之前,请确保您了解权衡。