事务性发件箱用本地提交连接数据库与消息发布
说明业务数据与发件箱记录怎样在同一事务提交,再由独立发布器可靠转发,并处理重复和停滞。
事务性发件箱用本地提交连接数据库与消息发布
服务在修改数据库后发布事件,最危险的间隙出现在两步之间。数据库提交成功而消息发送失败,下游看不到变化;消息先发出而数据库回滚,下游又会处理一个并未成立的事实。普通代码中的先后顺序不能把两个独立系统变成一次原子提交。
事务性发件箱把待发布事件作为本地数据库记录,与业务变更放在同一事务中提交。独立发布器随后读取发件箱并投递到消息系统。它消除了“业务已提交但没有任何发布记录”的间隙,却没有消除重复、延迟和下游一致性问题。
业务状态与事件意图在同一事务落库
一次请求先验证业务条件,更新领域表,同时插入发件箱记录。若事务回滚,两者都不存在;若提交,两者都可见。这里依赖数据库自身事务隔离和原子性,不要求数据库与消息代理参加分布式事务。
发件箱记录通常包含稳定事件 ID、聚合或实体 ID、事件类型、模式版本、负载、发生时间和追踪信息。负载应表达已提交事实,而不是要求发布器回头读取当前业务表。否则发布延迟期间业务状态可能再次变化,事件内容就不再对应原始事务。
事件 ID 必须在首次事务中生成,并在重试时复用。若每次发送都生成新 ID,消费者无法识别同一事实的重复投递。敏感字段也不应因为方便而复制进事件,数据最小化和访问边界仍然适用。
发布器可以轮询也可以读取变更日志
轮询发布器按未发送状态或递增主键批量读取记录,发送成功后标记完成。实现直接、可控,但轮询间隔带来延迟,查询和状态更新也增加数据库负载。多个发布器并行时,需要锁定、跳过已锁行或租约,防止长期争抢。
变更数据捕获可从数据库日志识别发件箱插入,减少业务表轮询,并保留提交次序。它依赖日志保留、连接器状态、模式解析和运维能力。CDC 不是不需要治理的旁路,连接器位置和失败恢复同样是关键状态。
无论采用哪种方式,发布器都可能在消息已被代理接收、但发件箱尚未标记完成时崩溃。恢复后它会再次发送。这是正常的至少一次边界,不能通过在代码中调整两行顺序彻底消除。
消费者幂等是完整方案的一部分
消费者可在本地数据库记录已处理事件 ID,并与业务副作用放在同一事务。收到重复事件时,检测到 ID 后返回已有结果。去重记录需要保留多久,取决于上游可重放范围和审计要求;过早清理会让旧事件重新生效。
有些操作天然可按状态覆盖,例如“把资料版本设置为 7”;另一些操作如“余额增加 10”重复执行就会出错。不能用 HTTP 方法名或消息类型猜测幂等性,应为每种副作用定义不变量、唯一约束和冲突处理。
外部邮件、短信和第三方支付可能无法与本地去重事务绑定。此时需要把调用意图再次持久化,使用对方提供的幂等键,或保留人工核对队列。发件箱解决的是本地事务到发布意图,不会自动让整个调用链 exactly-once。
顺序要围绕实体和版本维护
同一实体的多个事件应带聚合 ID 和单调版本。发布器可按事务顺序读取,但并行发送、代理分区和重试仍可能使到达顺序变化。消费者检查版本,可以延迟缺失版本、拒绝旧版本覆盖或触发重新同步。
全局自增 ID可提供数据库提交附近的读取次序,却不一定等于业务因果。多个事务的提交可见性、跨库操作和批量插入都可能改变观察。真正需要的通常是每个聚合内顺序,应把它写进事件契约。
事件模式演进也需要版本。发布器积压数小时后,旧负载可能由新消费者处理;消费者必须兼容仍在保留期内的模式,或通过转换层迁移。直接修改历史发件箱数据会破坏审计和重放依据。
清理策略要保护恢复窗口
已发布记录不能无限增长,也不能发送后立即删除。保留期至少要覆盖代理故障、连接器恢复、消费者重放和审计需求。可按时间分区归档或批量清理,避免大事务和表膨胀影响在线写入。
状态字段更新失败、永久不可序列化的负载和代理拒绝都需要明确处置。无限重试会阻塞后续记录并制造噪声;可以设置退避、最大尝试次数和隔离队列,同时保留人工修复与重新发布的审计轨迹。
发布器的读取查询应有合适索引,并监控最老未发布记录的年龄,而不只是待处理数量。低流量系统可能只有一条卡住记录,数量指标看起来正常,业务延迟却已超标。
验证要覆盖崩溃窗口而不是只走成功路径
测试应在业务提交前、提交后发送前、代理确认后标记前和消费者副作用前后注入故障。每次恢复都要验证业务状态、发件箱状态、代理消息和消费者结果最终收敛,且重复不会增加副作用。
还要模拟积压、乱序、模式升级和清理任务。数据库备份恢复后,发件箱位置可能早于消息代理状态;恢复手册应说明以哪一方为准,如何安全重放,如何识别已完成外部动作。
事务性发件箱是一种边界清楚的可靠发布方法:本地事务确保“状态变化与发布意图同生共死”,发布器和消费者再用至少一次语义、幂等与监控完成链路。把它称为全局事务或绝对一次交付,会掩盖真正需要治理的恢复条件。
资料依据
- PostgreSQL Global Development Group:Transaction Isolation,https://www.postgresql.org/docs/current/transaction-iso.html
- Apache Kafka Project:Kafka Design,https://kafka.apache.org/41/design/design/