模块四 · 数据与分布式
MQ 与分布式:把一致性讲清楚
MQ 题不是背组件,而是回答"消息会不会丢、会不会重、会不会乱"。这一节按可靠投递、幂等、顺序、分布式事务、分布式锁讲。
读完这一节,你能做到
- 说清 MQ 的三类价值与选型依据。
- 用"生产-存储-消费"三段解释消息不丢。
- 设计幂等与顺序消费方案。
- 在 2PC、TCC、Saga、本地消息表、事务消息之间做选择。
一、MQ 的价值与选型
三个价值:解耦(生产者不关心消费者)、削峰(把突发流量排队)、异步(缩短主链路)。
| 组件 | 特点 | 适用 |
|---|---|---|
| Kafka | 高吞吐、分区顺序、生态好 | 日志、流处理 |
| RocketMQ | 事务消息、延迟消息、顺序消息 | 电商交易 |
| RabbitMQ | 路由灵活、延迟低 | 业务解耦、任务队列 |
选型看三件事:吞吐、可靠性要求、是否需要事务/延迟/顺序消息。
二、可靠投递:三段都要管
| 阶段 | 风险 | 方案 |
|---|---|---|
| 生产 | 网络抖动,消息没到 Broker | 同步发送 + 重试 + 本地消息表 |
| 存储 | Broker 宕机,内存消息丢失 | 持久化 + 多副本 + 刷盘策略 |
| 消费 | 消费失败或重复 | 手动 ACK + 重试 + 死信队列 |
"消息不丢"是三个环节共同保证的,只做一段没有意义。反过来,可靠性越高,吞吐越低,要按业务取舍。
三、幂等与顺序
消息至少一次投递意味着可能重复,消费端必须幂等:
- 唯一索引:用业务唯一键防重。
- 状态机:只允许合法状态迁移。
- 去重表/Redis setnx:记录已处理的消息 ID。
顺序消费:
- 同一业务键路由到同一分区/队列(如订单 ID 取模)。
- 消费端单线程处理该分区,或用本地队列按 key 串行。
- 不要指望全局顺序,代价极高且通常不需要。
四、分布式事务的五种方案
| 方案 | 一致性 | 侵入性 | 适用 |
|---|---|---|---|
| 2PC/XA | 强一致 | 高,锁资源久 | 传统数据库、短事务 |
| TCC | 最终一致 | 高,要写 Try/Confirm/Cancel | 资金、库存 |
| Saga | 最终一致 | 中,要写补偿 | 长流程 |
| 本地消息表 | 最终一致 | 低 | 大多数业务 |
| 事务消息 | 最终一致 | 中 | RocketMQ 生态 |
本地消息表的核心:业务与"待发送消息"在同一个本地事务里落库,再由后台任务投递,失败重试;消费端幂等。它把"分布式事务"降级为"本地事务 + 可靠消息"。
五、分布式锁
三种实现:
- Redis:
SET key value NX PX,释放时用 Lua 校验 value;要考虑续期(看门狗)与主从切换丢锁。 - ZooKeeper:临时顺序节点,天然避免脑裂,性能低于 Redis。
- 数据库:唯一索引或
select for update,简单但性能差。
Redis 锁的边界:主从异步复制下,主节点宕机可能丢锁;对强一致要求高的场景用 RedLock 或 ZooKeeper,但仍要评估代价。
六、常见坑
- 只做生产重试,不做消费幂等。
- 消费失败无限重试,堵住队列;要有限重试 + 死信。
- 用全局顺序消费,吞吐骤降。
- 事务消息当成"强一致",忽略消费端失败。
- 分布式锁不设过期时间,宕机后死锁。
七、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 用本地消息表实现"下单 + 发消息"的最终一致。
- 用 Redis 写一个带 value 校验与过期时间的分布式锁。
- 模拟消费重复,用唯一索引保证幂等。
② 让 AI 写同一题
把第 2 题(可靠消息与幂等)交给 AI,补一句 说明重复投递时怎么保证只生效一次。
③ 对比要盯的点
- 消费端它做幂等了吗?还是默认“消息只来一次”。
- 顺序消息它用了什么方案?全局顺序与分区顺序的代价差别很大。
- 分布式事务它给的是 2PC、TCC 还是最终一致?有没有说清适用边界。
④ 一句话取舍
同一条消息投递两次时,它那版会多扣一次钱吗?
八、自测
- MQ 的三类价值是什么?
- 消息不丢要管哪三段?
- 幂等的三种实现方式?
- 本地消息表为什么能替代分布式事务?
- Redis 分布式锁的边界是什么?
参考答案要点:解耦/削峰/异步;生产/存储/消费;唯一索引/状态机/去重表;本地事务 + 可靠消息;主从切换丢锁。
附录:高频追问
问:消息重复消费怎么处理? 消费端幂等,不要依赖"消息只投一次"。
问:如何保证消息顺序? 同一业务键进同一分区,消费端串行处理;全局顺序代价很高。
问:消息积压怎么办? 先扩容消费者,再检查消费逻辑是否变慢;必要时临时转存再处理。
问:事务消息的两阶段是什么? 半消息 → 本地事务 → 提交/回滚;Broker 回查未确定的消息。
问:TCC 的空回滚与悬挂怎么解决? 记录事务状态,Cancel 前检查 Try 是否执行;用防悬挂标记。
问:Saga 与 TCC 的区别? Saga 是长流程的补偿,TCC 是资源预留;前者侵入低但中间状态可见。
问:分布式锁为什么要续期? 业务执行时间可能超过锁过期时间,看门狗定期延长。
问:Redis 主从切换会丢锁吗? 会。异步复制下新主可能没有锁记录,需要额外机制。
问:如何保证消费不阻塞? 限流、线程池隔离、失败进死信,不要让一个坏消息拖住整个队列。
问:MQ 如何做延迟消息? RocketMQ 延迟级别、RabbitMQ 死信 + TTL、Kafka 时间轮或外部调度。
消息积压的应急处理
- 先确认是消费者变慢还是生产突增。
- 消费者无状态时直接扩容,注意分区数上限。
- 有状态或下游受限时,先把消息转存到临时队列,再慢慢消费。
- 排查是否有单条消息反复失败,转入死信。
- 事后补监控:积压量、消费速率、失败率。
积压时不要盲目重启消费者,可能造成重复消费与顺序错乱。
顺序消息的边界
顺序只在“同一分区/队列内”成立。跨分区、跨 topic 都不保证;消费端多线程也会打乱顺序。要严格顺序,就把同一业务键固定到同一分区,并串行消费,接受吞吐下降。
消费失败的三种处理
- 可重试错误:有限次退避重试,仍失败进死信。
- 不可重试错误(参数非法):直接进死信并告警,不要占住队列。
- 依赖故障:暂停消费或降低速率,等依赖恢复。
关键是把“失败”分类,而不是统一无限重试。
补遗:CAP 与 BASE
CAP 三者只能同时满足两个;分区容错 P 在分布式系统里必须保留,所以实际是在 C 与 A 之间取舍。BASE 是基本可用、软状态、最终一致,是大多数互联网系统的选择。
补遗:分布式 ID
UUID(本地生成、无序、占空间);数据库自增(简单、单点瓶颈);号段模式(批量取号,推荐);雪花算法(时间 + 机器 + 序列,趋势递增,依赖时钟);Redis INCR(性能好、依赖可用性)。时钟回拨要让雪花算法报警或等待。
补遗:限流算法
计数器(简单、临界突刺);滑动窗口(平滑、内存开销);漏桶(恒定速率、不允许突发);令牌桶(允许突发、最常用)。分布式限流用 Redis + Lua 保证原子性,或网关层统一限流。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
- ★★★★☆ 怎么保证消息不丢?
- ★★★★☆ 如何保证消息幂等,避免重复消费?
- ★★★☆☆ 消息积压了怎么处理?如何保证顺序消费?
- ★★★★☆ CAP 理论是什么?BASE 又解决了什么问题?
- ★★★★☆ 分布式事务有哪些方案?分别适合什么场景?
提示:读完可以顺手做几道对应题目,检验是否真的理解