可靠消息与事务模式与分布式数据与协调

共 19 题
📑 题目列表 19 题
#
★★★

1. 本地消息表 + 定时任务中可靠发布的经典实现与重复消息的处理?

解释本地消息表 + 定时任务的可靠消息发布经典实现,以及重复消息的处理?

  • 本地消息表与业务同库同事务
  • 定时任务扫描并投递消息
  • 幂等处理重复消息

本地消息表(Local Message Table)方案:在业务数据库里建一张消息表,业务操作与"写消息记录"放在同一个本地事务中——业务成功则消息记录一并提交,从而保证"业务与消息发布"的原子性。随后由定时任务周期性扫描消息表,把待发送(未发送或发送失败)的消息投递到 MQ,并标记发送状态。这样即使投递失败,任务会重试,保证消息最终被发布(可靠投递)。重复消息的处理:由于定时任务可能重发、MQ 投递可能重复,消费端必须幂等——用唯一消息 ID(或业务主键)去重,消费前查重表/或用业务幂等键判断是否已处理,只处理一次。所以本地消息表保证"至少一次"发布,配合消费端幂等实现"恰好一次"业务效果。

核心是"同库同事务保证原子性 + 定时任务重试保证投递 + 消费端幂等去重"。本地消息表把"业务"与"消息"绑定在同一个本地事务,避免先发消息后业务失败的不一致。重复消息靠幂等消费兜底。

// 本地消息表:业务与消息同事务写入
@Transactional
public void businessOperation(Order order) {
    // 1. 业务处理
    orderDao.insert(order);
    // 2. 写入本地消息表(同一事务)
    MessageRecord msg = new MessageRecord(order.getId(), "order.created", serialize(order));
    messageRecordDao.insert(msg);
}

// 定时任务:扫描并发布待发送消息(带重试)
public void publishPendingMessages() {
    List<MessageRecord> pending = messageRecordDao.findPending(100);
    for (MessageRecord m : pending) {
        try {
            mqProducer.send(m.getTopic(), m.getPayload());
            messageRecordDao.markSent(m.getId());
        } catch (Exception e) {
            // 记录失败,下次定时任务重试
        }
    }
}
#
★★★

2. 本地消息表与事务消息中两种方案的实现代价与失败兜底(对账/补偿)差异?

解释本地消息表与事务消息(如 RocketMQ 事务消息)的差异:两种方案的实现代价与失败兜底(对账/补偿)差异?

  • 本地消息表 vs 事务消息的实现机制
  • 实现代价与侵入性
  • 失败兜底(对账/补偿)

本地消息表:业务与消息记录同库同事务写入,靠定时任务扫描发布,实现简单、不依赖 MQ 特殊能力,但需在业务库建表、定时任务轮询,有额外维护成本与轮询延迟。事务消息(如 RocketMQ):利用 MQ 的"半消息"机制——先发半消息(对消费者不可见),本地执行业务事务,成功则提交确认(Commit),MQ 才让消费者可见;失败则回滚(Rollback)。若业务方中途挂了,MQ 回查(check)业务方确认事务状态,决定提交或回滚。实现更优雅、实时性高,但依赖 MQ 支持事务消息、且需要业务方实现回查接口。失败兜底差异:两者都需最终一致性,本地消息表靠定时任务重试 + 对账(比对消息表与 MQ 状态补齐);事务消息靠 MQ 回查 + 对账,两者都需补偿机制处理"业务成功但消息未发"或"消息发了但业务失败"的异常。实现代价上本地消息表侵入业务库、事务消息依赖 MQ 能力。

核心是"本地同库 vs MQ 半消息"。本地消息表简单但侵入库、有轮询延迟;事务消息优雅但依赖 MQ 能力与回查。两者都需对账/补偿作最终兜底。理解"各自代价与兜底方式的差异"是关键。

#
★★★

3. 两阶段提交的阻塞问题中 prepare 后协调者宕机为何参与者只能阻塞等待,3PC 的提交阶段超时终止如何缓解

解释两阶段提交(2PC)的阻塞问题:prepare 后协调者宕机为何参与者只能阻塞等待,3PC 的提交阶段超时终止如何缓解?

  • 2PC 的 prepare/commit 两阶段
  • 协调者宕机导致参与者阻塞
  • 3PC 引入超时与状态机缓解

2PC 第一阶段协调者向所有参与者发 prepare,各参与者执行本地事务并锁定资源、返回"准备就绪";第二阶段协调者根据所有参与者响应决定发 commit 或 abort。阻塞问题:若协调者在 prepare 之后、commit 之前宕机,参与者已锁定资源并等待协调者的最终命令,却无法收到指令,只能一直阻塞等待(锁定资源不释放),导致系统不可用。因为参与者不知道协调者是提交还是回滚,不能自行决定释放资源。3PC 缓解:引入"超时终止"机制——参与者有超时,若在约定时间内未收到协调者命令,会主动执行 abort(或按状态机自行决定),同时增加"CanCommit"预提交阶段,使状态更清晰,参与者可在超时后主动终止,避免无限阻塞。但 3PC 在特定网络分区下仍可能不一致,且多一个阶段增加开销。2PC 的阻塞问题是其"同步阻塞 + 协调者单点"的固有缺陷。

核心是"协调者单点 + 参与者锁定资源等待"。2PC 在 prepare 后宕机,参与者无指令只能阻塞。3PC 用超时终止 + 额外阶段缓解,但仍有不完全性。理解"阻塞源于协调者失败与资源锁定"是关键。

#
★★★

4. 消息乱序与幂等的冲突中消费端如何用业务版本号或状态机拒绝过期消息,与去重表的配合

解释消息乱序与幂等的冲突:消费端如何用业务版本号或状态机拒绝过期消息,与去重表的配合?

  • 消息乱序与幂等处理
  • 业务版本号/状态机拒绝过期消息
  • 与去重表配合

消息乱序时,若消费端只做幂等去重(按消息 ID 去重),可能把"过期的旧消息"当作新消息处理,覆盖新状态。解决:用业务版本号或状态机判断消息是否过期。业务版本号:每条消息携带业务版本号(或更新时间戳),消费端比较版本号,若当前版本号 ≥ 消息版本号则丢弃(拒绝过期消息),否则才更新。状态机:业务状态按状态机流转,消费端只接受"合法状态迁移"的消息,如"已支付"的消息可以接受"已发货",但拒绝"已取消"的过期消息(状态非法迁移则拒绝)。配合去重表:去重表(记录消息 ID/业务键)保证同一消息只处理一次(幂等),版本号/状态机保证只处理"合理的新消息"(乱序拒绝)。两者结合:去重表防重复,版本号/状态机防乱序,共同保证消息消费的正确性。

核心是"幂等去重 vs 乱序丢弃"。去重表解决"重复",版本号/状态机解决"乱序"。理解"去重防重复、版本号防乱序"是关键。

#
★★

5. G-Counter 与 PN-Counter 的 CRDT 语义中 G-Counter 只能单调递增,PN-Counter 如何用两个 G-Counter 表示增减并保证收敛?

解释 G-Counter 与 PN-Counter 的 CRDT 语义:为什么 G-Counter 只能单调递增,PN-Counter 如何用两个 G-Counter 表示增减并保证收敛?

  • G-Counter 的单调递增特性
  • PN-Counter 用两个 G-Counter(增/减)表示
  • 合并收敛性

G-Counter(增长计数器)是 CRDT:每个副本维护一个按副本编号的向量(每副本一个槽位),只允许"增加"操作(递增本副本槽位),合并时取各槽位最大值(max)。因为只增不减,合并结果总会收敛到"所有副本操作的最大值",且各副本合并后一致。它只能单调递增,无法表示"减少"(减库存、取消费)。PN-Counter 用两个 G-Counter 表示:一个 P(positive,只增)记录增加次数,一个 N(negative,只增)记录减少次数,实际值 = P - N。增减都通过各自的 G-Counter 单调累积,合并时分别对 P、N 取 max,从而保证收敛。PN-Counter 能表示正负值(点赞/点踩、库存增减),且天然支持并发增减操作并最终一致。

核心是"G-Counter 只增 + max 合并收敛"与"PN-Counter 用增/减两个 G-Counter 表达"。PN-Counter 用两个单调计数器表示增减,保持 CRDT 的合并收敛性。理解"减法用两个加法合成"是关键。

#
★★

6. PN-Counter 在分布式计数中的实现中如何合并副本的增减计数,在点赞数、库存余量等场景的溢出与负数处理?

解释 PN-Counter 在分布式计数中的实现:如何合并副本的增减计数,在点赞数、库存余量等场景的溢出与负数处理?

  • 副本向量与合并(max)
  • 溢出的处理
  • 负数(库存)的处理

PN-Counter 每个副本维护两个向量(P、N),各按副本编号计数。合并时对每个副本槽位取 max(P 取 max、N 取 max),副本间通过交换合并最终收敛到同一值(P-N)。实现要点:向量大小 = 副本数,可用哈希/节点 ID 索引槽位;合并是逐槽位取 max。溢出处理:计数可能超出整数范围(如极高频点赞),用大整数(Long/BigInteger)或分槽/分批计数避免溢出;库存余量场景:实际值 = P - N 可能为负(超卖),需在应用层判断(如库存不足时拒绝扣减),并配合日志/对账纠正。点赞数场景:P 计赞、N 计踩,值 = P-N,支持并发增减。PN-Counter 保证最终一致,但中间状态可能短暂不一致(各副本值不同),需最终收敛。

核心是"副本向量 + max 合并 + 溢出/负数处理"。PN-Counter 用两个向量合并收敛,溢出用大整数,负数在应用层校验。理解"合并收敛 + 业务侧校验"是关键。

#
★★

7. 消息投递语义的实现中 Exactly-Once 为什么需要幂等消费 + 唯一键去重?

解释消息投递语义的实现:Exactly-Once 为什么需要幂等消费 + 唯一键去重?

  • At-Least-Once 的重复投递
  • 幂等消费 + 唯一键去重实现 Exactly-Once
  • 去重表/唯一约束

消息队列通常提供 At-Least-Once(至少一次)投递:ACK 失败、消费者宕机、重平衡等会导致消息被重复投递/重复消费。要实现 Exactly-Once(恰好一次)的业务效果,不可能靠 MQ 单独保证不重复投递(分布式下极难),只能靠"消费端幂等":消费端用唯一键去重——每条消息带唯一业务 ID(或幂等键),消费前先查去重表(数据库唯一约束/Redis setnx),若已处理过则跳过,否则处理并写入去重记录。这样即使消息被重复投递,消费端也只处理一次。因此 Exactly-Once = At-Least-Once 投递 + 幂等消费(唯一键去重)。去重看实现要原子(查+写)防并发重复,用 DB 唯一约束最可靠。

核心是"Exactly-Once 靠消费端幂等实现"。MQ 保证至少一次,消费端用唯一键去重把重复投递收敛为一次处理。理解"去重表 + 唯一约束"是关键。

#
★★

8. MVCC 的版本链与快照读中 InnoDB 的 undo log 版本链如何支撑可重复读,为什么写写冲突仍需锁而读写不互斥?

解释 MVCC 的版本链与快照读:InnoDB 的 undo log 版本链如何支撑可重复读,为什么写写冲突仍需锁而读写不互斥?

  • undo log 版本链
  • 快照读与可重复读
  • 写写冲突仍需锁、读写不互斥

InnoDB 的 MVCC 用 undo log 维护版本链:每条记录有多个历史版本,通过指针串成链,每个版本带事务 ID(trx_id)。快照读(普通 SELECT)读的是"符合当前事务可见性"的某个版本(通过 ReadView 判断可见性),不阻塞写。可重复读:事务开始时生成 ReadView,后续快照读都基于该 ReadView,因此多次读看到同一快照版本,实现可重复读(且 InnoDB 用间隙锁+当前读处理幻读)。写写冲突仍需锁:两个事务同时改同一行,必须互斥(用行锁/当前读 SELECT ... FOR UPDATE),否则会丢失更新;MVCC 只解决"读写不互斥"(读版本链、写加锁),不解决"写写互斥"。因此读写不互斥(读快照、写锁),写写互斥(都要当前读加锁)。这是"读写并发好、写写仍串行"的经典设计。

核心是"版本链支撑快照读 + 写写仍需锁"。MVCC 让读看旧版本不阻塞写,但写写必须互斥。理解"快照读 vs 当前读"、"读写不互斥 vs 写写互斥"是关键。

#
★★

9. 分布式 ID 生成与消息顺序中雪花算法与全局单调递增 ID 在消息排序场景的取舍?

解释分布式 ID 生成与消息顺序:雪花算法与全局单调递增 ID 在消息排序场景的取舍?

  • 雪花算法(趋势递增、非严格单调)
  • 全局单调递增 ID(严格有序)
  • 消息排序场景的取舍

雪花算法(Snowflake):由时间戳 + 机器 ID + 序列号组成,生成的 ID 在全局"趋势递增"但非严格单调(不同机器时钟偏差、并发下可能乱序),且不保证严格物理顺序。全局单调递增 ID:由单点/中心化发号(如 DB 自增、Redis INCR、或 Ticket Server)生成,严格递增有序,保证按生成顺序排序。消息排序场景的取舍:若需按消息产生的先后严格排序(如严格顺序消费),需用全局单调递增 ID 作为消息序号,保证大小关系与时间顺序一致;雪花 ID 因趋势递增、微乱序,在严格排序场景可能产生错序。但雪花 ID 无中心化瓶颈、可扩展、无单点,适合高并发、无需严格顺序的场景(如作为主键、或仅需"大致有序")。取舍:严格顺序用单调递增 ID(代价是中心化/单点/性能瓶颈),高并发分布式用雪花 ID(代价是仅趋势有序)。也可用雪花 ID + 排序字段(时间戳)结合。

核心是"趋势递增 vs 严格单调"。雪花 ID 分布式高并发但非严格有序,单调递增 ID 严格有序但中心化。消息严格排序用单调 ID,一般场景用雪花。理解"排序需求决定 ID 选择"是关键。

#
★★

10. 消息队列的投递语义中 At-Most-Once / At-Least-Once / Exactly-Once 各自如何实现?

解释消息队列的投递语义:At-Most-Once / At-Least-Once / Exactly-Once 各自如何实现?

  • At-Most-Once(最多一次)
  • At-Least-Once(至少一次)
  • Exactly-Once(恰好一次)

At-Most-Once(最多一次):发送方不重试,可能丢失消息(发送失败即丢弃),实现简单、吞吐高,适合可容忍丢失、不能容忍重复的场景(如日志、监控,重复无意义且可接受少量丢失)。At-Least-Once(至少一次):发送方/消费者在确认(ACK)失败、宕机时重试,保证消息不丢失但可能重复投递,实现普遍(MQ 默认),消费端需幂等处理重复。Exactly-Once(恰好一次):不丢失也不重复,通常靠"At-Least-Once 投递 + 消费端幂等去重(唯一键)"实现,或 MQ 提供事务/分布式事务能力(如 Kafka 的幂等 + 事务,用 transaction 保证跨分区原子写入),但实现复杂、代价高。取舍:按业务重要性选,"最多一次"最省但丢消息,"至少一次"普遍但需幂等,"恰好一次"最严格但代价高。

核心是"三种语义的投递/重试策略"。最多一次不重试怕重复,至少一次重试怕重复需幂等,恰好一次靠幂等+去重/事务。理解"重试与幂等的组合决定语义"是关键。

#
★★

11. OR-Set 与 Add-wins/Remove-wins 中为什么同时增删同一元素时需按操作类型裁定胜负,与 G-Set 的关系

解释 OR-Set 与 Add-wins/Remove-wins:为什么同时增删同一元素时需按操作类型裁定胜负,与 G-Set 的关系?

  • OR-Set(Observed-Remove Set)
  • Add-wins / Remove-wins 语义
  • 与 G-Set 的关系

G-Set(Grow-only Set)只支持添加,可合并收敛,但不能删除。OR-Set(Observed-Remove Set)支持增删:每个元素带唯一标识(tag),添加时生成新 tag,删除时记录"被移除的 tag 集合",合并时"若某元素的所有 tag 都被移除则删除,否则保留"。这样即使并发增删,也能收敛。为何需按操作类型裁定胜负:当两个副本同时对一个元素执行"添加"与"删除"且并发无先后,若不加裁定则结果不确定。Add-wins 语义:并发时"添加"获胜(即使有删除,只要添加存在则元素保留),适合"用户重新添加后应保留"的场景;Remove-wins 语义:删除获胜(添加的元素若被删除则移除),适合"删除是最终意图"的场景。与 G-Set 的关系:G-Set 是最简单的只增集合,OR-Set 是 G-Set 的扩展(引入删除与 tag),用"tag 标记 + 移除集合"实现增删并保证收敛。G-Set 是 OR-Set 的基础(添加部分类似 G-Set)。

核心是"并发增删需裁定 + tag 机制"。OR-Set 用 tag 区分版本,Add-wins/Remove-wins 定义并发时的胜负。G-Set 是只增基础,OR-Set 是其删除扩展。理解"tag + 裁定语义保证收敛"是关键。

#

12. TCC 与本地消息表的组合中 Try 阶段预留的资源如何在 Confirm 失败时释放,与 Outbox 组合保证最终一致性的边界?

解释 TCC 与本地消息表的组合:Try 阶段预留的资源如何在 Confirm 失败时释放,与 Outbox 组合保证最终一致性的边界?

  • TCC 的 Try/Confirm/Cancel
  • Confirm 失败时资源释放
  • 与 Outbox 组合的最终一致性边界

TCC(Try-Confirm-Cancel)把事务拆成三阶段:Try 阶段预留资源(如冻结库存、锁定余额),Confirm 阶段确认提交(把预留转正),Cancel 阶段释放预留(回滚)。若 Confirm 失败,需调用 Cancel 释放 Try 阶段预留的资源(逆向操作),并重试 Confirm 或转人工。与本地消息表/Outbox 组合:Try 阶段写业务 + 写消息(Outbox)到本地事务,Confirm 成功后发布消息;Confirm 失败时触发 Cancel 释放资源,同时消息不发布(或发补偿消息)。最终一致性边界:Outbox 保证"业务写入与消息发布原子",TCC 保证"跨服务资源预留与确认/取消",两者结合保证"业务 + 消息 + 资源"的最终一致。但边界在于:Confirm/Cancel 必须幂等可重入,跨服务无分布式锁时可能临时不一致,且 TCC 的 Confirm 一般"不能失败"(设计上应最终成功),否则陷入重试。组合的兜底是重试 + 对账 + 人工。

核心是"Try 预留、Confirm 转正、Cancel 释放"与"Outbox 原子发布"的组合。Confirm 失败用 Cancel 释放资源,Outbox 保证消息原子,最终一致性靠重试+对账兜底。理解"二者组合的边界与幂等要求"是关键。

#

13. 可靠消息投递的三种模式中事务消息、本地消息表、Outbox 的对比与各自的一致性边界?

解释可靠消息投递的三种模式:事务消息、本地消息表、Outbox 的对比与各自的一致性边界?

  • 三种模式的实现机制
  • 各自的一致性边界
  • 对比取值

三种可靠消息投递模式:1) 本地消息表:业务与消息记录同库同事务,定时任务扫描发布,实现简单、不依赖 MQ 能力,但有轮询延迟,一致性边界是"业务库与消息表同事务保证原子,投递靠重试";2) 事务消息(如 RocketMQ):MQ 半消息 + 本地事务 + 回查,业务提交后消息才可见,一致性边界是"MQ 与业务通过回查协调",依赖 MQ 事务能力;3) Outbox:业务表与 outbox 表同库同事务写入,由 CDC/轮询发布 outbox 里的消息到 MQ,一致性边界是"业务与 outbox 原子,发布靠 CDC/轮询 + 重试"。三者共同点:都保证"业务写入与消息待发布记录原子",再通过异步发布 + 重试保证消息最终投递,消费端幂等保证最终一致。差异:本地消息表与 Outbox 是"同库表 + 异步发布",本地消息表用定时任务、Outbox 用 CDC/轮询;事务消息是"MQ 半消息 + 回查"。一致性边界:三者都实现"至少一次 + 最终一致",靠幂等消费达到恰好一次。

核心是"原子记录 + 异步发布 + 重试"。三者都保证业务与消息记录原子,区别在发布机制(定时任务/CDC/半消息回查)。理解"共同点(原子记录)与差异(发布机制)"是关键。

#

14. Outbox 模式如何保证数据库写入与消息发布原子性,轮询发布与 CDC(如 Debezium)两种实现的差异?

解释 Outbox 模式如何保证数据库写入与消息发布原子性:轮询发布与 CDC(如 Debezium)两种实现的差异?

  • Outbox 同库同事务
  • 轮询发布 vs CDC 发布
  • 两种实现的差异

Outbox 模式:业务表与 outbox 表在同一数据库、同一事务中写入——业务变更时同时插入一条 outbox 记录,从而保证"业务写入与消息待发布记录"原子(要么都成功要么都失败)。随后由发布器把 outbox 记录投递到 MQ 并删除/标记。发布器两种实现:1) 轮询发布:定时任务周期性扫描 outbox 表,把未发布记录发送到 MQ,标记已发送。实现简单、不依赖数据库能力,但有轮询延迟(实时性差)、需处理并发扫描与重复。2) CDC(如 Debezium):监听数据库 binlog(change data capture),把 outbox 表的插入/变更实时解析为事件发布到 MQ。实时性高、无轮询延迟、不侵入应用代码,但依赖数据库 CDC 能力、需要配置与运维(binlog、连接器)。差异本质:轮询是"应用主动周期扫描",CDC 是"数据库变更被动触发"。实时性、侵入性、运维复杂度不同。

核心是"同库原子 + 发布机制差异"。Outbox 保证原子性,轮询简单有延迟、CDC 实时但依赖 binlog。理解"轮询主动 vs CDC 被动"是关键。

#

15. TCC 的 Confirm/Cancel 幂等设计中为什么 TCC 要求 Confirm/Cancel 可重入且不能失败,与 Saga 的逆向补偿有何区别?

解释 TCC 的 Confirm/Cancel 幂等设计:为什么 TCC 要求 Confirm/Cancel 可重入且不能失败,与 Saga 的逆向补偿有何区别?

  • Confirm/Cancel 可重入(幂等)
  • Confirm/Cancel 不能失败
  • 与 Saga 逆向补偿的区别

TCC 要求 Confirm/Cancel 可重入且不能失败:因为网络重试、故障恢复会导致 Confirm/Cancel 被多次调用,必须幂等(多次执行结果一致);且 Confirm/Cancel 一旦进入执行阶段就应"最终成功"(不能失败),否则会陷入无限重试或不确定状态——因此 TCC 设计上把可能失败的操作放在 Try 阶段,Confirm/Cancel 只做简单、不失败、可重入的提交/回滚。与 Saga 逆向补偿的区别:TCC 的 Cancel 是"释放 Try 阶段预留的资源"(针对预留资源,幂等性好),Saga 的补偿是"逆向业务操作"(如已扣款则退款,是重新执行一个补偿业务);TCC 是"预留-确认-取消"三阶段(Try 时资源已锁定),Saga 是对已完成的事务做"逆向补偿"(无预留,直接执行补偿操作)。TCC 适合资源可预留、需强一致预留的场景;Saga 适合无预留、可补偿的业务操作。

核心是"幂等可重入 + 不能失败"与"预留 vs 逆向补偿"。TCC 用 Try 预留换取 Confirm/Cancel 简单可靠,Saga 用补偿业务操作回滚。理解"预留-确认 vs 执行-补偿"是关键。

#

16. 分布式数据的分片与复制中一致性哈希与 Raft 复制组如何配合实现扩展与容错?

解释分布式数据的分片与复制:一致性哈希与 Raft 复制组如何配合实现扩展与容错?

  • 一致性哈希分片实现扩展
  • Raft 复制组实现容错
  • 分片与复制的配合

分布式数据系统通常用"分片(sharding)+ 复制(replication)"实现扩展与容错。一致性哈希分片:把数据按 key 哈希到环上,数据分布到多个分片节点,增加/删除节点时只影响环上相邻区域的数据(迁移小),实现水平扩展。Raft 复制组:每个分片由一个"复制组"维护(一主多从),主节点接收写请求,把日志复制到从节点,多数派(quorum)确认后提交,实现容错(主节点故障时从节点选举新主)。配合:先按一致性哈希把数据分片(扩展维度),每个分片内部用 Raft 复制组保证高可用(容错维度)。即"分片解决扩展、Raft 解决容错":分片让数据分散到多组避免单点瓶颈,Raft 让每组在主节点故障时仍可用。数据路由:请求按哈希定位到分片,再访问该分片的复制组(主节点)。两者结合实现"可扩展 + 高可用"。

核心是"分片管扩展、复制管容错"。一致性哈希让数据分散可扩展,Raft 让每组故障自愈。理解"分片与复制两个正交维度"是分布式存储的关键。

#

17. 分布式协调服务选型中 ZooKeeper 的 ZAB 与 etcd 的 Raft 在领导选举/配置发布上的差异?

解释分布式协调服务选型:ZooKeeper 的 ZAB 与 etcd 的 Raft 在领导选举/配置发布上的差异?

  • ZAB 与 Raft 的共识协议差异
  • 领导选举机制
  • 配置发布能力

ZooKeeper 用 ZAB(ZooKeeper Atomic Broadcast)协议,etcd 用 Raft 协议,两者都是共识协议但实现有差异。领导选举:ZAB 用"事务 ID(zxid)+ 多数派"选举,zxid 单调递增,保证选举出"数据最新"的 leader;Raft 用"任期(term)+ 投票"选举,节点先发起投票、获得多数派成为 leader,Raft 用随机超时避免选举分裂。配置发布:ZooKeeper 提供持久/临时节点、watch 机制(客户端监听节点变化、变更通知),适合服务发现、配置发布、分布式锁;etcd 基于 Raft 提供强一致读写,watch 机制 + lease 租约,适合配置发布、服务发现、Kubernetes 等。差异:ZooKeeper 的 watch 是一次性的(需重新注册),etcd 的 watch 可持续;ZooKeeper 会话(session)管理复杂,etcd 用 lease 更简洁;两者都提供 leader 选举与配置存储,但 API 与生态不同(etcd 更受云原生/K8s 青睐)。选型:已有 ZooKeeper 生态(如 Kafka、HBase)用 ZK,云原生/新系统常用 etcd(Raft)。

核心是"ZAB vs Raft 的选举与 watch 差异"。两者都做共识与配置,但实现细节(zxid vs term、一次性 watch vs 持续 watch)与生态不同。理解"协议差异与生态匹配"是关键。

#

18. Saga 与 TCC 的区别中 Saga 的补偿是逆向业务操作而 TCC 是预留-确认-取消,各自适合什么场景?

解释 Saga 与 TCC 的区别:为什么 Saga 的补偿是逆向业务操作而 TCC 是预留-确认-取消,各自适合什么场景?

  • Saga 的补偿(逆向业务操作)
  • TCC 的预留-确认-取消
  • 各自适用场景

Saga 把长事务拆成多个本地事务,逐个执行,若某步失败则逆向执行之前各步的补偿操作(如已扣款则退款、已下单则取消)。补偿是"逆向业务操作",即重新执行一个合理的业务动作来抵消副作用,不要求事前预留资源。TCC 则把事务拆成 Try/Confirm/Cancel:Try 阶段先预留资源(冻结库存、锁定余额),Confirm 阶段把预留转正,Cancel 阶段释放预留。TCC 的 Cancel 是针对"预留资源"的释放(幂等、简单),而非逆向业务。区别本质:Saga 是"执行后补偿"(无预留,补偿是逆向业务),TCC 是"预留-确认-取消"(先预留,确认/取消针对预留)。适用场景:Saga 适合"业务操作天然可补偿、无需预留资源"(如订单、支付流程中可退款的步骤),补偿逻辑清晰;TCC 适合"需要提前锁定资源、保证预留"(如库存、余额、座位预订,需强一致预留),但实现复杂、要求 Try 阶段预留。TCC 适合资源紧张、需预占的场景;Saga 适合可最终补偿、无预占的场景。

核心是"预留-确认 vs 执行-补偿"。TCC 用 Try 预留换取简单的确认/取消,Saga 用逆商业操作补偿。理解"预留资源 or 事后补偿"决定选 TCC 还是 Saga。

#

19. 对账任务兜底中对账窗口、差异发现与自动修复如何作为最终一致性的最后防线

解释对账任务兜底:对账窗口、差异发现与自动修复如何作为最终一致性的最后防线?

  • 对账窗口(对账时间范围)
  • 差异发现
  • 自动修复与人工兜底

对账任务作为最终一致性的最后防线,保障"消息/事务虽经重试但仍可能遗留的不一致"。对账窗口:每次对账处理的时间范围(如过去 1 小时/1 天的数据),需覆盖超过最大重试/补偿延迟,保证不留遗漏。差异发现:定时对账任务把"两个数据源"(如业务库与消息侧、订单表与支付对账单、本地与远端)按对账键比对,找出不一致(如"业务成功但消息未发""订单已支付但未入账""存在悬挂记录")。自动修复:对发现的差异按规则自动处理——重发消息、补写记录、执行补偿、标记状态,尽量让系统自动收敛到一致;对无法自动修复的差异(如金额不一致、需人工判断)告警并转人工处理。对账本质是"兜底的最终校验",与重试、补偿、幂等共同构成最终一致性的多重保障。对账实现要点:用幂等键对齐、批量处理、防重复触发、记录对账日志。

核心是"窗口 + 差异发现 + 自动修复"。对账是最后一道防线,覆盖重试/补偿之外的不一致。理解"对账窗口要覆盖最大延迟、差异自动修复+人工兜底"是关键。