知识卡片

基于日志消息传递实现唯一性约束与多分区请求处理

结构图卡

内容

唯一性约束(用户名、座位预订、账户余额不为负)在分布式环境下需要共识——多个并发请求争抢同一值时,系统必须决定接受哪个、拒绝其余的,通常做法是让单个节点作为领导者集中决策,本质上又回到[[单领导者复制通过纪元编号与法定人数回避而非消除共识]]的共识问题;[[全序广播的两个安全属性与状态机复制]]恰好提供了另一条路:把日志按需要唯一性的字段分区(如按用户名散列),流处理器单线程依序消费单个分区内的全部消息,因此能无歧义、确定性地判定几个冲突操作里谁先到达——申请可用用户名的记为成功并写入本地表,申请已占用用户名的记为拒绝,客户端监视输出流等待结果,这套算法本质上和”用全序广播实现线性一致存储”是同一套逻辑,且能靠增加分区数水平伸缩。这个思路还能扩展到多分区场景:例如账户转账涉及请求ID、付款方、收款方三个独立分区,传统方案要跨三个分区做原子提交;分拆方案则是先把请求本身(带唯一请求ID)作为单条消息写入按请求ID分区的日志(单对象写入天然原子),流处理器读取后向输出流分别发出按付款方分区的借记指令和按收款方分区的贷记指令(都带着原始请求ID),下游处理器再按请求ID去重后应用余额变更——即便流处理器中途崩溃重启导致重复生成借记贷记指令,下游也能用请求ID轻松去重,从而在没有原子提交协议、也没有跨分区协调的情况下达成”每个请求对双方都恰好生效一次”的效果。

结构图

flowchart TD
    A[客户端提交带唯一请求ID的转账请求] --> B[写入按请求ID分区的日志: 单对象写入天然原子]
    B --> C[流处理器读取请求日志]
    C --> D[发出借记指令: 按付款方账户分区]
    C --> E[发出贷记指令: 按收款方账户分区,均带原始请求ID]
    D --> F[下游处理器按请求ID去重后应用余额变更]
    E --> F
    F -.即使流处理器崩溃重启重复生成指令,去重也能保证恰好生效一次.-> G[无需跨分区原子提交]

参考来源

- 位置:《数据密集型应用系统设计》第十二章《数据系统的未来》"基于日志消息传递中的唯一性""多分区请求处理"(源文件:_epub-src/ch12_split_002.html) - 结论依据:原文描述用按需唯一字段分区+流处理器单线程顺序消费来无歧义判定冲突操作的算法,并给出转账场景中先把请求写成单条消息、再衍生出借记贷记指令、最后按请求ID去重的多分区处理方案,说明可以不用原子提交实现同等正确性,直接支撑本卡片的结构图与解释。 - 原始内容:流处理器依序读取日志中的请求,并使用本地数据库来追踪哪些用户名已经被占用了……我们首先将请求持久化记录为单条消息,然后从这第一条消息中衍生出贷记指令与借记指令……可以使用端到端的请求ID轻松地对其除重。