发件箱模式 - 对于任何 SQL 和 NoSQL DB 没有重复和无序的消息中继

Evg*_*yst 4 2phase-commit distributed-transactions duplicates microservices outbox-pattern

当我们需要更改两个系统中的数据时,双写入就会出现问题:数据库(SQL 或 NoSQL)和 Apache Kafka(例如)。必须可靠地/自动地更新数据库并发布消息。最终一致性是可以接受的,但不一致是不能接受的。

如果没有两阶段提交 (2PC),双写入会导致不一致。

但在大多数情况下,2PC 并不是一个选择。

事务发件箱是一种微服务架构模式,其中单独的消息中继进程将插入数据库的事件发布到消息代理。

交易发件箱

并行运行的多个消息中继进程会导致发布重复项(2 个进程读取 OUTBOX 表中的相同记录)或无序(如果每个进程只读取 OUTBOX 表的一部分)。

单个消息中继进程也可能多次发布消息。消息中继可能会在处理 OUTBOX 记录之后但在记录它已完成此操作的事实之前崩溃。当消息中继重新启动时,它将再次发布相同的消息。

如何在事务发件箱模式中实现消息中继,以便将重复消息或无序的风险降至最低,并且该概念适用于所有 SQL 和 NoSQL 数据库?

Evg*_*yst 7

使用事务发件箱模式很难实现精确一次交付保证,而不是至少一次交付。

消息中继发布的消息的使用者必须是幂等的,并过滤重复和无序的消息。

消息必须包括

  • 实体的当前状态(而不是仅更改字段,也称为更改事件,“增量”),
  • ID 标头或字段,
  • 版本标头或字段。

ID 标头/字段可用于检测重复项(确定消息已被处理)。

版本标头/字段可用于确定消息的更新版本已被处理(如果消费者收到 msg_a:v1、v2、v4,那么当消息到达时,它必须删除 msg_a 的 v3,因为更新版本的 v4 msg_a 已被处理)。

消息中继提取到单独的微服务中,并在单个副本中运行(Kubernetes 中的 .spec.replicas=1),并在所有现有 Pod 在新 Pod 之前被杀死时使用重新创建部署策略(Kubernetes 中的 .spec.strategy.type=Recreate)进行更新创建(而不是 RollingUpdate 部署策略)无助于解决重复问题。消息中继可能会在处理 OUTBOX 记录之后但在记录它已完成此操作的事实之前崩溃。当消息中继重新启动时,它将再次发布相同的消息。

拥有多个主动-主动消息中继实例可以实现更高的可用性,但会增加发布重复和无序的可能性。

为了实现快速故障转移,消息中继的主备集群可以基于

  • 使用 sidecar 进行 Kubernetes 领导者选举 k8s.io/client-go/tools/leaderelection
  • Redis分布式锁(Redlock)
  • SQL 锁使用SELECT ... FOR UPDATE NOWAIT
  • ETC。

正如Martin Klappmann 所解释的,没有围栏的分布式锁被打破,只会最大限度地减少领导者选举中多个领导者(短时间内)的机会。

分布式锁损坏