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

Outbox pattern - Message Relay without duplicates and unordering for any SQL and NoSQL DB

当我们需要在两个系统中更改数据时,双写入是一个问题:数据库(SQL 或否SQL)和 Apache Kafka(例如)。 必须更新数据库并发布消息 reliably/atomically。 最终一致性是acceptable,但不一致不是。

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

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

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

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

单个消息中继进程也可能多次发布消息。消息中继可能会在处理 OUTBOX 记录之后但在记录它已经这样做的事实之前崩溃。当 Message Relay 重新启动时,它会再次发布相同的消息。

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

Exactly-once 交付保证而不是 at-least-once 交易发件箱模式很难实现。

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

消息必须包含

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

ID header/field可用于检测重复(确定消息已被处理)。

版本 header/field 可用于确定消息的更新版本已被处理(如果消费者收到 msg_a: v1, v2, v4 那么它必须丢弃 v3 of msg_a 什么时候到达因为 msg_a 的更新版本 v4 已经被处理了)。

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

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

为了快速 fail-over active-standby 消息中继集群可以基于

实现
  • Kubernetes Leader Election using sidecar k8s.io/client-go/tools/leaderelection
  • Redis分布式锁(Redlock)
  • SQL 使用 SELECT ... FOR UPDATE NOWAIT
  • 锁定
  • 等等

因为 explained by Martin Klappmann 没有防护的分布式锁被破坏了,只能最大限度地减少领导者选举中多个领导者(短时间)的机会。