分布式协同面试
分布式协同面试
复制
【简单】什么是复制?复制有什么作用?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:分布式协同 / 复制基础
💎 关键结论
复制就是把同一份数据存到多台机器上。核心收益就三个:机器挂了系统还能用(可用性)、数据离用户更近(降延迟)、多台机器一起扛读请求(提吞吐)。一切复制技术都是围绕这三点展开的。
⚡记忆卡片
- 口诀:一份数据多处存,可用、低延、高吞吐
- 关键词:多机副本 / 可用性 / 地理就近 / 读扩展
- 链路:数据存多份 → 部分节点故障仍可服务 → 就近部署降低延迟 → 多节点并行读提升吞吐
📖 核心知识
复制是将同一份数据存储在多台机器上,以提升系统的可用性、可靠性和性能。
复制数据,可能出于各种各样的原因:
- 提高可用性:当部分组件出现故障,系统依然可以继续工作。
- 降低访问延迟:使数据在地理位置上更接近用户。
- 提高读吞吐量:扩展至多台机器以同时提供数据访问服务。
【中等】复制有哪些模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 复制模式
💎 关键结论
复制就三种模式:主从(单写多读,最常用)、多主(多个可写节点,适合多数据中心)、无主(任意节点可写,靠 Quorum 收敛)。选哪种本质是问"写入入口要不要唯一",入口越分散,冲突处理越复杂。
⚡记忆卡片
- 口诀:主从单写、多主多写、无主随便写
- 关键词:主从复制 / 多主复制 / 无主复制 / 写冲突
- 链路:写入口数量增加 → 并发写冲突概率上升 → 需要冲突解决与数据修复机制兜底
📖 核心知识
复制的模式有以下几种:
- 主从复制:所有的写入操作都发送到主节点,由主节点负责将数据更改事件发送到从节点。每个从节点都可以接收读请求,但内容可能是过期值。支持主从复制的系统:
- 数据库:MySQL、PostgreSQL、MongoDB 等
- 消息队列:Kafka、RabbitMQ 等
- 多主复制:系统存在多个主节点,每个都可以接收写请求,客户端将写请求发送到其中的一个主节点上,由该主节点负责将数据更改事件同步到其他主节点和自己的从节点。
- 无主复制:系统中不存在主节点,每一个节点都能接受客户端的写请求。此外,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。支持无主复制的系统:
- 数据库:Cassandra
此外,复制还需要考虑以下问题:
- 同步还是异步
- 如何处理失败的副本
- 如何保证数据一致
🔬 扩展知识
详情
【L3】三种模式的选型直觉:写入口唯一(主从)实现最简单但主节点是写入单点;写入口分散(多主/无主)可用性更好,但必须引入冲突解决(LWW、版本向量)与副本修复(读修复、反熵)机制,复杂度陡增。单数据中心内没有理由用多主。
【L4】现代系统的混合形态:Kafka 的副本是主从模型(Leader/ISR),但 Controller 本身靠外部协调服务选主;Cassandra 是无主模型但允许指定"本地优先"写。理解模型要看"写入如何排序",而不是节点叫什么名字。
🔀 发散问题
主从复制中主节点挂了怎么办?
需要故障转移(failover):把某个从节点提升为新主,并让客户端和其他从节点切换路由。难点在于新主可能缺数据、以及脑裂风险,详见本文档『如何通过主从复制技术来实现系统高可用呢?』。
为什么从节点读到的可能是过期值?
异步复制下从节点的数据同步滞后于主节点,存在复制延迟窗口,详见本文档『什么是复制延迟问题?有哪些常见的解决思路?』。
【中等】主从复制的工作原理是什么?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 主从复制
💎 关键结论
主从复制一句话:写只进主节点,主把变更日志按序发给从,从照着重放,读可以打到任意从。关键约束是"只有主能写",所以主挂了所有写都停,这是该模型最大的可用性短板。
⚡记忆卡片
- 口诀:主写、日志传、从重放、随便读
- 关键词:主副本 / 复制日志 / 写入顺序 / 只读从副本
- 链路:写请求到主 → 主写本地存储 → 变更日志发往从 → 从按相同顺序重放 → 从可服务读请求
📖 核心知识
主节点负责写操作,从节点同步主节点数据并处理读操作。
最常见的解决方案就是主从复制,其原理如下:
主从复制模式中只有一个主副本(或称为主节点),其余称为从副本(或称为从节点)。
- 所有的写请求只能发送给主副本,主副本首先将新数据写入本地存储。
- 然后,主副本将数据更改作为复制的日志或更新流发送给所有从副本。每个从副本获得更新数据之后将其应用到本地,且严格保持与主副本相同的写入顺序。
- 读请求既可以在主副本上,也可以在从副本上执行。
再次强调,只有主副本才可以接受写请求:从客户端的角度来看,从副本都是只读的。如果由于某种原因,例如与主节点之间的网络中断而导致主节点无法连接,主从复制方案就会影响所有的写入操作。

🔬 扩展知识
详情
【L3】"严格保持与主副本相同的写入顺序"是主从复制正确性的根基:日志是串行单流的,从节点顺序重放即可收敛。这也解释了为什么主从复制天然是"最终一致"——从节点只是暂时落后,顺序不会错。
【L4】主从复制的瓶颈演化:写吞吐受限于单机主节点;当单主扛不住写时,要么纵向扩容,要么转向多主/无主模型,或者用分区把数据切到多个"各自带主从的小集群"(分区 + 复制的组合是大规模系统的标准做法)。
⚠️ 常见误区
详情
常见误区:
- ❌ "从副本也可以接受写请求,写完成后再同步给主" → 那是多主复制,不是主从。主从模型中从副本对客户端是只读的。
- ❌ "主节点挂了,系统只是不能读" → 恰恰相反,主挂了影响的是所有写操作;读请求仍可由从副本继续服务。
- ❌ "从节点重放日志的顺序可以和主不同,只要最终数据一样" → 写入顺序必须与主副本严格一致,否则并发更新语义会被破坏。
🔀 发散问题
主从复制的日志长什么样?
可以是语句、WAL、逻辑日志或触发器,各有取舍,详见本文档『复制日志的工作原理是什么?』。
同步复制和异步复制怎么选?
全同步强一致但易阻塞,全异步高性能但主挂可能丢数据,生产多用半同步折中,详见本文档『同步复制、半同步复制、异步复制有什么差异?』。
【中等】同步复制、半同步复制、异步复制有什么差异?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 复制同步性
💎 关键结论
同步等所有从节点复制完才返回(强一致但一个从卡住全局阻塞),异步不等(快但主挂丢数据),半同步只等一个或半数从节点确认(保底有两个最新副本)。生产几乎都用半同步,全同步不切实际。
⚡记忆卡片
- 口诀:同步等全部、异步谁不等、半同步等一个
- 关键词:全同步 / 全异步 / 半同步 / 阻塞 / 数据丢失
- 链路:等待复制确认的范围越大 → 一致性越强 → 但可用性与性能越差
📖 核心知识

一般,复制速度会非常快;但是,系统不能保证复制多久能完成。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如:从节点刚从故障中恢复;或系统已经接近最大设计上限;或节点之间的网络出现问题。
- 全同步复制:
- 优点:只有所有从节点都完成复制,才视为成功,因此是强一致的。
- 缺点:即使只有一个从节点未完成复制,写入都不能视为成功。所有从节点完成复制过程之前,主节点会阻塞后续所有的写操作。
- 因此,把所有从节点都配置为同步复制有些不切实际。因为这样的话,任何一个同步节点的中断都会导致整个系统更新停滞不前。
- 全异步复制:
- 优点:不管从节点上数据多么滞后,主节点总是可以继续响应写请求,系统的性能更好。
- 缺点:如果主节点发生故障且不可恢复,则所有尚未复制到从节点的写请求都会丢失。
- 半同步复制(折中方案):只要有一个从节点或半数以上的从节点同步成功,就视为同步,直接返回结果;剩下的节点都通过异步方式同步。万一同步的从节点变得不可用或性能下降,则将另一个异步的从节点提升为同步模式。这样可以保证至少有两个节点(即主节点和一个同步从节点)拥有最新的数据副本。
🔬 扩展知识
详情
【L3】MySQL 的半同步复制(5.5 引入)默认是"至少一个从库收到 relay log 并写入"即返回;5.7 引入增强半同步(enhanced semi-synchronous replication),把确认点移到引擎提交之前(after_sync),进一步降低主库崩溃丢已确认事务的概率。版本不同语义有差异,引用结论时应注明版本。
【L4】半同步的隐性代价:那个"同步从节点"成了写路径上的关键依赖,它变慢会直接拖慢主库所有写入。因此生产上常把同步从节点放在与主同机房低延迟链路上,并监控其复制延迟。
⚠️ 常见误区
详情
常见误区:
- ❌ "同步复制下从节点的数据永远和主一致,不存在延迟" → 同步复制只保证"返回成功的数据已复制",异步的那部分从节点(半同步场景)仍可能滞后。
- ❌ "异步复制性能最好,所以核心业务也选异步" → 主节点故障不可恢复时,未复制的写入全部丢失,资金类业务不可接受。
- ❌ "半同步就是等半数以上从节点确认,别无其他" → 常见实现是"一个从节点确认即可",且同步从节点故障时会自动降级/切换,机制是动态的。
🔀 发散问题
新加入的从节点怎么把数据追上来?
靠快照 + 变更日志追赶,详见本文档『新的从节点如何复制主节点数据?』。
从节点长期落后会导致什么问题?
读旧数据、读写不一致等复制延迟问题,详见本文档『什么是复制延迟问题?有哪些常见的解决思路?』。
【中等】新的从节点如何复制主节点数据?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 副本初始化
💎 关键结论
新从节点上线不能"直接拷文件"(数据在变,拷不一致),也不能"锁库再拷"(违反高可用)。标准做法是:先打快照、拷快照、期间变更写日志,拷完后回放日志追赶,直至与主一致。
⚡记忆卡片
- 口诀:快照打底,日志追赶
- 关键词:快照 / 变更日志 / binlog / AOF / 追赶
- 链路:生成主节点快照 → 从节点复制快照 → 期间变更写入日志 → 从节点回放日志追赶 → 主从数据一致
📖 核心知识
两种不可行的方案:
- 由于主节点会源源不断接受新的写入数据,数据始终处于变化中,因此一次性从主节点复制数据到从节点是无法保证数据一致的。
- 另一种思路是:考虑锁定数据库(使其不可写)来使磁盘上的文件保持一致,但这会违反高可用的设计目标。
可行的方案:初始化连接 → 全量数据同步 → 增量数据同步
- 生成主节点某时刻的快照,避免长时间锁定数据库。
- 将快照复制到从节点。
- 从节点复制主节点快照过程中,所有的数据变更写入一个日志中(这个数据变更日志在不同数据库中有着不同的称呼,MySQL 称其为 binlog;Redis 称其为 AOF)。
- 从节点复制完主节点的快照后,请求数据变更日志中的数据,并基于此补全数据,这个过程称为追赶,直至主从数据一致。并重复步骤 1 ~步骤 4。
🔬 扩展知识
详情
【L3】快照的"一致性"通常靠数据库自身机制保证:如 MySQL 用全局读锁或基于 MVCC 的一致性快照(FLUSH TABLES WITH READ LOCK / START TRANSACTION WITH CONSISTENT SNAPSHOT),Redis 用 RDB 的 fork 子进程写快照。快照必须记录"对应的日志位点",追赶才知道从哪条日志开始回放。
【L4】这套"快照 + 日志追赶"的模式不止用于副本初始化,也是分区再均衡迁移、Raft 日志压缩(snapshot + log compaction)的通用骨架,理解了它可以一通百通。
⚠️ 常见误区
详情
常见误区:
- ❌ "锁库拷贝最简单,维护窗口做就行" → 锁库违反高可用设计目标,且大库锁定时间长,风险不可控,只在极小规模或离线场景可接受。
- ❌ "快照拷完数据就一致了" → 快照只是某时刻的切面,快照生成后到拷贝完成之间的变更必须靠日志追赶补齐。
🔀 发散问题
从节点故障恢复后如何追赶?
和本地保存的数据变更日志偏移量对比,确认滞后量后向主节点请求日志补全,同样是"追赶"过程,详见本文档『如何通过主从复制技术来实现系统高可用呢?』。
【困难】如何通过主从复制技术来实现系统高可用呢?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 高可用与故障转移
💎 关键结论
主从复制实现高可用 = 数据冗余 + 心跳监控 + 自动选主切换 + 客户端重定向,四件套缺一不可。光有副本没有自动切换,主挂了还是要人工救火,谈不上高可用。
⚡记忆卡片
- 口诀:冗余、检测、切主、重定向
- 关键词:一主多从 / 心跳超时 / 选主 / 切换 / 脑裂
- 链路:多副本冗余 → 心跳检测故障 → 多数派选新主 → 客户端路由切换 → 原主恢复降级防脑裂
📖 核心知识
主从复制实现高可用 = 数据冗余(一主多从) + 心跳监控 + 自动选主切换 + 客户端重定向
- 数据冗余:
- 一个主节点(写+读),多个从节点(读+备份)。
- 数据从主实时/近实时复制到从。
- 从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点从故障中恢复,可以和主节点对比数据变更日志的偏移量,从而确认数据是否滞后。如果数据存在滞后,则向主节点请求数据变更日志,并补全数据。这个过程称为追赶。
- 故障检测:
- 监控服务(如 Prometheus、ZooKeeper)持续检查主节点健康。
- 常用方法:心跳续活、超时判断。
- 自动切换:
- 主节点故障时,选主算法(如 Raft、Paxos)自动从从节点中选举新主。
- 配置更新:客户端/中间件(如 Proxy)自动将写请求路由到新主。
- 主节点失效后,需要选举出新主节点。然后,客户端需要更新路由,将所有写请求发送给新的主节点;其他从节点要接受来自新的主节点上的变更数据。这个过程称之为切换。
- 主节点切换可以手动或自动进行。自动切换的步骤通常如下:
- 确认主节点失效。有很多种出错可能性,很难准确检测出问题的原因。所以,大多数系统都基于超时机制来确认主节点是否失效:节点间频繁地互相发送心跳存活消息,如果发现某一个节点在一段比较长时间内没有响应,即认为该节点发生失效。
- 选举新的主节点。基于多数派共识选主。候选节点最好与原主节点的数据差异最小,这样可以最小化数据丢失的风险。
- 重新配置系统使新主节点生效。客户端现在需要将写请求发送给新的主节点。原主节点若恢复,需降级处理,避免脑裂。
- 数据一致性:
- 切换后,确保新主拥有最新数据(避免脑裂)。
- 常用方法:半同步复制(确保至少一个从有最新数据)。
🔬 扩展知识
详情
【L3】故障转移的三大风险点:① 超时判活的误判(网络抖动导致假死切换);② 新主缺数据(异步复制滞后窗口内的写入丢失);③ 脑裂(旧主恢复后两个主同时接受写)。应对分别是:调优超时与探测多路径、半同步保底、旧主恢复时强制降级为从(fencing)。
【L4】"选数据差异最小的节点为新主"和共识算法的选举限制本质相同:Raft 靠"日志不够新拿不到多数票"实现同样效果。可以说成熟的故障转移 = 复制 + 简化版共识。
⚠️ 常见误区
详情
常见误区:
- ❌ "有了主从复制就自动高可用" → 复制只解决数据冗余,还需要故障检测、自动选主、客户端路由切换配套,否则主挂了写入照样中断。
- ❌ "原主节点恢复后让它继续当主就行" → 原主恢复必须先降级为从,否则可能出现双主写入的脑裂。
- ❌ "新主随便选一个从节点就行" → 应优先选数据最新(与主差异最小)的从节点,最小化数据丢失风险。
🔀 发散问题
为什么超时机制是故障检测的主流做法?
分布式环境下无法准确区分"慢"和"死",只能用超时启发式判断,代价是误判,这与 FLP 不可能定理的讨论一脉相承,可参考《分布式理论面试》『什么是 FLP 不可能定理?它对分布式系统有什么影响?』。
切换时如何避免丢数据?
依赖半同步复制保证至少一个从节点持有最新数据,详见本文档『同步复制、半同步复制、异步复制有什么差异?』。
【困难】复制日志的工作原理是什么?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 复制日志
💎 关键结论
复制日志四种实现:语句、WAL、行级逻辑日志、触发器。现代数据库主流选"行级逻辑日志"——与存储引擎解耦、跨版本兼容;追求性能的同构场景则直接传 WAL。
⚡记忆卡片
- 口诀:语句太脆、WAL 太低层、逻辑日志是主流、触发器靠边站
- 关键词:语句复制 / WAL / 逻辑日志 / 触发器 / 存储引擎解耦
- 链路:写操作产生变更 → 以某种日志格式记录 → 传输到从节点 → 从节点按格式重放
📖 核心知识
复制日志的实现方式:
- 基于语句的复制:将数据写操作写入日志。主要缺点是必须完全按照相同顺序执行,否则可能会产生不同的结果。
- 基于预写日志(WAL)传输:通常每个写操作都是以追加写的方式写入到日志中。主要缺点是日志描述的数据结果非常底层,如果数据库不同版本的存储格式存在差异,就可能无法兼容。
- 对于日志结构存储引擎,日志是主要的存储方式。日志段在后台压缩并支持垃圾回收。
- 对于采用覆写磁盘的 BTree 结构,每次修改会预先写入日志,如系统发生崩溃,通过索引更新的方式迅速恢复到此前一致状态。
- 基于行的逻辑日志复制:如果复制和存储引擎采用不同的日志格式,这样复制与存储的逻辑就可以剥离。这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。
- 基于触发器的复制:这种方式很灵活,可以定制化控制复制逻辑。主要缺点是复制开销更高,也更容易出错。
对比与选择:
- 追求性能与可靠 → 基于预写日志的复制(适用于同构、同版本)。
- 追求灵活与兼容 → 基于行的逻辑日志复制(现代数据库主流选择)。
- 特殊定制需求 → 基于触发器的复制(需谨慎评估性能)。
- 遗留或简单场景 → 基于语句的复制(不推荐用于强一致性要求)。
🔬 扩展知识
详情
【L3】基于语句复制的经典陷阱:NOW()、RAND()、自增列等非确定性语句在从节点重放会得到不同结果,这也是 MySQL 早期 statement 格式 binlog 被 row 格式逐步取代的核心原因(MySQL 5.7 起 binlog 默认格式为 ROW)。
【L4】逻辑日志的价值在于"解耦存储与复制":它让异构复制(如 MySQL → ES 的 CDC 同步)成为可能,Debezium、Canal 这类 CDC 工具本质上都是解析行级逻辑日志。
⚠️ 常见误区
详情
常见误区:
- ❌ "直接传 WAL 最好,所有场景都用它" → WAL 非常底层,数据库版本间存储格式差异会导致不兼容,只适合同构同版本场景。
- ❌ "基于语句的复制日志最小,应该首选" → 语句必须严格按序执行且对非确定性函数不友好,强一致场景不推荐。
- ❌ "触发器复制又灵活又轻量" → 触发器复制开销更高且更容易出错,只适合特殊定制需求。
🔀 发散问题
无主复制里没有统一的日志流,靠什么收敛数据?
靠 Quorum 读写 + 版本号 + 读修复/反熵,详见本文档『无主复制的工作原理是什么?』。
【困难】多主复制的工作原理是什么?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 多主复制
💎 关键结论
多主复制 = 每个数据中心各设一个主、内部走常规主从、主与主之间互相同步。它只在多数据中心、离线客户端、协作编辑这三类场景有意义,单数据中心用它纯属自找麻烦。
⚡记忆卡片
- 口诀:每数据中心一个主,主主互相同步
- 关键词:多主复制 / 多数据中心 / 异步跨中心复制 / 就近写入
- 链路:写请求在本数据中心的主节点快速响应 → 异步同步到其他数据中心主节点 → 屏蔽广域网延迟 → 单中心故障不影响其他中心
📖 核心知识
对主从复制模型进行自然的扩展,则可以配置多个主节点,每个主节点都可以接受写操作,后面复制的流程类似:处理写的每个主节点都必须将该数据更改转发到所有其他节点。这就是多主节点(也称为主-主,或主动/主动)复制。此时,每个主节点还同时扮演其他主节点的从节点。
在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。但是,以下场景这种配置则是合理的:
- 多数据中心
- 离线客户端操作
- 协作编辑
有了多主节点复制模型,则可以在每个数据中心都配置主节点。在每个数据中心内,采用常规的主从复制方案;而在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。

部署单主节点的主从复制方案与多主复制方案之间的差异:
- 性能:对于主从复制,每个写请求都必须经由广域网传送至主节点所在的数据中心。这会大大增加写入延迟,并基本偏离了采用多数据中心的初衷(即就近访问)。而在多主节点模型中,每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。因此,对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。
- 容忍数据中心失效:对于主从复制,如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。在多主节点模型中,每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。
- 容忍网络问题:数据中心之间的通信通常经由广域网,它往往不如数据中心内的本地网络可靠。对于主从复制模型,由于写请求是同步操作,对数据中心之间的网络性能和稳定性等更加依赖。多主节点模型则通常采用异步复制,可以更好地容忍此类问题,例如临时网络闪断不会妨碍写请求最终成功。
🔬 扩展知识
详情
【L3】多主复制的代价是并发写冲突:两个数据中心的客户端同时修改同一条记录时,异步复制无法避免冲突,需要 LWW、版本向量或应用层合并策略(协作编辑常用 CRDT 自动合并),冲突处理是多主模型最大的工程负担。
【L4】"离线客户端操作"本质是把每个客户端当成一个"主":笔记应用离线编辑、邮件客户端发件箱都是这个模型,同步时同样要面对冲突合并问题。
⚠️ 常见误区
详情
常见误区:
- ❌ "多主复制就是多个主节点分担写压力,任何系统都适用" → 单数据中心内多主的复杂度远超收益,它主要服务多数据中心、离线操作、协作编辑场景。
- ❌ "多主之间同步和主从一样,不会有冲突" → 多个主并发接受写,异步同步时必然可能出现并发写冲突,必须有冲突解决机制。
🔀 发散问题
并发写冲突具体怎么解决?
LWW、版本向量、读修复、反熵等,详见本文档『无主复制中如何解决并发写冲突?』。
多数据中心为什么不直接上共识算法?
跨数据中心延迟高,多数派共识性能不可接受,共识的局限详见《分布式理论面试》『共识算法的局限性有哪些?』。
【困难】无主复制的工作原理是什么?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 无主复制
💎 关键结论
无主复制没有写入口限制:写时并行写 N 个副本(W 成功即返回),读时并行读 R 个副本用版本号挑最新,旧数据靠读修复和反熵兜底。只要 W+R>N,读写必有重叠,就能读到最新值。
⚡记忆卡片
- 口诀:写 W 读 R,W+R>N 必见最新值
- 关键词:并行写 / 并行读 / Quorum NWR / 版本号 / 读修复 / 反熵
- 链路:客户端并行写多副本 → 并行读多副本 → 版本号识别最新 → 读修复/反熵修复旧副本
📖 核心知识
无主复制模式,系统中不存在主节点,每一个节点都能接受客户端的写请求。此外,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。
- 无主复制流程:
- 写入时:客户端并行将数据写入 N 个副本节点(W 个成功即返回)。无固定主节点,所有节点平等。
- 读取时:客户端并行从 N 个副本节点读取数据(读取 R 个响应)。通过版本号(向量时钟、时间戳)识别最新数据。
- 修复与同步:
- 读修复:读取时若发现旧副本,立即用新数据修复它。
- 反熵:后台进程持续同步副本,弥合差异。
- Quorum NWR 算法:无主复制模式中,究竟多少个副本完成才可以认为写成功?如果有 N 个副本,写入需要 W 个节点确认,读取必须至少查询 R 个节点,则只要
W + R > N,读取的节点中一定会包含最新值。即:确保读取集(R)和写入集(W)必有重叠,从而一定能读到最新写入。 - 并发写冲突:无主模式中,并发向多副本写操作,以及读时修复或数据回传都会导致并发写冲突。解决机制包括:最后写入获胜(LWW)(简单但可能丢失并发写入)、版本向量(跟踪因果历史,识别并发冲突,交应用层解决)。
🔬 扩展知识
详情
【L3】W + R > N 只是"能读到最新值"的必要条件而非充分保证:如果写操作部分失败(W 个确认里有的节点后续丢失数据)、或修复机制失效,仍可能读到旧值。Dynamo 风格系统用"读到的多版本交应用层合并"来兜底这种边缘情况。
【L4】无主复制与 Quorum 的关系可以推广:ZooKeeper/Raft 的多数派写也是 Quorum 思想(W = 多数派),区别是无主复制把"谁是权威"交给版本号,共识系统把"谁是权威"交给 Leader。
⚠️ 常见误区
详情
常见误区:
- ❌ "W+R>N 就一定强一致" → 它只保证读写集合有重叠从而能读到最新值,写入部分失败、节点修复异常时仍需版本号与修复机制兜底,不等于线性一致。
- ❌ "无主复制不需要任何协调,读一个节点就行" → 读取必须并行查询 R 个节点并用版本号比较,只读单节点可能读到过期数据。
🔀 发散问题
并发写冲突的完整解决机制有哪些?
LWW、版本向量、读修复、反熵四种,详见本文档『无主复制中如何解决并发写冲突?』。
无主复制的 Quorum 和共识算法的多数派是一回事吗?
思想同源(集合重叠原理),但共识有 Leader 定序,无主靠版本号定序,详见《分布式理论面试》『什么是 Quorum 机制?为什么多数派能保证一致性?』。
【中等】什么是复制延迟问题?有哪些常见的解决思路?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 复制延迟
💎 关键结论
复制延迟就是异步复制下从节点落后主节点,读从会读到旧数据。对症下药三招:读己之写(自己写的读主)、单调读(固定读一个从)、前缀一致性(按因果序读),实在不行关键路径强制读主。
⚡记忆卡片
- 口诀:己写己读主、单调读定从、因果保顺序
- 关键词:复制延迟 / 读己之写 / 单调读 / 前缀一致性 / 强制读主
- 链路:异步复制滞后 → 读从读到旧数据 → 按场景选择一致性策略 → 牺牲部分读扩展性换一致性
📖 核心知识
复制延迟问题是指:在异步(或半同步)主从复制中,从节点的数据滞后于主节点,导致从从节点读取到过期数据。复制延迟的原因包括:网络延迟、从节点负载高、大事务、批量写入等。
复制延迟引发的典型问题:
- 读写不一致:用户写入主节点后,立即从从节点读取,读到的还是旧数据(Read-After-Write 问题)。
- 跨会话不一致:用户 A 发帖,用户 B 立即查看,由于读到的是未同步的从节点,看不到 A 的帖子。
- 因果关系破坏:先回复评论,再查看帖子,发现回复在帖子之前出现(时间倒错)。
常见的解决思路:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 读己之写一致性 | 用户写主后,读主(或读从 + 超时回退读主) | 用户修改个人资料后立即查看 |
| 单调读一致性 | 同一用户始终读同一从节点(基于用户 ID 哈希路由) | 防止"数据倒退"(读到新再读旧) |
| 读前缀一致性 | 按因果顺序读取(如版本号、时间戳) | 聊天、评论等有因果关系的场景 |
| 同步/半同步复制 | 等待从节点同步后再返回写成功 | 对一致性要求高的场景 |
| 读写分离 + 强制读主 | 关键路径强制从主节点读取 | 写后立即读的场景 |
Read-After-Write(读己之写)一致性实现策略:
- 读主策略:用户写后的某段时间内(如 1 分钟),该用户的所有读请求都路由到主节点。
- 时间戳策略:从节点记录最后同步的主节点时间戳。读请求携带客户端的最后写时间戳,如果从节点同步时间 < 该时间戳,则拒绝读取(或等待)。
- 会话粘连:同一会话内的读写都路由到主节点(牺牲读扩展性)。
🔬 扩展知识
详情
【L3】这些一致性模型是"以用户为中心的一致性"(client-centric consistency),不承诺全局视图,只承诺单个用户观察到的顺序合理——这比全局强一致便宜得多,是互联网产品读扩展与体验之间的标准折中。
【L4】工程落地常量化延迟预算:监控主从延迟(如 MySQL Seconds_Behind_Master),当延迟超过阈值(如秒级)时自动把该从节点移出读流量池,避免把"最终一致"拖成"长期不一致"。
⚠️ 常见误区
详情
常见误区:
- ❌ "读写分离后用户立刻能看到自己刚写的数据" → 异步复制存在延迟窗口,写后立即读从大概率读旧,必须走读己之写策略。
- ❌ "单调读就是让读请求都走主" → 单调读只是让同一用户固定读同一个从节点,避免先读新后读旧的"数据倒退",仍保留读扩展能力。
🔀 发散问题
为什么半同步复制不能完全消除这类问题?
半同步只保证至少一个从有最新数据,其余异步从仍可能滞后,读流量若打到异步从上问题依旧,详见本文档『同步复制、半同步复制、异步复制有什么差异?』。
【困难】无主复制中如何解决并发写冲突?⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 写冲突解决
💎 关键结论
无主复制的并发写冲突没有银弹:图简单用 LWW(靠时间戳取胜者,但会丢并发写),图正确用版本向量识别冲突交应用层合并,再用读修复和反熵保证副本最终收敛。
⚡记忆卡片
- 口诀:LWW 快丢、版本向量准、读修复顺手、反熵兜底
- 关键词:LWW / 版本向量 / 读修复 / 反熵 / 应用层合并
- 链路:并发写落不同副本 → 时间戳/版本号比较 → 胜出版本传播 → 读修复与反熵收敛旧副本
📖 核心知识
无主复制模式下,多个客户端并发写入同一 key 到不同副本,会产生冲突。常见的冲突解决机制:
- 最后写入获胜(LWW, Last Write Wins):
- 每个写入附带时间戳,保留时间戳最大的版本,丢弃其他版本。
- 优点:简单,易于实现。
- 缺点:可能丢失并发写入;依赖时钟同步,时钟偏移会导致错误丢弃。
- 适用:缓存等对数据丢失不敏感的场景。
- 版本号 / 向量时钟:
- 每个副本维护一个版本号(或向量时钟),记录数据的因果历史。
- 读取时比较版本号,识别并发写入,交由应用层解决冲突。
- 优点:不丢失数据,能识别并发。
- 缺点:应用层需处理冲突,复杂度高。
- 适用:Riak、Voldemort 等 Dynamo 风格数据库。
- 读修复(Read Repair):
- 读取时发现多个副本版本不一致,将最新版本写回旧副本。
- 优点:自愈机制,无需额外后台进程。
- 缺点:只在读取时修复,未被读取的数据可能长期不一致。
- 反熵(Anti-Entropy):
- 后台进程持续比较副本间数据差异,修复不一致。
- 优点:最终一致性保证。
- 缺点:延迟较高,占用后台资源。
🔬 扩展知识
详情
【L3】LWW 的时钟依赖是致命弱点:分布式系统时钟只能做到大致同步(NTP 毫秒级误差),并发写恰好落在误差窗口内时,"胜者"是随机的。因此 LWW 只适合缓存这类丢写无害的场景,Dynamo 系论文也明确以向量时钟替代。
【L4】读修复与反熵是互补而非二选一:读修复响应快但依赖数据被访问(冷数据修不到),反熵覆盖全量但延迟高,成熟系统(如 Cassandra)两者同时启用。
⚠️ 常见误区
详情
常见误区:
- ❌ "LWW 能保证并发写入都不丢" → LWW 恰恰会丢弃时间戳较小的并发写入,它牺牲正确性换简单。
- ❌ "版本向量能自动合并冲突" → 版本向量只能识别出"这些写入是并发的",合并不需要业务语义,必须由应用层完成。
- ❌ "有读修复就不需要反熵" → 读修复只覆盖被读到的数据,长期不被访问的数据靠反熵后台修复。
🔀 发散问题
向量时钟和逻辑时钟是什么关系?
向量时钟是逻辑时钟的一种,用于刻画因果顺序,与 Lamport 时钟、版本向量的差异可延伸阅读《数据密集型应用系统设计》第 5 章(见文末参考资料)。
冲突解决和一致性模型有什么联系?
冲突解决是手段,最终一致性是目标,二者关系可参考《分布式理论面试》。
分区
【简单】什么是分区?为什么要分区?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:分布式协同 / 分区基础
💎 关键结论
分区就是分而治之:把大数据集水平切成多个子集分散到不同节点。目的三个:突破单机容量与吞吐极限、加节点就能线性扩展、按地域就近部署降延迟。
⚡记忆卡片
- 口诀:切小、分散、就近
- 关键词:水平切分 / 突破单机极限 / 线性扩展 / 地理局部性
- 链路:数据集过大 → 水平切分到多节点 → 容量与吞吐随节点数线性增长 → 就近部署降低延迟
📖 核心知识
分区(Partitioning):将大数据集水平切分成多个独立子集,分散到不同节点存储与管理。
分区的核心思想是:分而治之。
分区的目的:
- 突破单机极限:数据量、吞吐量。
- 提升扩展性:数据与负载线性扩展:加节点 → 加容量与性能。
- 实现局部性优化:将数据就近部署到用户所在区域(地理分区),降低访问延迟。
【中等】分区有哪些模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 分区模式
💎 关键结论
分区两大模式:范围分区保序、区间查询快但有热点风险(HBase 代表);哈希分区打散均匀、无热点但区间查询差(ES、Redis 代表)。分区通常还会和复制组合,每个分区多节点存副本。
⚡记忆卡片
- 口诀:范围保序怕热点,哈希均匀慢区间
- 关键词:范围分区 / 哈希分区 / 区间查询 / 热点 / 分区副本
- 链路:按关键字排序或哈希 → 数据映射到分区 → 范围分区支持区间查询 → 哈希分区均衡负载 → 每分区再配副本容错
📖 核心知识
分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区,而同样的内容会保存在不同的节点上以提高系统的容错性。
一个节点上可能存储了多个分区。每个分区都有自己的主副本,例如被分配给某节点,而从副本则分配在其他一些节点。一个节点可能既是某些分区的主副本,同时又是其他分区的从副本。
分区主要有两种模式:
- 范围分区:先对关键字进行排序,每个分区只负责一段包含最小到最大关键字范围的一段关键字。对关键字排序的优点是可以支持高效的区间查询,但是如果应用程序经常访问与排序一致的某段关键字,就会存在热点的风险。采用这种方式,当分区太大时,通常将其分裂为两个子区间,从而动态地再平衡分区。典型代表:HBase。
- 哈希分区:将哈希函数作用于每个关键字,每个分区负责一定范围的哈希值。这种方法打破了原关键字的顺序关系,它的区间查询效率比较低,但可以更均匀地分配负载。采用哈希分区时,通常事先创建好足够多(但固定数量)的分区,让每个节点承担多个分区,当添加或删除节点时将某些分区从一个节点迁移到另一个节点,也可以支持动态分区。典型代表:Elasticsearch、Redis。
🔬 扩展知识
详情
【L3】实践中的折中是"复合键":前缀用散列度高的字段(如用户 ID)做哈希定位分区,剩余部分按范围组织,兼顾定位均匀与局部区间扫描(HBase 的 rowkey 设计就是这种思路)。
【L4】范围分区的热点本质是"写入单调递增":以时间戳为主键时所有写都打到最后一个分区。应对是给 key 加随机前缀或改用哈希分区,代价是牺牲按时间区间的物理局部性。
🔀 发散问题
节点增减时分区怎么迁移?
通过分区再均衡,常见固定分区数、动态分裂、按节点比例三种策略,详见本文档『分区再均衡有哪些策略?』。
二级索引跟着数据一起分区吗?
有本地索引和全局索引两种方式,详见本文档『二级索引如何分区?』。
【困难】二级索引如何分区?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 二级索引
💎 关键结论
二级索引分区两条路:本地索引(索引跟数据同分区,写快读要扫全部分区)和全局索引(按词条独立分区,读快写要更新多分区)。本质是写放大和读放大二选一。
⚡记忆卡片
- 口诀:本地写快读散,全局读快写散
- 关键词:本地索引 / 全局索引 / 文档分区 / 词条分区 / 写放大读放大
- 链路:索引与数据的分区对齐方式 → 决定写更新范围或读扫描范围 → 按读写比选型
📖 核心知识
二级索引是关系数据库的必备特性,在文档数据库中应用也非常普遍。但考虑到其复杂性,许多键值存储(如 HBase 和 Voldemort)并不支持二级索引。此外,二级索引技术也是 Solr 和 Elasticsearch 等搜索引擎数据库存在之根本。
分区不仅仅是针对数据,二级索引也需要分区。通常有两种方法:
- 基于文档来分区二级索引(本地索引):二级索引存储在与关键字相同的分区中,这意味着写入时我们只需要更新一个分区,但缺点是读取二级索引时需要在所有分区上并行执行。它广泛用于实践:MongoDB、Riak、Cassandra、Elasticsearch、SolrCloud 和 VoltDB 都支持基于文档分区二级索引。

- 基于词条来分区二级索引(全局索引):它是基于索引的值而进行的独立分区。二级索引中的条目可能包含来自关键字的多个分区里的记录。在写入时,不得不更新二级索引的多个分区;但读取时,则可以从单个分区直接快速提取数据。

🔬 扩展知识
详情
【L3】全局索引的写更新通常采用"异步更新"来缓解写放大:主数据先落盘,索引更新走后台异步流(如 Elasticsearch 的近实时 refresh),代价是索引查询存在短暂延迟窗口(这也是 ES"近实时"而非实时的原因之一)。
【L4】选型经验:写多读少(如日志、监控数据)选本地索引;读多写少且索引查询是核心路径(如商品搜索)选全局索引。搜索引擎普遍采用按词条分区的倒排索引并配合副本提升读吞吐。
⚠️ 常见误区
详情
常见误区:
- ❌ "全局索引读写都更快" → 全局索引只是读快,写入时要更新多个索引分区,写放大明显,写密集场景会成为瓶颈。
- ❌ "本地索引没有缺点" → 本地索引的每次索引查询都要扇出到所有分区(scatter-gather),查询延迟受最慢分区制约。
🔀 发散问题
为什么 HBase 这类键值存储干脆不支持二级索引?
二级索引的跨分区维护成本与键值存储的极简定位冲突,需要时通常靠上层(如 Phoenix、ES 同步)实现。
索引更新的一致性窗口怎么处理?
依赖近实时刷新 + 业务容忍秒级延迟,或关键路径回源主键校验,这与复制延迟的治理思路相通,详见本文档『什么是复制延迟问题?有哪些常见的解决思路?』。
【简单】什么是分区再均衡?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:分布式协同 / 分区再均衡
💎 关键结论
分区再均衡就是集群节点变化后自动重新分配分区,让负载重新均匀。三个触发时机:扩容缩容、数据倾斜出热点、节点故障被移除。
⚡记忆卡片
- 口诀:伸缩、倾斜、故障,都要再均衡
- 关键词:再均衡 / 扩容缩容 / 数据倾斜 / 节点故障
- 链路:节点数量或负载变化 → 分区分布失衡 → 触发再均衡 → 分区在节点间迁移 → 负载恢复均匀
📖 核心知识
分区再均衡:当集群节点数量发生变化时,自动重新分区,使得集群分布均匀的过程。
触发时机:
- 集群伸缩:增加节点(扩容)或减少节点(缩容)。
- 数据倾斜:某个节点负载过高(热点分区)。
- 节点故障:故障节点被移除。
【中等】分区再均衡有哪些策略?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 再均衡策略
💎 关键结论
再均衡三大策略:固定分区数(预建远多于节点的分区,增删节点只转移分区,Riak/ES 常用)、动态分区(分区按数据量自动分裂合并,HBase 常用)、按节点比例分区(每节点固定分区数,Cassandra 常用)。核心都是让迁移量最小、过程可渐进。
⚡记忆卡片
- 口诀:固定转移、动态分裂、按节点平分
- 关键词:固定分区 / 动态分区 / 按节点比例 / 分区迁移 / 预分裂
- 链路:节点数变化 → 分区与节点映射重排 → 分区渐进迁移 → 旧分区继续服务 → 路由切换完成再均衡
📖 核心知识
- 固定数量的分区:
- 核心做法:集群初始化时创建大量固定分区(如 1000 个),远超当前节点数;增删节点时只转移部分分区,不修改分区键范围。
- 关键特点:
- 分区总数不变,只调整"分区 → 节点"的映射关系
- 迁移可渐进完成,旧分区在迁移期间继续服务
- 可按节点性能分配不同数量的分区(高性能节点分更多)
- 初始化注意:需预估未来最大节点数,设置足够大的分区数;分区数过高会带来管理开销。
- 代表系统:Redis、Elasticsearch、Couchbase、Voldemort

- 动态分区:
- 核心做法:分区按数据量自动分裂与合并——超过阈值(如 HBase 默认 10GB)则分裂为两个,低于阈值则与相邻分区合并。
- 关键特点:
- 分区数量自动适配数据总量,无需预先设定
- 分裂后可将一半转移到其他节点实现负载均衡
- 空库时所有写入集中在单个分区节点 → 可通过预分裂缓解
- 代表系统:HBase、RethinkDB、MongoDB(支持区间分区 + 哈希分区两种动态分裂)
- 按节点比例分区:
- 核心做法:分区总数与节点数成正比,每个节点持有固定数量的分区(如 Cassandra 默认每节点 256 个)。
- 关键特点:
- 节点数不变时,分区大小随数据量正比增长;节点增加时,分区自动变小,保持每个分区大小稳定
- 新节点加入时,随机选择现有分区进行分裂,拿走一半数据
- 基于哈希分区实现,最符合一致性哈希定义
- 代表系统:Cassandra、Ketama
🔬 扩展知识
详情
【L3】三种策略的元数据成本对比:固定分区需要维护"分区→节点"映射表;动态分区的分裂/合并事件会频繁改变分区边界,依赖 ZooKeeper 类服务跟踪;按节点比例分区可用一致性哈希隐式计算,元数据开销最低。
【L4】再均衡的触发要避免"振荡":负载刚超阈值就迁移,迁移过程本身又造成负载波动,可能引发反复再均衡。成熟系统会设置触发阈值 + 冷却时间(hysteresis),并优先迁移负载差异最大的分区。
⚠️ 常见误区
详情
常见误区:
- ❌ "固定分区数的分区数随便设,以后再改" → 分区数建库时确定后通常不再改变,必须预估未来最大节点数;设过小限制扩容,设过大增加管理开销。
- ❌ "动态分区从空库启动就能自动扩" → 空库只有一个分区,达到分裂点前所有写入集中在单节点,需要预分裂缓解冷启动。
- ❌ "再均衡期间服务必须停机" → 主流设计都支持渐进迁移,旧分区在迁移期间继续服务,详见本文档『分区再均衡过程中如何保证服务不中断?』。
🔀 发散问题
再均衡期间怎么保证读写不受影响?
滚动迁移、双写、追赶机制、限流四招,详见本文档『分区再均衡过程中如何保证服务不中断?』。
客户端怎么知道数据在哪个分区?
客户端路由、代理路由、服务端重定向三种,详见本文档『如何确定读写请求发往哪个节点?』。
【困难】如何确定读写请求发往哪个节点?⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 请求路由
💎 关键结论
请求找节点本质是服务发现问题,三种做法:客户端内置路由表直连(快但客户端重)、代理层转发(客户端无感知但多一跳)、服务端重定向(先任意节点再跳板)。元数据来源要么 ZooKeeper 类协调服务,要么节点间 gossip。
⚡记忆卡片
- 口诀:客户端直连、代理转发、重定向跳板
- 关键词:客户端路由 / 代理路由 / 服务端重定向 / ZooKeeper / gossip
- 链路:客户端发起请求 → 获取分区到节点映射 → 直连或经代理或重定向 → 到达目标分区节点
📖 核心知识
当数据集分布到多个节点上,需要解决一个问题:当客户端发起请求时,如何知道应该连接哪个节点?如果发生了分区再均衡,分区与节点的对应关系随之还会变化。
这其实属于一类典型的服务发现问题,任何通过网络访问的系统都有这样的需求,尤其是当服务目标支持高可用时(在多台机器上有冗余配置)。
服务发现有以下处理策略:
- 客户端路由:
- 客户端内置/依赖 SDK,知晓分区与节点映射(元数据)。
- 直接连接目标节点,无中间跳转。
- 优点:低延迟,架构简单。
- 缺点:客户端需感知拓扑变化。
- 代表:Cassandra、Redis Cluster。
- 代理路由:
- 请求先发往独立的代理/中间件(如 ProxySQL、MyCat)。
- 代理根据路由规则转发到正确节点。
- 优点:客户端无感知,集中管理。
- 缺点:增加一跳延迟,代理可能成瓶颈。
- 代表:数据库中间件、服务网格 Sidecar。
- 服务端重定向:
- 客户端先发请求到任意节点,若数据不在该节点,则收到重定向响应(含正确节点地址)。
- 客户端再向正确节点发送请求。
- 优点:客户端无初始元数据。
- 缺点:增加一次往返。
- 代表:MongoDB 分片集群(
mongos路由)、DynamoDB(SDK 缓存路由表)。

许多分布式数据系统依靠独立的协调服务(如 ZooKeeper)跟踪集群范围内的元数据。每个节点都向 ZooKeeper 中注册自己,ZooKeeper 维护了分区到节点的最终映射关系。其他参与者(如路由层或分区感知的客户端)可以向 ZooKeeper 订阅此信息。一旦分区发生了改变,或者添加、删除节点,ZooKeeper 就会主动通知路由层,这样使路由信息保持最新状态。
例如,HBase、SolrCloud 和 Kafka 也使用 ZooKeeper 来跟踪分区分配情况。MongoDB 有类似的设计,但它依赖于自己的配置服务器和 mongos 守护进程来充当路由层。

Cassandra 和 Redis 则采用了不同的方法,它们在节点之间使用 gossip 协议来同步群集状态的变化。请求可以发送到任何节点,由该节点负责将其转发到目标分区节点。这种方式增加了数据库节点的复杂性,但是避免了对 ZooKeeper 之类的外部协调服务的依赖。
🔬 扩展知识
详情
【L3】元数据一致性与可用性的权衡在这里再次出现:依赖 ZooKeeper 意味着协调服务不可用时路由元数据无法更新(但已缓存的旧路由仍可读);依赖 gossip 则允许元数据短暂不一致,靠重试/重定向收敛。
【L4】客户端路由的隐藏成本在多语言生态:每种语言都要维护一份"聪明客户端"SDK 并保持版本同步,这是 Cassandra、Redis Cluster 客户端碎片化的根源;代理路由把复杂度收拢到一处,是异构客户端场景的常见选择。
⚠️ 常见误区
详情
常见误区:
- ❌ "客户端路由一定最优" → 客户端直连延迟最低,但所有语言的 SDK 都要实现路由与拓扑感知逻辑,维护成本高,且拓扑变化时客户端要能刷新缓存。
- ❌ "路由表是静态配置" → 再均衡、节点增删都会改变分区到节点的映射,路由信息必须能被订阅刷新或按需重定向更新。
🔀 发散问题
gossip 协议是怎么同步集群状态的?
节点间随机互相同步元数据、指数级扩散收敛,可与本文档的分布式理论系列中 Gossip 相关内容对照理解。
再均衡时路由信息过期会怎样?
请求会收到重定向或转发到正确节点,旧分区在迁移完成前继续服务,详见本文档『分区再均衡过程中如何保证服务不中断?』。
【中等】如何解决分区热点问题?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 分区热点
💎 关键结论
热点就是请求扎堆打在一个分区上,把所在节点压垮。解法分两类:把 key 打散(哈希分片、随机前缀、虚拟槽),把请求拦下(热点缓存、客户端缓存、读写分离)。
⚡记忆卡片
- 口诀:打散 key、拦截请求
- 关键词:哈希分片 / 随机前缀 / 虚拟槽 / 热点缓存 / 客户端缓存
- 链路:请求集中于单分区 → 识别热点 key → 打散到多分区或前置缓存 → 单节点负载回落到集群均摊
📖 核心知识
分区热点(Hotspot) 是指:大量请求集中访问某个分区,导致该分区所在节点负载过高,而其他节点空闲。热点问题会削弱分区的负载均衡效果。
热点产生的原因:
- 数据倾斜:某些 key 的访问频率远高于其他 key(如热门商品、明星用户)。
- 单调递增的 key:基于时间戳或自增 ID 的 key,写入总是落在最后一个分区。
- 范围查询集中:基于范围分区时,某段范围的查询特别频繁。
解决方案:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 哈希分片 | 对 key 做哈希,打散到不同分区 | 无法预知热点的场景 |
| key 添加随机前缀 | 在热点 key 前加随机数(如 商品ID_0~9),分散到多分区 | 热点 key 可识别的场景 |
| 一致性哈希 | 节点增减时只影响相邻分区,减少数据迁移 | 节点频繁增减的场景 |
| 虚拟槽 | Redis Cluster 的 16384 个虚拟槽,节点负责多个槽 | 中大规模缓存集群 |
| 热点缓存 | 热点数据多副本缓存,读取时随机选择副本 | 读多写少的热点数据 |
| 客户端缓存 | 热点数据在客户端本地缓存,减少对服务端的请求 | 读多写少且容忍短暂不一致 |
🔬 扩展知识
详情
【L3】Redis Cluster 的热点 key 处理:
- 客户端本地缓存:如 Guava Cache、Caffeine,减少对 Redis 的访问。
- key 分片:将热点 key 拆分为多个子 key(如
hotkey_0~hotkey_9),分散到不同节点,读取时随机选一个。 - 读写分离:热点的读请求分散到从节点。
- proxy 层拦截:如 Twemproxy、Codis 在代理层做热点识别和限流。
【L4】热点的前提是"能被识别":生产上通常靠代理层统计 key 访问频次(如每秒 TopN)或采样探测发现热点;没有热点探测,打散方案只能盲目预分片。
⚠️ 常见误区
详情
常见误区:
- ❌ "分区越多热点越少" → 热点是流量分布问题不是分区数量问题,单个超级热 key 无论多少分区都会集中命中一个分区,必须打散 key 或前置缓存。
- ❌ "随机前缀没有代价" → 加前缀后按原 key 的聚合/区间查询需要扫所有前缀分片,读路径要一并改造。
🔀 发散问题
单调递增 key 为什么必然热点?
范围分区下递增 key 的写入永远落在最后一个分区,详见本文档『分区有哪些模式?』。
Redis Cluster 的 16384 个虚拟槽属于哪种再均衡策略?
按节点比例分区的思想:槽与节点绑定,节点增减迁移槽,详见本文档『分区再均衡有哪些策略?』。
【中等】分区再均衡过程中如何保证服务不中断?⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 再均衡可用性
💎 关键结论
再均衡不停机的核心是"先搬数据、后切路由":滚动迁移保旧分区服务、双写保增量不丢、追赶机制补差异、限流防迁移压垮在线请求。路由切换必须原子,避免客户端路由错。
⚡记忆卡片
- 口诀:滚动迁、双写保、追赶补、限流控
- 关键词:滚动迁移 / 双写 / 追赶机制 / 限流 / 路由原子更新
- 链路:全量数据迁移 → 增量日志追赶 → 新旧节点数据对齐 → 原子更新路由表 → 流量切到新节点
📖 核心知识
分区再均衡(Rebalancing)涉及大量数据在节点间迁移,如果处理不当,会导致服务中断或性能抖动。主流数据库通过以下策略保证再均衡期间的可用性:
- 滚动迁移:一次只迁移一个分区(或分区的一部分),迁移期间旧分区继续提供服务;迁移完成后,更新路由表,将流量切到新节点。类似蓝绿部署,保证任何时候都有可用的副本。
- 双写 / 影子写:再均衡期间,写入同时发往新旧两个节点;迁移完成后,停止旧节点的写入。优点:无停机;缺点:写入开销加倍。
- 追赶机制:迁移期间,记录源节点上的增量变更日志;全量数据迁移完成后,回放增量日志,使新节点追上最新状态。类似于主从复制的追赶过程。
- 限流:再均衡期间,控制迁移速度,避免占用过多网络/磁盘 IO 影响正常请求;在业务低峰期执行再均衡。
🔬 扩展知识
详情
【L3】再均衡的注意事项:
- 避免频繁再均衡:频繁的再均衡会导致数据反复迁移,影响性能。应设置合理的触发阈值(如负载差异超过 30% 才触发)。
- 预分裂:对于动态分区,空数据库启动时可以预创建一批初始分区(如 HBase 的
region预分裂),避免冷启动时所有写入集中在一个节点。 - 元数据一致性:再均衡期间,路由表的更新必须原子化,避免客户端路由到错误节点。
【L4】"全量拷贝 + 增量追赶 + 原子切换"是通用迁移骨架:MySQL 在线扩容、ES 分片迁移、缓存集群扩容本质上都是这套流程的变体,差异只在增量日志的载体(binlog / translog / 命令流)。
⚠️ 常见误区
详情
常见误区:
- ❌ "再均衡必须停写迁移" → 主流方案通过滚动迁移 + 增量追赶实现不停机,停写只在切换瞬间的极短窗口(甚至无窗口)。
- ❌ "迁移越快越好,全速跑" → 不限流的迁移会抢占在线请求的网络与磁盘 IO,引发业务延迟抖动,应限速并选择低峰期。
🔀 发散问题
新节点数据没追平前,读请求打过去怎么办?
要么旧分区继续服务读,要么读请求校验数据版本,这与复制延迟治理同源,详见本文档『什么是复制延迟问题?有哪些常见的解决思路?』。
分布式事务
【简单】什么是事务?什么是分布式事务?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:分布式协同 / 事务基础
💎 关键结论
事务是把多个读写捆成一个"要么全成、要么全败"的逻辑单元。分布式事务的难点在于没有全局权威:跨节点后任何环节宕机都会停在"结果未知"的中间态,所以它本质是个共识问题。
⚡记忆卡片
- 口诀:单机靠引擎,跨机靠共识
- 关键词:本地事务 / 分布式事务 / ACID / 结果未知 / 共识
- 链路:多操作捆绑为事务 → 跨节点后无全局权威 → 任一环节故障产生中间态 → 需要共识式手段决定提交或回滚
📖 核心知识
事务将多个读、写操作捆绑在一起成为一个逻辑操作单元。事务中的所有读写是一个执行的整体,整个事务要么成功(提交)、要么失败(中止或回滚)。
在单一数据节点中,事务仅限于对单一数据库资源的访问控制,称之为本地事务。几乎所有的成熟的关系型数据库都提供了对本地事务的原生支持。
分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。
本质:从"单机日志"到"跨节点共识"的鸿沟
单机事务之所以能实现 ACID,是因为所有状态都在一个进程 / 一台机器内:回滚靠 undo log、持久化靠 redo log、隔离靠锁和 MVCC,全部由一个权威(数据库引擎)裁决。分布式事务的难点在于没有一个全局权威:参与者分属不同服务 / 不同存储,任何一个环节宕机、网络中断,都会让事务停在"结果未知"的中间态。所以分布式事务的本质问题是:在部分失效的环境下,如何让多个独立节点对"提交还是回滚"达成一致——这实际上就是一个共识问题。
方案权衡:强一致 vs 最终一致的路线选择
| 路线 | 代表方案 | 代价 | 适用边界 |
|---|---|---|---|
| 强一致 | 2PC/XA、Seata XA | 同步阻塞、锁持有时间长、协调者单点风险;吞吐低 | 跨库资金转账、强监管报表等"必须即时一致"的场景 |
| 最终一致 | 本地消息表、事务消息、SAGA、TCC | 存在不一致窗口,需幂等 + 对账兜底;开发成本高 | 高吞吐互联网业务,绝大多数跨服务协同 |
工程经验:能用最终一致就绝不用强一致。强一致方案的可用性是所有参与者可用性的交集(任一参与者不可用则整体阻塞),在互联网规模下几乎必然成为瓶颈。
🔬 扩展知识
详情
【L3】量化参考:
- 单机 MySQL 事务平均耗时约 1
5ms;同样业务用 2PC 跨两库,耗时通常翻 310 倍(多两轮网络 + 锁持有时间变长)。 - XA 事务的锁持有时间贯穿 prepare→commit 全程,若协调者响应慢(如 500ms),热点行的锁冲突率会急剧上升。
- 业界主流互联网交易链路几乎全部采用最终一致方案,不一致窗口目标一般控制在秒级(如 P99 < 10s),用对账任务兜底到零差异。
🏭 实战场景
详情
失效场景:分布式事务的典型死法
- 协调者宕机:2PC 下参与者无限期持锁等待决议,锁住的资源拖死整个库。
- 结果未知的中间态:参与者发出 commit 后宕机,重启后不知道事务是否已提交——任何"结果未知"都必须可重查,否则只能人工介入。
- 悬挂资源:TCC 的 Try 成功但 Cancel/Confirm 因异常未送达,冻结的库存/资金永远不释放。
踩坑案例:某支付系统用 XA 跨两个库做转账。某天协调者(应用服务器)在第二阶段发完第一个 commit 后崩溃,第二个参与者未收到指令;参与者默认超时策略是"无限等待协调者",其行锁一直不释放。现象:该账户后续所有交易全部排队超时,30 分钟后堆积到数百笔。排查:DBA 发现长时间持锁会话,追溯到 XA 事务处于 PREPARED 状态。修复:① 参与者配置 xa recovery 定时轮询恢复;② 协调者改多实例 + 事务日志持久化,重启后可续处理;③ 长期方案把转账链路改成事务消息(最终一致)+ 对账。教训:XA 的"强一致"是拿"可用性 + 锁时长"换的,协调者单点和超时策略是它的两颗地雷。
场景题:电商下单链路需要"创建订单 + 扣库存 + 扣优惠券",当前用 Seata XA 实现。大促压测时发现 TPS 上不去,库存库的行锁等待飙升,DBA 建议拆掉 XA。你怎么决策?
- 应急处理:压测环境先确认瓶颈确实是 XA(观察 prepare→commit 间隔与锁持有时间);临时缩短事务边界(把非必要的 DB 操作移出事务),观察改善幅度。
- 根因分析:XA 的锁持有时间贯穿两阶段全程,大促时 prepare 后等待协调者汇总所有参与者响应的窗口被拉长,热点库存行的锁队列雪崩式堆积。强一致方案的可用性 = 最慢参与者的可用性,大促时必然先死。
- 长期方案:拆 XA,改为"订单本地事务 + 事务消息驱动库存/优惠券扣减"的最终一致方案;库存扣减用乐观更新(
UPDATE stock SET cnt = cnt - ? WHERE id = ? AND cnt >= ?)+ 幂等;优惠券支持回滚补偿;T+0 对账兜底。若业务坚持"下单时库存必须实时准确",可用"预扣 + 异步确认"折中。 - 权衡:拆 XA 后用"秒级不一致窗口 + 对账兜底"换 5~10 倍的吞吐与可用性;库存超卖风险用数据库行级条件更新 + 对账控制到零。大促场景下,可用性损失(拒绝下单)的代价远大于短暂不一致,这个权衡几乎总是值得的。
⚠️ 常见误区
详情
常见误区:
- ❌ "分布式事务 = 本地事务 + 网络调用,语义完全一样" → 跨节点后没有全局权威,"结果未知"中间态是本地事务不存在的新问题,语义与工程复杂度都不同。
- ❌ "强一致方案更正确,所以优先用" → 强一致的可用性是所有参与者可用性的交集,高吞吐场景几乎必然成为瓶颈,能用最终一致就不用强一致。
- ❌ "事务失败就是回滚,没有第三种状态" → 分布式下存在"结果未知":重试可能重复执行,放弃可能丢失已生效变更,必须靠状态持久化 + 幂等兜底。
🔀 发散问题
分布式事务和分布式共识是什么关系?
"所有参与者一致决定提交或回滚"本身就是一个共识问题。2PC 是退化的共识(协调者独裁,无容错);TCC/SAGA 则把"达成一致"拆解为"本地事务 + 可靠消息 + 幂等补偿",用工程手段绕过共识的同步阻塞代价。
为什么"结果未知"是分布式事务最危险的状态?
明确失败可以重试,明确成功可以继续;结果未知时,重试可能重复执行,放弃可能丢失已生效的变更。所以一切生产级方案都必须具备:事务状态持久化(可重查)+ 操作幂等(可重试)。
什么场景下你会坚持用强一致方案(2PC/XA)?
三个条件同时满足:① 业务不允许任何不一致窗口(如监管要求的实时对账);② 参与者数量少且同机房(控制延迟);③ 吞吐不高(可接受锁放大)。典型如银行核心跨库转账;其余场景首选最终一致。
【中等】分布式事务有哪些解决方案?各有什么利弊?⭐⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 事务方案选型
💎 关键结论
六大方案:2PC/3PC 强一致但慢且脆,TCC 资源隔离好但侵入高,本地消息表/事务消息/SAGA 走最终一致换吞吐。选型先问"能不能不做跨服务事务",再看能否预留资源、事务长短、吞吐高低。
⚡记忆卡片
- 口诀:强一致 2PC、预留用 TCC、长流程 SAGA、异步靠消息
- 关键词:2PC / 3PC / TCC / 本地消息表 / 事务消息 / SAGA / 幂等 / 对账
- 链路:判断一致性需求 → 强一致选 2PC/TCC → 最终一致选消息/SAGA → 所有方案都靠幂等 + 对账兜底
📖 核心知识
分布式事务的常见方案如下:
- 两阶段提交(2PC):将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
- 三阶段提交(3PC):与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。将二阶段的准备阶段拆分为 2 个阶段,插入了一个 preCommit 阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的"不确定状态"所产生的可能相当长的延时的问题得以解决。
- 补偿事务(TCC):
- Try:操作作为一阶段,负责资源的检查和预留。
- Confirm:操作作为二阶段提交操作,执行真正的业务。
- Cancel:是预留资源的取消。
- 本地消息表:在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
- 事务消息:基于 MQ 的分布式事务方案其实是对本地消息表的封装。
- SAGA:Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序依次调用补偿操作。
分布式事务方案对比:
- 要强一致,不怕慢 → 2PC、3PC
- 强一致,性能中等,但侵入性高:TCC
- 要高性能,可接受最终一致 → 本地消息表、MQ 事务消息、SAGA
- 长事务,跨多服务 → SAGA
- 快速集成,有现成框架 → Seata
| 2PC | 3PC | TCC | 本地消息表 | MQ 事务消息 | SAGA | |
|---|---|---|---|---|---|---|
| 数据一致性 | 强¹ | 强¹ | 最终² | 最终 | 最终 | 最终 |
| 容错性 | 低 | 中 | 高 | 高 | 高 | 高 |
| 复杂性 | 中 | 高 | 高 | 低 | 低 | 高 |
| 性能 | 低 | 低 | 中 | 中 | 高 | 中 |
| 维护成本 | 低 | 中 | 高 | 中 | 中 | 高 |
| 业务侵入 | 低 | 低 | 高 | 中 | 中 | 中 |
¹ 2PC/3PC 在协调者正常工作时保证强一致性,但协调者宕机可能导致参与者处于阻塞状态,存在数据不一致风险。
² TCC 通过 Try 阶段预留资源、Confirm/Cancel 保证最终一致,并非真正的强一致,但比消息类方案一致性更强(Try 阶段已隔离资源)。
选型决策:按四个维度判断
选型的优先级建议:先问"能不能不做"(能否通过业务设计消除跨服务事务),再问"能不能只做本地事务 + 异步补偿",最后才在 2PC/TCC/SAGA 中选:
| 判断维度 | 倾向方案 |
|---|---|
| 资源是否支持预留/冻结(库存可冻结、资金可预授权) | 能预留 → TCC;不能预留 → SAGA / 事务消息 |
| 事务时长 | 毫秒级短事务 → 2PC/TCC;长流程(跨天履约)→ SAGA |
| 吞吐要求 | 高吞吐(万级 TPS)→ 事务消息;低吞吐强一致 → 2PC/XA |
| 失败后能否"反向补偿" | 可补偿(退款、恢复库存)→ SAGA;不可补偿(已发短信、已开票)→ 只能向前恢复(重试到成功) |
🔬 扩展知识
详情
【L3】失效场景:各方案的阿喀琉斯之踵
- 2PC/3PC:协调者宕机 → 参与者阻塞持锁;第二阶段局部网络故障 → 部分提交部分回滚。
- TCC:空回滚(Cancel 先于 Try 到达)、悬挂(Cancel 后 Try 才到达)、幂等失败导致重复冻结/释放。
- 本地消息表 / 事务消息:消息积压导致不一致窗口拉长;回查接口实现错误导致半消息永远悬着;消费端无幂等导致重复执行。
- SAGA:补偿操作本身失败(补偿也需要重试到成功);无隔离性导致"更新丢失"(两个 SAGA 并发修改同一资源)。
【L3】量化参考:
- 2PC:跨两库事务端到端耗时通常 50
200ms(取决于协调者汇总速度),锁持有时间是本地事务的 310 倍。 - 事务消息:RocketMQ 回查默认间隔 6s、最多 15 次;正常链路从发送到消费可见通常毫秒~秒级。
- TCC:Try 阶段需额外一次 DB 写(冻结记录),接口开发量约为普通接口的 3 倍(三个方法),适合资金/库存等核心资源。
- SAGA:每步都是真实提交,失败时已执行步骤的"暴露时间"= 全部步骤累计耗时,长流程可能达分钟级,需评估中间态的业务影响。
【L4】所有方案都绕不开的两个工程地基:幂等和对账。幂等让重试安全(所有方案都依赖重试来兑现"最终");对账让不一致可发现可修复(所有方案都存在兜底窗口外的极端 case)。没有这两个地基,任何分布式事务方案都是玩具。
🏭 实战场景
详情
踩坑案例:某电商用 SAGA 实现下单履约(扣库存 → 创建订单 → 扣积分)。上线后某次积分服务发布时批量报错,触发大量反向补偿;但补偿动作"恢复积分"的实现是简单的 UPDATE score = score + ?(非幂等),而失败重试机制把同一条补偿执行了多次,造成部分用户积分凭空多出数倍,财务盘账才发现。修复:① 所有正向/补偿操作全部改为基于流水号的幂等实现(流水表唯一索引);② 补偿失败进人工工单队列而非无限重试;③ 增加积分流水与余额的定时对账。教训:SAGA 的每个 Ti 和 Ci 都必须幂等,否则"修复不一致的机制"本身会制造新的不一致。
场景题:公司要做"预订机票 + 预订酒店 + 扣积分"的套餐下单,各子能力分属不同团队的独立服务,其中机票供应商接口偶尔会超时 30 秒,且预订成功后 10 分钟内可免费取消。技术选型怎么做?
- 应急处理:(新项目无应急,此处置为"上线前兵棋推演":先枚举每个环节失败时的行为,确认供应商取消接口可用后再定方案。)
- 根因分析:需求特征:长流程(含 30 秒级外部调用)、每步都有天然的反向操作(取消预订、退积分)、中间态对用户可见(订单显示"预订中"可接受)。逐一对应:长事务排除 2PC(锁持有 30 秒会拖死库存/积分库);有天然补偿动作且资源无需严格隔离 → SAGA 比 TCC 更合适(TCC 要求每个供应商接口支持 Try 预留,改造成本不可控)。
- 长期方案:① 编排式 SAGA(中央协调器),每步子事务幂等,每步配幂等补偿;② 机票步骤用"向前恢复":超时后不立即取消,而是轮询查单确认状态(避免"其实成功了但被当失败取消");③ 补偿失败进人工工单;④ 全链路流水表 + T+1 对账。
- 权衡:SAGA 的代价是无隔离(两个套餐单可能并发抢同一张特价票,需要库存侧行级条件更新兜底)和中间态暴露(用户会看到"预订中"状态)。相比 TCC 的"要求所有外部供应商配合改造 Try 接口",SAGA 的代价明显更小——选型的本质是看哪个方案的假设更容易在现实中成立。
⚠️ 常见误区
详情
常见误区:
- ❌ "3PC 解决了 2PC 的所有问题,应该用 3PC" → 3PC 的"参与者超时自动提交"在网络分区时恰恰制造不一致,它解决可用性痛点却引入更隐蔽的正确性问题,生产中几乎没人用。
- ❌ "TCC 是强一致方案" → TCC 通过 Try 预留 + Confirm/Cancel 保证的是最终一致,只是 Try 阶段提供了资源隔离,一致性比消息类更强。
- ❌ "SAGA 补偿了就等于没发生过" → 无隔离性意味着中间态已被其他事务看到,补偿只能撤销数据、撤销不了副作用(已发通知、已占外部资源)。
🔀 发散问题
为什么 3PC 在生产中几乎没人用?
3PC 引入超时默认提交来解决阻塞,但"参与者超时自动提交"在网络分区时恰恰会制造不一致(一侧提交一侧回滚)。它解决了 2PC 的可用性痛点却引入了更隐蔽的正确性痛点,而这两个问题用最终一致方案都能更好地解决,所以 3PC 停留在理论层面。
TCC 和 SAGA 的本质区别是什么?
TCC 有 Try 预留阶段,资源在事务结束前处于"冻结但可见"的隔离态,一致性更强;SAGA 每步直接提交,无隔离,靠补偿回滚。选型关键:资源能否低成本预留(能 → TCC),以及中间态被其他业务看到是否有害(有害 → TCC)。
各方案的细节原理去哪看?
见本文档『2PC 的工作原理是什么?』『TCC 的工作原理是什么?』『SAGA 事务的工作原理是什么?』『本地消息表的工作原理是什么?』;事务消息详见《RocketMQ 面试》『事务消息是如何工作的?』。
【中等】2PC 的工作原理是什么?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 2PC
💎 关键结论
2PC 两步走:准备阶段协调者问、参与者执行但不提交并记 undo/redo;提交阶段协调者按全员反馈发 commit 或 rollback。致命弱点三个:同步阻塞、协调者单点、第二阶段局部网络故障致数据不一致。
⚡记忆卡片
- 口诀:先问后提交,一人否决全回滚
- 关键词:协调者 / 参与者 / 准备阶段 / 提交阶段 / undo 日志 / 同步阻塞
- 链路:协调者发起询问 → 参与者执行并记日志反馈 yes/no → 协调者汇总决议 → 全员 commit 或 rollback → 释放锁资源
📖 核心知识
二阶段提交协议(Two-phase Commit,即 2PC)将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。事务的发起者称协调者,事务的执行者称参与者。二阶段提交的思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈,决定提交或回滚。
阶段 1:准备阶段
- 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
- 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
- 如参与者执行成功,给协调者反馈 yes,即可以提交;如执行失败,给协调者反馈 no,即不可提交。
阶段 2:提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)接下来分两种情况分别讨论提交阶段的过程。
情况 1,当所有参与者均反馈 yes,提交事务。

- 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
- 参与者执行 commit 请求,并释放整个事务期间占用的资源。
- 各参与者向协调者反馈 ack(应答)完成的消息。
- 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况 2,任何一个参与者反馈 no,中断事务。

- 协调者向所有参与者发出回滚请求(即 rollback 请求)。
- 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
- 各参与者向协调者反馈 ack 完成的消息。
- 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。
方案总结:
2PC 方案实现起来简单,实际项目中使用比较少,主要因为以下问题:
- 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
- 可靠性问题:如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
- 数据一致性问题:在阶段 2 中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
🔬 扩展知识
详情
【L3】2PC 的"阻塞点"精确位置:参与者发出 yes 之后、收到协调者最终决议之前,处于"已就绪但结果未知"状态,锁必须一直持有。协调者宕机时间就是这个阻塞时长,这就是为什么 XA 实现必须有 recovery 机制(重放事务日志恢复决议)。
【L4】对比视角:2PC 是"独裁式共识"——协调者一人汇总意见定结果,没有多数派容错;这也是它和 Raft 等共识算法的本质差距,把协调者换成共识组(如 Seata 的 TC 集群化)就能缓解单点问题。
⚠️ 常见误区
详情
常见误区:
- ❌ "参与者反馈 yes 时事务已经提交了" → yes 只表示"执行成功且已记日志",事务尚未提交,锁仍持有,最终结果由协调者决议。
- ❌ "协调者发了 commit 就一定全员一致" → 局部网络故障下部分参与者收不到 commit,出现部分提交部分回滚的数据不一致。
🔀 发散问题
3PC 是怎么改进 2PC 的?
引入超时机制并拆分准备阶段,详见本文档『3PC 的工作原理是什么?』。
2PC 的阻塞问题在 XA 场景下如何缓解?
参与者配置 xa recovery 轮询恢复、协调者集群化 + 事务日志持久化,案例详见本文档『什么是事务?什么是分布式事务?』。
【中等】3PC 的工作原理是什么?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 3PC
💎 关键结论
3PC 在 2PC 基础上加超时机制、把准备阶段拆成 canCommit 和 preCommit 两步。好处是降低阻塞、协调者挂了参与者超时后能自行提交;缺点是数据不一致依然存在——所以生产几乎没人用。
⚡记忆卡片
- 口诀:can、pre、do,超时自己走
- 关键词:canCommit / preCommit / doCommit / 超时机制 / 中断事务
- 链路:canCommit 询问可行性 → preCommit 预执行记日志 → doCommit 真正提交 → 任一环节超时按阶段规则提交或中断
📖 核心知识
三阶段提交协议(Three-phase Commit,3PC),是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。
阶段 1:canCommit
协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 响应(参与者不执行事务操作),否则返回 no 响应:
- 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
- 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
阶段 2:preCommit
协调者根据阶段 1 canCommit 参与者的反应情况来决定是否可以基于事务的 preCommit 操作。根据响应情况,有以下两种可能。
情况 1:阶段 1 所有参与者均反馈 yes,参与者预执行事务。

- 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
- 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
- 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
情况 2:阶段 1 任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务。

- 协调者向所有参与者发出 abort 请求。
- 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
阶段 3:doCommit
该阶段进行真正的事务提交,也可以分为以下两种情况:
情况 1:阶段 2 所有参与者均反馈 ack 响应,执行真正的事务提交。

- 如果协调者处于工作状态,则向所有参与者发出 doCommit 请求。
- 参与者收到 doCommit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
- 各参与者向协调者反馈 ack 完成的消息。
- 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况 2:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务。

- 如果协调者处于工作状态,向所有参与者发出 abort 请求。
- 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
- 各参与者向协调者反馈 ack 完成的消息。
- 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。
注意:进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 doCommit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。
方案总结:
- 优点:相比二阶段提交,三阶段降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。
- 缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
🔬 扩展知识
详情
【L3】3PC 与 2PC 的关键差异在"超时后的默认动作":阶段 1、2 超时默认中断(安全方向),阶段 3 超时默认提交(因为能走到阶段 3 说明所有参与者都已预执行成功,提交大概率正确)。正是这个"超时默认提交"在分区时制造不一致。
【L4】3PC 停留在理论层面的根本原因:它用超时启发式同时去解决可用性和正确性,而这两者在分区下互斥(CAP)。工程上要么接受 2PC 的阻塞换正确,要么走最终一致方案,3PC 两头不讨好。
⚠️ 常见误区
详情
常见误区:
- ❌ "3PC 彻底解决了 2PC 的数据不一致" → 阶段 3 中协调者中断请求无法送达时,参与者超时后默认提交,不一致依然存在。
- ❌ "canCommit 阶段参与者已经执行了事务" → canCommit 只是可行性询问,参与者不执行事务操作,真正执行在 preCommit。
🔀 发散问题
既然 3PC 有缺陷,为什么教科书还要讲它?
它是理解"超时默认动作如何影响正确性"的经典样本,也解释了为什么工程界转向最终一致方案而非继续改进 nPC。
【中等】TCC 的工作原理是什么?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / TCC
💎 关键结论
TCC 是业务层的二阶段:Try 检查并预留(冻结)资源,全部成功走 Confirm 真正提交,任一失败走 Cancel 释放预留。好处是锁粒度小、协调者可集群化;代价是三个接口都要业务自己写,侵入性高。
⚡记忆卡片
- 口诀:Try 冻结、Confirm 落袋、Cancel 解冻
- 关键词:Try / Confirm / Cancel / 资源预留 / 幂等 / 业务侵入
- 链路:Try 检查并预留资源 → 全部 Try 成功则 Confirm 执行业务 → 任一失败则 Cancel 释放预留 → Confirm/Cancel 幂等重试到成功
📖 核心知识
TCC 是服务化的二阶段编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现;
- Try:操作作为一阶段,负责资源的检查和预留。
- Confirm:操作作为二阶段提交操作,执行真正的业务。
- Cancel:是预留资源的取消。
TCC 事务的 Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Commit、Rollback。
Try 阶段
从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
- 完成所有业务检查(一致性)
- 预留必须业务资源(准隔离性)
- Try 尝试执行业务。TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
Confirm / Cancel 阶段
根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。
Confirm:当 Try 阶段服务全部正常执行,执行确认业务逻辑操作

这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。
Cancel:当 Try 阶段存在服务执行失败,进入 Cancel 阶段

Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。
方案总结
TCC 事务机制相比于 XA 事务机制,有以下优点:
- 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
- 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
🔬 扩展知识
详情
【L3】TCC 的"准隔离性"来自 Try 的资源冻结:冻结态对其他全局事务不可用(通过业务状态标记实现),这是它比 SAGA/消息类方案一致性更强的根源;代价是每个资源类型都要设计"可用/冻结"双态。
【L4】TCC 的三个方法都必须幂等且可重试到成功:Confirm 失败不能放弃(资源已预留,放弃即悬挂),Cancel 失败同理。工程上通常配事务状态表记录每个分支进度,重试调度器按表驱动。
⚠️ 常见误区
详情
常见误区:
- ❌ "Try 阶段就是执行完整业务" → Try 只做检查与资源预留(冻结),真正的业务落库在 Confirm;Try+Confirm 合起来才是完整业务。
- ❌ "Cancel 失败可以放弃重试" → Cancel 必须幂等重试到成功,否则冻结的资源永久悬挂;Confirm 同理。
- ❌ "TCC 和 2PC 一样靠数据库锁" → TCC 的隔离靠业务层的资源冻结状态实现,锁粒度由业务控制,不依赖数据库长事务。
🔀 发散问题
Cancel 先于 Try 到达怎么办?
这就是空回滚问题,配套还有防悬挂,详见本文档『什么是 TCC 事务的空回滚、防悬挂?』。
TCC 和 SAGA 怎么选?
看资源能否低成本预留、中间态暴露是否有害,详见本文档『分布式事务有哪些解决方案?各有什么利弊?』。
【困难】什么是 TCC 事务的空回滚、防悬挂?⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / TCC 异常处理
💎 关键结论
空回滚和悬挂本质都是网络异常导致的时序错乱:Cancel 先于 Try 到叫空回滚(要识别 Try 未执行、直接返回成功);Cancel 之后迟到的 Try 又执行成功叫悬挂(要识别 Cancel 已执行、拒绝 Try)。解法都是幂等 + 事务分支状态记录。
⚡记忆卡片
- 口诀:空回滚认栽放行,防悬挂拒收迟客
- 关键词:空回滚 / 防悬挂 / 时序错乱 / 幂等 / 事务状态记录
- 链路:网络异常打乱 Try/Cancel 顺序 → Cancel 先到触发空回滚 → 记录分支状态 → 迟到 Try 发现已 Cancel → 拒绝执行避免资源悬挂
📖 核心知识
- 空回滚与悬挂本质是网络异常导致的时序错乱。
- 解决方案:幂等设计 + 事务日志记录执行状态,确保 Try 和 Cancel 只生效一次。
空回滚
- 定义:Cancel 先于 Try 执行,但 Try 实际并未执行。
- 产生:网络延迟导致 Try 未到,事务协调器超时判定失败,直接发起 Cancel。
- 应对:Cancel 方法需能识别 Try 是否执行,若未执行则直接返回成功。
悬挂
- 定义:Cancel 执行后,迟到的 Try 又执行成功,预留资源无法释放。
- 产生:空回滚后,网络恢复使延迟的 Try 到达并执行。
- 应对:Try 方法需能识别 Cancel 是否已执行,若已执行则拒绝执行。
🔬 扩展知识
详情
【L3】典型实现是维护一张事务分支记录表(xid + 分支 ID + 状态):Cancel 到达时先插记录标记"已回滚";Try 到达时先查记录,若已存在"已回滚"标记则拒绝执行。空回滚与防悬挂用同一张表即可同时解决。
【L4】还有一类相关异常是"幂等失效":Confirm/Cancel 重试时被重复执行,导致重复冻结/释放资源。三个方法与空回滚、防悬挂共同构成 TCC 的"三异常两对策"(幂等 + 状态记录),是 TCC 落地面试的完整答题面。
⚠️ 常见误区
详情
常见误区:
- ❌ "空回滚时 Cancel 应该报错等待 Try" → Cancel 应识别 Try 未执行后直接返回成功;等待会让事务永远卡住。
- ❌ "防悬挂靠 TTL 过期就行" → 悬挂资源(冻结的库存/资金)不会自动释放,必须在 Try 入口基于持久化状态显式拒绝。
🔀 发散问题
TCC 的 Confirm 失败会触发 Cancel 吗?
一般不会:Confirm 被认为"Try 成功则必可完成",失败走幂等重试到成功,而非转 Cancel;具体策略取决于框架实现。
这些异常在 Seata TCC 模式下怎么处理?
Seata 提供事务状态存储与重试框架,但空回滚/防悬挂的业务判断仍需开发者实现,详见本文档『Seata 的工作原理是什么?支持哪些事务模式?』。
【困难】SAGA 事务的工作原理是什么?⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / SAGA
💎 关键结论
SAGA 把长事务拆成一串幂等短事务 Ti,每个 Ti 配幂等补偿 Ci:正常就顺序走完,某步失败就逆序补偿回去。没有预留动作、每步直接提交,所以无隔离性;实现分命令协调(中央编排)和事件编排(链式监听)两种。
⚡记忆卡片
- 口诀:正向 Ti、逆向 Ci,命令编排或事件接力
- 关键词:子事务 Ti / 补偿 Ci / 向前恢复 / 向后恢复 / 命令协调 / 事件编排
- 链路:长事务拆分为有序子事务 → 顺序执行 Ti → 某步失败 → 逆序执行 Ci 补偿 → 或向前恢复重试到成功
📖 核心知识
Saga 事务的核心思想是:将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序依次调用补偿操作。
Saga 事务基本协议如下:
- 将长事务拆分为多个有序子事务:每个 Saga 事务由一系列幂等的有序子事务(sub-transaction)Ti 组成。
- 每个子事务 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。
可以看到,和 TCC 相比,Saga 没有"预留"动作,它的 Ti 就是直接提交到库。
下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分。Saga 的执行顺序有两种:

- 事务正常执行完成 T1, T2, T3, ..., Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。
- 事务回滚 T1, T2, ..., Tj, Cj, ..., C2, C1,其中 0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。
恢复策略:Saga 定义了两种恢复策略:
- 向前恢复(forward recovery)

对应于上面第一种执行顺序,适用于必须要成功的场景,失败需要进行重试,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试), ..., Tn,其中 j 是发生错误的子事务(sub-transaction)。该情况下不需要 Ci。
- 向后恢复(backward recovery)

对应于上面提到的第二种执行顺序,其中 j 是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。
Saga 事务常见的有两种不同的实现方式:命令协调和事件编排。
命令协调
- 命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。
中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。

以电商订单的例子为例:
- 事务发起方的主业务逻辑请求 OSO 服务开启订单事务。
- OSO 向库存服务请求扣减库存,库存服务回复处理结果。
- OSO 向订单服务请求创建订单,订单服务回复创建结果。
- OSO 向支付服务请求支付,支付服务回复处理结果。
- 主业务逻辑接收并处理 OSO 事务处理结果回复。
中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
事件编排
- 事件编排(Event Choreography):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
以电商订单的例子为例:

- 事务发起方的主业务逻辑发布开始订单事件。
- 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
- 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
- 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
- 主业务逻辑监听订单已支付事件并处理。
事件编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。
方案总结
命令协调设计的优点和缺点:
优点如下:
- 服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
- 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
- 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。
缺点如下:
- 中央协调器容易处理逻辑容易过于复杂,导致难以维护。
- 存在协调器单点故障风险。
事件编排设计的优点和缺点:
优点如下:
- 避免中央协调器单点故障风险。
- 当涉及的步骤较少服务开发简单,容易实现。
缺点如下:
- 服务之间存在循环依赖的风险。
- 当涉及的步骤较多,服务间关系混乱,难以追踪调测。
值得补充的是,由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性,当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。
🔬 扩展知识
详情
【L3】补偿动作的语义边界:Ci 只能撤销"数据影响",撤销不了"副作用"——已发送的短信、已开具的发票、已通知的第三方无法回滚。所以 SAGA 选型先问"每一步是否都有可行的补偿",补偿不了的步骤只能向前恢复。
【L4】无隔离性的工程对策:① 语义锁(状态字段标记"处理中");② 交换律(重排操作顺序避免冲突);③ 悲观视图(UI 隐藏中间态数据);④ 重读值(提交前校验读取值未变)。这些是 SAGA 论文给出的标准补偿手段。
⚠️ 常见误区
详情
常见误区:
- ❌ "SAGA 回滚后系统状态和事务没发生过一样" → 每步 Ti 都是真实提交,中间态已被其他事务观察,回滚只能逆序补偿数据,无法抹去副作用。
- ❌ "补偿动作随便写个反向 SQL 就行" → Ci 必须幂等(失败会重试),且要基于流水/版本号执行,否则重复补偿会制造新不一致。
- ❌ "事件编排一定优于命令协调" → 步骤多时事件链难以追踪调试、服务间循环依赖风险高,长流程通常选命令协调。
🔀 发散问题
SAGA 和 TCC 的本质区别是什么?
SAGA 无预留、每步直接提交、靠补偿回滚;TCC 有 Try 预留、隔离性更强。详见本文档『分布式事务有哪些解决方案?各有什么利弊?』。
补偿一直失败怎么办?
补偿也需要重试到成功,仍失败则进人工工单队列 + 对账兜底,不能无限自动重试。
【中等】本地消息表的工作原理是什么?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 本地消息表
💎 关键结论
本地消息表把"业务操作 + 写消息记录"放进同一个本地事务,保证"业务成功消息必落表、业务失败消息不落表",再靠轮询把消息发到 MQ 让下游消费。它用本地事务的原子性绕开了分布式事务。
⚡记忆卡片
- 口诀:业务消息同事务,轮询投递靠幂等
- 关键词:消息表 / 本地事务 / 轮询发送 / 幂等消费 / 状态回写
- 链路:主动方本地事务写业务 + 写消息表 → 轮询发送 MQ → 被动方消费并处理 → 回传结果更新消息状态 → 失败轮询重发
📖 核心知识
本地消息表的核心思路是将分布式事务拆分成本地事务进行处理。
方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样设计可以避免"业务处理成功 + 事务消息发送失败",或"业务处理失败 + 事务消息发送成功"的棘手情况出现,保证 2 个系统事务的数据一致性。
事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。
整个业务处理流程如下:

- 步骤 1、事务主动方处理本地事务。 事务主动方在本地事务中处理业务更新操作和写消息表操作。上面例子中库存服务在本地事务中完成扣减库存和写消息表(图中 1、2)。
- 步骤 2、事务主动方通过 MQ 通知事务被动方处理事务。 消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。
- 步骤 3、事务被动方通过 MQ 返回处理结果。 上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8)。
为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。具体保存一致性的容错处理如下:
- 当步骤 1 处理出错,事务回滚,相当于什么都没发生。
- 当步骤 2、步骤 3 处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询超时的消息数据,再次发送消息到 MQ 进行处理。事务被动方消费事务消息重试处理。
- 如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
- 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。
方案总结
方案的优点如下:
- 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
- 方案简单,容易实现。
缺点如下:
- 与具体的业务场景绑定,耦合性高,不可复用。
- 需要额外维护消息数据的传输,占用业务系统资源。
- 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
🔬 扩展知识
详情
【L3】本地消息表与事务消息的本质相同点:都是"先把事务意图持久化到可靠存储(业务库消息表 / MQ 半消息存储),再用异步机制(轮询 / 回查)兑现"。区别只在意图存储的位置与驱动方。
【L4】性能瓶颈量化直觉:消息表与业务表同库同事务,高吞吐下消息表写入与业务争抢 DB 连接与 IO;轮询周期又决定不一致窗口下限。吞吐敏感场景应转向 MQ 事务消息方案。
⚠️ 常见误区
详情
常见误区:
- ❌ "本地消息表需要 MQ 支持事务特性" → 恰恰相反,它把可靠性做在业务库侧,任何 MQ 都能用,这是它相对事务消息的优势。
- ❌ "消息发出去就完事了" → 还需要被动方回执 + 主动方状态更新 + 定时轮询兜底未处理消息,链路任一环节缺失都可能卡单。
🔀 发散问题
本地消息表和事务消息怎么选?
看吞吐与 MQ 能力:事务消息吞吐更高但依赖 MQ 回查特性,本地消息表自主可控但占用业务库,详见《RocketMQ 面试》『事务消息是如何工作的?』。
最大努力通知和本方案的区别?
可靠性与方向不同,详见本文档『什么是最大努力通知?它和本地消息表有什么区别?』。
【中等】什么是最大努力通知?它和本地消息表有什么区别?⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 最大努力通知
💎 关键结论
最大努力通知是最柔性的方案:发起方完成本地事务后尽力通知接收方(按递增间隔重试),但不保证对方一定处理成功,接收方还要提供查询接口主动核对。典型场景就是支付回调。
⚡记忆卡片
- 口诀:尽力推、重试推、拉来对
- 关键词:最大努力通知 / 递增重试 / 主动核对 / 支付回调
- 链路:发起方完成本地事务 → 推送通知给接收方 → 失败按递增间隔重试 → 接收方未收到则主动查询核对 → 双方数据最终对齐
📖 核心知识
最大努力通知是一种柔性事务方案,事务发起方在完成本地事务后,尽最大努力(通过消息队列等)通知事务接收方处理,通知可能有多次重试,但不保证接收方一定能处理成功。接收方可以根据通知结果自行核对、补偿。
典型场景:支付回调通知。支付平台完成支付后,通知商户系统支付结果。如果商户系统没有收到或处理失败,支付平台会按递增的时间间隔多次重试(如 1m、5m、10m、30m、1h、2h、6h、15h),直到通知成功或达到最大重试次数。
工作流程:
- 事务发起方(如支付平台)完成本地事务。
- 发起方通过 MQ 或 HTTP 回调通知接收方。
- 接收方处理业务并返回结果。
- 如果通知失败或未收到响应,发起方按策略多次重试。
- 接收方也可主动调用发起方的查询接口核对结果。
与本地消息表的区别:
| 维度 | 本地消息表 | 最大努力通知 |
|---|---|---|
| 可靠性 | 发送方保证消息不丢(本地事务保证) | 接收方需主动核对(发送方尽力通知) |
| 业务方向 | 发送方 → 接收方(推) | 发送方 → 接收方(推)+ 接收方查询(拉) |
| 一致性 | 较强(发送方保证消息发出) | 较弱(通知可能丢失,需接收方兜底) |
| 重试机制 | 发送方轮询重发 | 发送方按递增间隔重试,接收方提供查询接口 |
| 典型场景 | 跨服务数据一致性(如扣库存+下单) | 第三方回调(如支付回调、物流通知) |
🔬 扩展知识
详情
【L3】最大努力通知的适用边界是"跨组织、弱约束":发起方无法控制接收方的实现质量(第三方商户系统),只能尽力推送 + 提供查询;而对内部服务间的数据一致性要求,应使用本地消息表/事务消息这类发送方保证不丢的方案。
【L4】递增重试间隔(1m→15h)的设计动机:接收方故障通常是短时(抖动、发布)或长时(宕机)两类,指数级拉开间隔既避免短时故障时风暴重试,又在长时故障后保留低频兜底尝试。
⚠️ 常见误区
详情
常见误区:
- ❌ "最大努力通知能保证最终一致" → 它只承诺尽力重试,最终一致性靠接收方主动核对兜底,一致性强度弱于本地消息表。
- ❌ "支付回调收到并处理成功就不用管了" → 回调可能乱序、重复,接收方仍需幂等处理并保留主动查询对账能力。
🔀 发散问题
为什么支付平台不直接用事务消息?
跨组织场景下接收方不在同一 MQ 体系内,只能走 HTTP 回调 + 查询接口的开放协议。
【困难】Seata 的工作原理是什么?支持哪些事务模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / Seata
💎 关键结论
Seata 三角色:TC 管全局状态、TM 定事务边界、RM 管分支资源。四种模式:AT(自动补偿,默认)、TCC、SAGA、XA。AT 最常用:一阶段生成 undo_log 随本地事务提交,二阶段提交只需删日志、回滚靠镜像恢复。
⚡记忆卡片
- 口诀:TC 指挥、TM 开局、RM 干活;AT 自动、XA 强一致
- 关键词:TC / TM / RM / AT / TCC / SAGA / XA / undo_log / 全局锁
- 链路:TM 开启全局事务拿 XID → 各 RM 执行分支事务并记录 undo_log → TC 汇总分支结果 → 二阶段统一删除 undo_log 或按镜像回滚
📖 核心知识
Seata 是阿里巴巴开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
核心角色:
- TC(Transaction Coordinator,事务协调者):维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
- TM(Transaction Manager,事务管理器):定义全局事务的范围,开始、提交或回滚一个全局事务。
- RM(Resource Manager,资源管理器):管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,接收 TC 的指令提交或回滚分支事务。
Seata 支持四种事务模式:
| 模式 | 原理 | 一致性 | 侵入性 | 性能 |
|---|---|---|---|---|
| AT | 基于 SQL 解析自动生成回滚 SQL,通过 undo_log 表实现补偿 | 最终一致 | 低 | 高 |
| TCC | 业务实现 Try/Confirm/Cancel 三个接口 | 最终一致 | 高 | 中 |
| SAGA | 长事务拆分为多个短事务,失败时执行补偿 | 最终一致 | 中 | 中 |
| XA | 基于数据库 XA 协议的强一致性方案 | 强一致 | 低 | 低 |
AT 模式工作原理(最常用):
AT 模式是 Seata 的默认模式,其核心是通过 undo_log 表记录数据变更前后的快照,实现自动补偿。
一阶段:
- TM 向 TC 注册全局事务,获取全局事务 XID。
- 拦截业务 SQL,查询更新前数据,生成
before image。 - 执行业务 SQL。
- 查询更新后数据,生成
after image。 - 将
before image和after image存入undo_log表。 - 向 TC 注册分支事务,并在本地事务中提交(业务 SQL + undo_log 一起提交)。
- 报告分支事务状态给 TC。
二阶段-提交:
- TC 收到所有分支事务成功报告,通知各 RM 异步删除
undo_log记录。 - 由于一阶段已经提交本地事务,二阶段只需清理 undo_log,非常高效。
二阶段-回滚:
- TC 收到某个分支事务失败报告,通知各 RM 回滚。
- RM 根据 XID 和 Branch ID 查找
undo_log记录。 - 校验
after image与当前数据是否一致(脏写检测)。 - 如果一致,用
before image恢复数据;如果不一致,说明数据被其他事务修改,需要人工介入。 - 删除
undo_log记录。
🔬 扩展知识
详情
【L3】AT 模式的写隔离:AT 模式在一阶段提交本地事务后,会释放本地锁,这可能导致脏写问题。Seata 通过全局锁机制解决:在提交本地事务前,RM 先向 TC 申请全局锁(锁定被修改行的主键)。其他全局事务在修改同一行时,必须等待全局锁释放。这样保证了在全局事务提交/回滚前,不会被其他全局事务修改。
但注意,AT 模式的全局锁不能防止本地事务的脏写(非 Seata 管理的事务不受约束),这是 AT 模式的一个局限。
【L4】AT 与 2PC 的关键差异:2PC 的参与者一阶段"执行但不提交"、锁持有到二阶段;AT 一阶段直接提交本地事务释放资源锁,二阶段异步清理 undo_log——这就是 AT 性能显著优于 XA 的原因,代价是牺牲了跨分支的实时隔离。
⚠️ 常见误区
详情
常见误区:
- ❌ "AT 模式是强一致方案" → AT 是最终一致:一阶段已提交本地事务,分支间存在不一致窗口,靠全局锁缓解而非消除。
- ❌ "全局锁能挡住所有并发写" → 全局锁只约束 Seata 管理的全局事务,绕过 Seata 的本地事务仍可能脏写,回滚时靠脏写检测发现并人工介入。
- ❌ "四种模式可以随意混用" → 模式选择取决于一致性要求与资源类型(XA 强一致低性能、AT 低侵入、TCC 高侵入强隔离、SAGA 长流程),同一事务链路内的分支应匹配同一模式。
🔀 发散问题
AT 的 undo_log 和 MySQL undo log 是一回事吗?
不是:AT 的 undo_log 是 Seata 在业务库建的应用层表,记录行级前后镜像;MySQL undo log 是引擎层回滚段。
Seata 的 TCC 模式要自己处理空回滚吗?
框架提供状态存储与重试,但空回滚/防悬挂的业务判断逻辑仍需开发者实现,详见本文档『什么是 TCC 事务的空回滚、防悬挂?』。
【中等】什么是幂等性?分布式幂等如何实现?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 幂等性
💎 关键结论
幂等 = 同一操作执行多次与一次结果相同。分布式下重试不可避免,所以写操作幂等是基本要求。本质一句话:"唯一标识 + 去重"——先找业务唯一键,再选可靠去重介质,且判断与业务必须同一原子操作内完成。
⚡记忆卡片
- 口诀:唯一键打底,同事务去重
- 关键词:唯一索引 / Token / 去重表 / 乐观锁 / 状态机 / 分布式锁
- 链路:请求携带业务唯一键 → 去重介质原子判断 → 重复则拒绝并返回幂等成功 → 首次则执行业务并落去重记录
📖 核心知识
幂等性指同一个操作执行多次与执行一次产生的结果相同。分布式环境下,网络重试、MQ 重复投递、定时任务重复执行都无法完全避免,因此写操作的幂等设计是分布式系统的基本要求。
幂等性分级:
- 读操作:天然幂等。
- 删除操作:
DELETE WHERE id = x天然幂等。 - 更新操作:绝对值更新(
SET balance = 100)幂等,增量更新(SET balance = balance + 1)不幂等。 - 插入操作:不幂等,需要额外设计。
常见实现方案:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 唯一索引/约束 | 业务唯一键建唯一索引,重复插入报异常 | 订单、支付等有天然业务主键 |
| Token 机制 | 先申请 token,提交时原子校验并删除,重复请求拒绝 | 表单提交、防重复点击 |
| 去重表 | 将 requestId/bizId 写入去重表(唯一键) | 通用消息消费去重 |
| 乐观锁 | UPDATE ... WHERE version = ? | 更新场景 |
| 状态机 | 状态只能单向流转(已支付 → 已完成),非法流转拒绝 | 订单状态流转 |
| 分布式锁 | 执行前先锁住资源,防止并发重复执行 | 并发防护 |
实现要点:
- MQ 消费端应提取消息唯一 ID(或业务唯一键)做去重,消费失败重试时靠幂等兼容。
- Redis 去重有过期窗口,强一致要求高的场景应用数据库去重表。
- 去重判断与业务操作尽量在同一事务内完成,避免"判断通过但业务失败"导致的状态不一致。
一句话总结:幂等的本质是"唯一标识 + 去重"——先找到业务唯一键,再选择可靠的去重介质。
方案权衡:去重介质的可靠性 vs 性能
| 去重介质 | 可靠性 | 性能 | 适用边界 |
|---|---|---|---|
| 数据库唯一索引 / 去重表 | 强(事务内原子判断 + 业务同事务提交) | 中(受 DB 并发限制) | 资金、订单等不容许任何重复的场景,首选 |
| Redis SETNX / TTL | 中(过期窗口、主从切换可能丢 key) | 高 | 高并发、可容忍极端情况重复(可事后对账)的场景 |
| Token 机制 | 强(申请-核销原子化,可用 Redis Lua 或 DB) | 中 | 表单提交、防重复点击等前置校验场景 |
| 状态机 | 强(状态单向流转天然去重) | 高 | 订单状态流转类业务,应与唯一索引组合使用 |
选型经验:去重判断必须与业务操作在同一事务 / 同一原子操作内完成,否则会出现"判断通过但业务失败"的窗口:Redis 去重与 DB 写入分两步时,若中间崩溃,去重记录已存在但业务未执行,重试反而被拒绝。
🔬 扩展知识
详情
【L3】失效场景:幂等方案在什么条件下失效
- 唯一键选错:用自增 ID、时间戳这类非业务键做去重,重试时每次生成新键,去重形同虚设。
- Redis 去重窗口失效:去重 key TTL 设短了(如 10 分钟),超过窗口的延迟重试(如 MQ 重投递、跨天补偿)照样重复执行;Redis 主从切换时未同步的 key 丢失,同样失效。
- "查询 + 插入"两步操作的竞态:两个并发请求同时查到"不存在",都插入成功。必须用唯一索引兜底或分布式锁串行化。
- 时钟回拨导致唯一键重复:用雪花算法生成业务键时,时钟回拨可能生成重复 ID,与幂等去重叠加成双重风险。
【L3】量化参考:
- MQ 重复投递:Kafka 消费重试默认最多 3 次(
delivery.timeout默认 2 分钟),RocketMQ 消费重试最多 16 次、从 10s 到 2h 递增——去重窗口必须覆盖小时级。 - Redis 去重的 TTL 经验值:效率拦截场景 5~10 分钟;兜底场景必须用持久化介质,而不是加大 TTL。
- 数据库唯一索引的并发兜底:MySQL 下并发插入相同唯一键,只有一个成功,其余报
Duplicate entry(错误码 1062),业务需捕获该异常并返回幂等成功而非报错。
【L4】幂等和"恰好一次(exactly-once)"的关系:分布式系统的消息投递只能做到 at-least-once,"恰好一次"实际上是"at-least-once + 幂等去重"的效果。所以任何声称 exactly-once 的系统,本质上都是在某一端做了幂等(如 Kafka 事务是把消费位点提交与生产原子绑定 + 生产者幂等序列号)。
🏭 实战场景
详情
踩坑案例:某支付回调接口用 Redis SETNX 做幂等(TTL 1 小时)。某次渠道方因自身故障,在 3 小时后才补推一批历史支付通知,此时去重 key 早已过期,这批订单被重复入账,产生资损。排查:对账发现同一支付单号出现两笔入账,追溯发现回调间隔超过了 TTL。修复:① 幂等介质改为数据库唯一索引(支付单号 + 渠道流水号),永久去重;② Redis 仅保留作为前置快速拦截(减轻 DB 压力),不再作为唯一防线;③ 对超过 24 小时的迟到回调转人工审核。教训:幂等的有效期必须 ≥ 重试可能发生的最大时间跨度,资金类场景的唯一防线必须是持久化的唯一索引。
场景题:订单支付回调接口在大促期间被渠道方重复回调(同一单最多 7 次),监控发现部分订单的"发放奖励"被重复执行,奖励多发。如何排查和修复?
- 应急处理:立即对回调接口加临时去重前置(Redis SETNX 支付单号,拦掉大部分重复);对已多发的奖励暂停自动发放,导出清单人工核对回收;与渠道方确认其重试策略(间隔、次数上限),评估后续重复压力。
- 根因分析:检查代码发现"发放奖励"的幂等依赖的是"查询订单是否已发放"(先查后写),两个并发回调同时查到"未发放",都执行了发放;且没有唯一索引兜底。这属于典型的"用查询代替幂等约束"错误。
- 长期方案:① 发放记录表对(订单号)建唯一索引,发放动作与插入记录同事务,靠
Duplicate entry兜底;② 回调入口保留 Redis 前置拦截降低 DB 压力;③ 状态机约束:订单只能从"待发放"单向流转;④ 奖励流水与订单 T+1 对账。 - 权衡:数据库唯一索引比 Redis 去重性能低,但幂等场景的正确性优先级永远高于性能:前置缓存挡流量、DB 约束保正确,两层组合而不是二选一。
⚠️ 常见误区
详情
常见误区:
- ❌ "先查一下有没有执行过,没有再执行,这就是幂等" → 查询与写入之间存在竞态窗口,并发重试可能同时通过查询;必须依赖唯一索引、乐观锁、CAS 等原子约束。
- ❌ "Redis SETNX 做去重足够可靠" → Redis 有 TTL 窗口且主从切换可能丢 key,只能做效率拦截,资金类场景唯一防线必须是持久化唯一索引。
- ❌ "接口幂等了就不需要分布式锁" → 幂等解决"重复执行结果相同",锁解决"并发执行的互斥";并发扣减同一余额这类资源竞争仍需锁或行级约束。
🔀 发散问题
为什么"先查后写"不能实现幂等?
查询与写入之间存在竞态窗口,并发重试可能同时通过查询。真正的幂等必须依赖原子约束:唯一索引、乐观锁版本号、CAS、分布式锁,把"判断与生效"变成不可分割的一步。
接口幂等了还需要分布式锁吗?
幂等解决"重复执行结果相同",锁解决"并发执行的互斥",两者维度不同。若并发执行会产生资源竞争(如同一账户余额并发扣减),仍需锁或数据库行级约束;幂等不能替代互斥。
幂等键怎么选?
选业务唯一键(支付单号、订单号)而非技术键(自增 ID、时间戳),并注意雪花算法时钟回拨可能生成重复 ID 的叠加风险。
分布式锁
【简单】什么是分布式锁?为什么需要分布式锁?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:分布式协同 / 分布式锁基础
💎 关键结论
分布式锁就是把进程内锁的互斥对象扩展到跨机器的共享资源。用之前先问动机:保正确性(防资损超卖)必须选强一致存储 + fencing;保效率(防重复计算)用 Redis 锁即可,偶尔失效可接受。
⚡记忆卡片
- 口诀:正确性上强一致,效率用 Redis
- 关键词:互斥 / 正确性 / 效率 / fencing / TTL
- 链路:多进程跨机器竞争共享资源 → 本地锁失效 → 引入分布式锁 → 按动机选型 → 正确性场景配 fencing/幂等兜底
📖 核心知识
在计算机科学中,锁是在并发场景下用于强行限制资源访问的一种同步机制,即用于在并发控制中通过互斥手段来保证数据同步安全。
在 Java 进程中,可以使用 Lock、synchronized 等来支持并发锁。如果是同一台机器的不同进程,想要同时操作一个共享资源(例如修改同一个文件),可以使用操作系统提供的「文件锁」或「信号量」来做互斥。这些发生在同一台机器上的互斥操作,可以称为本地锁。

本地锁无法协同不同机器间的互斥操作。为了解决这个问题,需要引入分布式锁。
分布式锁,顾名思义,应用于分布式场景下,它和单进程中的锁并没有本质上的不同,只是控制对象由一个进程中的多个线程变成了多个进程中的多个线程。此外,临界区的资源也由进程内共享资源变成了分布式系统内部共享资源。

为什么需要分布式锁:两类典型动机
| 动机 | 说明 | 例子 | 失效后果 |
|---|---|---|---|
| 正确性(防数据损坏) | 互斥失败会直接破坏数据 | 并发扣减同一账户余额、并发分配同一库存 | 资损、超卖,不可逆 |
| 效率(防重复劳动) | 互斥失败只是浪费资源 | 定时任务多实例重复执行、重复计算报表 | 资源浪费、重复通知,可容忍偶发 |
这个区分至关重要,它直接决定选型:保正确性的锁必须选强一致存储(ZooKeeper/etcd)并配套 fencing;保效率的锁可以用高性能的 Redis,偶尔失效可接受。
方案权衡:三大实现路线的定位
| 路线 | 代表 | 优点 | 缺点 / 适用边界 |
|---|---|---|---|
| 基于数据库 | 唯一索引锁表 | 实现简单、无额外组件 | 性能最差、无阻塞等待、易死锁;仅适合低频、无现成中间件的场景 |
| 基于 Redis | SET NX EX + Redisson | 性能最高(10 万+ TPS 量级) | 主从切换可能丢锁;适合效率类、高并发锁 |
| 基于 ZooKeeper | 临时顺序节点 + Watch | 可靠性最高(会话断开自动释放、CP 强一致) | 性能弱于 Redis(写全走 Leader);适合正确性优先、中低并发场景 |
🔬 扩展知识
详情
【L3】失效场景:分布式锁的三大失效根源
- 锁过期但业务未完成:TTL 到期锁自动释放,另一个线程拿到锁,两个线程同时持有锁。需看门狗续期缓解,但 GC 停顿(Stop-The-World)期间续期线程也会停摆,无法根治。
- 锁服务端故障:Redis 主从切换丢锁(未同步的 key 丢失);ZooKeeper 选主期间锁服务短暂不可用。
- 客户端假死:网络断连 / 长时间 GC 后客户端恢复,误以为自己仍持锁继续操作——锁只能保护"诚实的客户端",无法约束已经失联后恢复的持有者。
【L3】量化参考:
- 锁超时经验值:TTL 应显著大于业务最大执行时间(常见取 3~10 倍),如业务 P99 执行 3 秒则 TTL 设 30 秒;Redisson 看门狗默认锁时长 30s、每 10s(1/3 周期)续期一次。
- 性能对比:Redis 锁可达 10 万+ TPS 量级;ZooKeeper 锁受限于写走 Leader,通常数千 TPS;数据库锁数百 TPS 即可能成为瓶颈。
- ZooKeeper 会话超时默认 2 倍 tickTime(常见 4~40s 区间),会话断开即自动删除临时节点释放锁,无需 TTL 担心死锁。
📚 延伸阅读:分布式锁实现汇总、分布式锁实现原理与最佳实践 - 阿里云开发者、聊聊分布式锁 - 字节跳动技术团队、Redis、ZooKeeper、Etcd,谁有最好用的分布式锁? - 腾讯云开发者
🏭 实战场景
详情
踩坑案例:某系统的对账定时任务在两个实例上用 Redis 锁互斥。某次实例 A 持锁期间发生 Full GC 停顿 45 秒,锁 TTL(30 秒)过期后实例 B 拿到锁开始对账;A 醒来后误以为自己还持锁继续执行,两个实例并发修改同一批待平账记录,导致部分账款被重复平账。排查:日志显示两个实例的同一批次处理时间重叠 12 秒。修复:① 关键写操作增加 fencing:带锁版本号的条件更新(UPDATE ... WHERE lock_version = ?),旧版本写入被拒;② GC 调优减少长停顿;③ 对账动作幂等化。教训:锁 TTL 和客户端 GC 是天然矛盾的,锁只能互斥"正常活着的客户端",正确性兜底必须靠 fencing 或幂等。
场景题:某库存服务用 Redis 分布式锁保护"扣减库存"临界区。某天 Redis 主从切换后,监控发现同一 SKU 在 5 秒内出现两次并发扣减,产生超卖。如何排查和决策?
- 应急处理:先锁定影响面:导出切换时间窗内的该 SKU 扣减流水,核对实际超卖量,人工或补偿脚本修正库存;评估切换期间是否有其他 SKU 受影响(按时间窗扫描)。
- 根因分析:时间线与 Redis 主从切换吻合,典型的"主从异步复制丢锁":实例 A 在旧主上加锁成功,旧主未同步 key 到从库即宕机,从库提升为新主后 key 不存在,实例 B 加锁成功,两者同时持锁。这是 Redis 锁的已知失效模式,不是 bug 而是架构固有特性。
- 长期方案:① 若锁用于保正确性,迁移到 ZooKeeper / etcd 锁(CP,切换不丢锁);或② 保留 Redis 锁但把正确性兜底移到存储层:扣减改为数据库条件更新(
UPDATE ... WHERE cnt >= ?),锁退化为效率优化;③ 若坚持 Redis 且必须强互斥,评估 RedLock(但需接受其争议与复杂度)。 - 权衡:把正确性押在 Redis 锁上,等于接受"主从切换窗口内可能双持锁"的尾部风险;库存这种错了就是资损的场景,正确做法是让锁只管效率、让存储层约束保正确——锁的选型先问"它防的是效率问题还是正确性问题",答案决定一切。
⚠️ 常见误区
详情
常见误区:
- ❌ "分布式锁比本地锁只是范围更大,失效模式相同" → 本地锁持有者崩溃后操作系统回收资源、锁自然释放;分布式锁服务端无法区分"死了"和"网络不好",只能靠 TTL/会话超时启发式判死,存在误判窗口——这是分布式锁一切复杂性的根源。
- ❌ "锁过期就等于锁被安全释放" → 过期只是服务端删了 key,原持有者对此一无所知,醒来后仍会操作临界区;安全性还需 fencing token、幂等、条件更新配合。
- ❌ "并发控制必须上分布式锁" → 幂等插入、数据库行锁/乐观锁能解决的场景不该引入分布式锁,滥用会徒增故障面。
🔀 发散问题
分布式锁和本地锁在失效模式上有什么本质区别?
本地锁的持有者崩溃后,操作系统会回收进程资源,锁自然释放;分布式锁的持有者崩溃 / 断网后,锁服务端无法区分"死了"和"只是网络不好",只能靠 TTL / 会话超时这种"超时即认定死亡"的启发式机制,而超时机制天然存在误判窗口——这是分布式锁一切复杂性的根源。
什么场景下分布式锁是完全不必要的?
两类:① 操作本身幂等且无资源竞争(如基于唯一索引的插入);② 可以用数据库行级锁 / 乐观锁在存储层解决的并发控制。很多"分布式锁滥用"实际上是把存储层能解决的问题搬到了应用层,增加了故障面。
实现分布式锁要注意哪些要点?
见本文档『实现分布式锁有哪些要点?』。
【困难】实现分布式锁有哪些要点?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 分布式锁设计要点
💎 关键结论
分布式锁六大要点:互斥(唯一 key)、避免死锁(超时 + 续期,或 ZK 的 Watch 机制)、可重入(记录持有者标识)、公平性(排队按序颁发)、重试(自旋加锁)、容错(多节点多数派写入)。无论基于哪种存储,要点大同小异。
⚡记忆卡片
- 口诀:互斥、防死、可重入,公平、重试、能容错
- 关键词:唯一 key / 超时续期 / 可重入 / 公平性 / 自旋重试 / 多数派容错
- 链路:插入唯一 key 实现互斥 → 超时机制防死锁 → 续期机制防误过期 → 记录持有者标识支持重入与公平 → 多节点写入提升容错
📖 核心知识
分布式锁的解决方案大致有以下几种:
- 基于数据库实现
- 基于缓存(Redis、Memcached 等)实现
- 基于 ZooKeeper 实现
分布式锁的实现要点大同小异,仅在实现细节上有所不同。
分布式锁的实现要点如下:
- 互斥:分布式锁必须是独一无二的,表现形式为:向数据存储插入一个唯一的 key,一旦有一个线程插入这个 key,其他线程就不能再插入了。
- 保证 key 唯一性的最简单的方式是使用 UUID。
- 此外,可以参考 Snowflake ID(雪花算法),将机器地址(IP 地址、机器 ID、MAC 地址)、JVM 进程 ID(应用 ID、服务 ID)、时间戳等关键信息拼接起来作为唯一标识。
- 避免死锁:在分布式锁的场景中,部分失败和异步网络这两个问题是同时存在的。如果一个进程获得了锁,但是这个进程与锁服务之间的网络出现了问题,导致无法通信,那么这个情况下,如果锁服务让它一直持有锁,就会导致死锁的发生。
- 常见的解决思路是引入超时机制,即成功申请锁后,超过一定时间,锁失效(删除 key)。超时机制解锁了死锁问题,但又引入了一个新问题:如果应用加锁时,对于操作共享资源的时长估计不足,可能会出现:操作尚未执行完,但是锁没了的尴尬情况。为了解决这个问题,需要引入锁续期机制:当持有锁的线程尚未执行完操作前,不断周期性检测锁的超时时间,一旦发现快要过期,就自动为锁续期。
- ZooKeeper 分布式锁避免死锁采用了另外一种思路——Watch 机制。
- 可重入:可重入指的是:同一个线程在没有释放锁之前,能否再次获得该锁。其实现方案是:只需在加锁的时候,记录好当前获取锁的节点 + 线程组合的唯一标识,然后在后续的加锁请求时,如果当前请求的节点 + 线程的唯一标识和当前持有锁的相同,那么就直接返回加锁成功;如果不相同,则按正常加锁流程处理。
- 公平性:当多个线程请求同一锁时,它们必须按照请求的顺序来获取锁,即先来先得的原则。锁的公平性的实现也非常简单,对于被阻塞的加锁请求,我们只要先记录好它们的顺序,在锁被释放后,按顺序颁发就可以了。
- 重试:有时候,加锁失败可能只是由于网络波动、请求超时等原因,稍候就可以成功获取锁。为了应对这种情况,加锁操作需要支持重试机制。常见的做法是,设置一个加锁超时时间,在该时间范围内,不断自旋重试加锁操作,超时后再判定加锁失败。
- 容错:分布式锁若存储在单一节点,一旦该节点宕机或失联,就会导致锁失效。将分布式锁存储在多数据库实例中,加锁时并发写入
N个节点,只要N / 2 + 1个节点写入成功即视为加锁成功。
🔬 扩展知识
详情
【L3】"超时 + 续期"是一对矛盾组合:超时防死锁,续期防误过期,但两者都依赖客户端存活且时钟大致正常。客户端整体停顿(GC、宿主机卡死)时,续期线程同样停摆,锁照样过期——这就是 Kleppmann 对 Redis 锁批评的核心(Process Pause)。
【L4】容错要点中的"多数派写入 N 个节点"正是 RedLock 的思路,但它引入了时钟与延迟假设的争议,详见本文档『RedLock 算法有什么争议?Martin Kleppmann 和 Antirez 的争论核心是什么?』。
⚠️ 常见误区
详情
常见误区:
- ❌ "加了超时机制就万事大吉" → 超时只解决死锁,还会引入"业务没做完锁先没了"的新问题,必须配合续期机制。
- ❌ "可重入就是加个计数器" → 必须先校验请求者标识与持有者一致再计数,否则别的线程也能"重入",互斥被破坏。
🔀 发散问题
三种存储介质的锁分别怎么实现?
见本文档『数据库分布式锁的工作原理是什么?』『ZooKeeper 分布式锁的工作原理是什么?』『Redis 分布式锁的工作原理是什么?』。
【中等】数据库分布式锁的工作原理是什么?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 数据库锁
💎 关键结论
数据库锁最朴素:建一张带唯一索引的锁表,insert 成功就是拿到锁,delete 就是释放。问题也很朴素:死锁要加失效时间、非阻塞要循环重试、非重入要记持有者、单点要多实例多数派——补到最后方案越来越复杂,所以它只适合低频场景。
⚡记忆卡片
- 口诀:insert 拿锁、delete 放锁,缺陷全靠补丁
- 关键词:锁表 / 唯一索引 / insert / delete / 失效时间 / 多数派
- 链路:建带唯一约束的锁表 → insert 竞争唯一键 → 成功者持锁 → 业务完成 delete 释放 → 异常场景靠超时清理/重试/持有者记录补丁
📖 核心知识
数据库分布式锁原理:
(1)创建锁表
CREATE TABLE `distributed_lock` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '资源',
`count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '锁次数,统计可重入锁',
`desc` TEXT DEFAULT NULL COMMENT '备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_resource`(`resource`)
)
ENGINE = InnoDB DEFAULT CHARSET = `utf8mb4`;(2)获取锁
想要锁住某个方法时,执行以下 SQL:
insert into distributed_lock(resource,desc) values (‘resourceA’,‘desc’)因为我们对 resource 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
成功插入则获取锁。
(3)释放锁
当方法执行完毕之后,想要释放锁的话,需要执行以下 SQL:
delete from methodLock where resource ='resourceA'数据库分布式锁小结:
数据库分布式锁的问题:
- 死锁:一旦释放锁操作失败,或持有锁的机器宕机、断连,就会导致锁记录一直存在,其他线程无法再获得锁。解决办法:为锁增加失效时间字段,启动一个定时任务,隔一段时间清除一次过期的数据。
- 非阻塞:因为
insert操作一旦失败就会报错,因此未获得锁的线程并不会进入排队队列,要想获得锁就要再次触发加锁操作。解决办法:循环重试,直到插入成功,这么做会产生一定额外开销。 - 非重入:同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。解决办法:在数据库表中加个字段,记录当前获得锁的节点信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
- 单点问题:如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。解决办法:单点问题可以用多数据库实例,同时写入
N个节点,N / 2 + 1个成功就加锁成功。
数据库分布式锁的利弊:
- 优点:直接借助数据库,简单易懂。
- 缺点:会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。此外,数据库性能易成为瓶颈。
🔬 扩展知识
详情
【L3】数据库锁的独特优势常被忽略:它天然持久化、可查询,锁的归属与持有时长可直接审计排查;在没有 Redis/ZK 的极简架构里,它是唯一零额外组件的选择。但数百 TPS 级的加锁频率就可能成为瓶颈,切勿用于高并发路径。
【L4】"多实例 N/2+1 写入"的补丁实质是把 Quorum 思想套在数据库上,但它没有多数派间的复制一致性保证(各库独立写入),可靠性远不如原生共识存储,属于"聊胜于无"级别的容错。
⚠️ 常见误区
详情
常见误区:
- ❌ "数据库锁实现简单,所以是首选" → 它的简单只在 happy path;补齐死锁、阻塞、重入、单点后复杂度反超 Redis/ZK 方案,且性能最差。
- ❌ "insert 失败会排队等待" → insert 违反唯一约束直接报错,不会阻塞排队,需要自行循环重试实现等待语义。
🔀 发散问题
为什么不直接用数据库的行锁/SELECT FOR UPDATE?
行锁依赖长连接与事务,连接断开即释放,跨服务持锁语义难以维持,且占用 DB 连接资源,与独立锁表是不同取舍。
【困难】ZooKeeper 分布式锁的工作原理是什么?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / ZooKeeper 锁
💎 关键结论
ZK 锁靠"临时顺序节点 + Watch":加锁就是在锁目录下建临时顺序节点,序号最小者持锁;其余节点只监听前一位。持有者断连则临时节点自动删除,天然防死锁;可靠性最高,但写全走 Leader,性能不如 Redis。
⚡记忆卡片
- 口诀:临时顺序建节点,最小拿锁,监听前一位
- 关键词:临时节点 / 顺序节点 / Watch 机制 / 自动释放 / 强一致
- 链路:创建临时顺序节点 → 比较序号最小者获锁 → 未获锁者 Watch 前一节点 → 前节点删除触发通知 → 下一个最小者获锁
📖 核心知识
ZooKeeper 分布式锁的实现基于 ZooKeeper 的两个重要特性:
- 顺序临时节点:ZooKeeper 的存储类似于 DNS 那样的具有层级的命名空间。ZooKeeper 节点类型可以分为持久节点(
PERSISTENT)、临时节点(EPHEMERAL),每个节点还能被标记为有序性(SEQUENTIAL),一旦节点被标记为有序性,那么整个节点就具有顺序自增的特点。 - Watch 机制:ZooKeeper 允许用户在指定节点上注册一些
Watcher,并且在特定事件触发的时候,ZooKeeper 服务端会将事件通知给用户。
下面是 ZooKeeper 分布式锁的工作流程:
- 创建一个目录节点,比如叫做
/locks; - 线程 A 想获取锁,就在
/locks目录下创建临时顺序 zk 节点; - 获取
/locks目录下所有的子节点,检查是否存在比自己顺序更小的节点:若不存在,则说明当前线程创建的节点顺序最小,获取锁成功; - 此时,线程 B 试图获取锁,发现自己的节点顺序不是最小,设置监听锁号在自己前一位的节点;
- 线程 A 处理完,删除自己的节点。线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
ZooKeeper 分布式锁的优点是较为可靠:
- 避免死锁:ZooKeeper 通过顺序临时节点 + 监听机制,可以保证:如果持有临时节点的线程主动解锁或断连,ZK 会自动删除临时节点,这意味着锁的释放。所以,不存在锁永久不释放从而导致死锁的问题。
- 单点问题:ZooKeeper 采用主从架构,并确保主从同步是强一致的,因此不会出现单点问题。
ZooKeeper 分布式锁的缺点是:加锁、解锁操作,本质上是对 ZooKeeper 的写操作,全部由 ZooKeeper 主节点负责,然后再同步到从节点。如果加锁、解锁的吞吐量很大,容易出现单点写入瓶颈。
🔬 扩展知识
详情
【L3】为什么监听"前一位"而不是监听最小节点?若所有等待者都监听当前持锁节点,它释放时会引发羊群效应(所有等待者同时被唤醒、同时查询比较)。监听前一位节点让每次释放只唤醒一个后继者,消息量从 O(n) 降到 O(1)。
【L4】ZK 锁的"会话超时自动释放"是把双刃剑:它免去了 TTL 续期的麻烦,但会话超时窗口内客户端若只是网络抖动/长 GC,锁已被别人拿走而它浑然不知——ZK 锁同样无法防御客户端假死,正确性场景仍需 fencing 或幂等兜底。生产上推荐直接用 Apache Curator 的 InterProcessMutex。
⚠️ 常见误区
详情
常见误区:
- ❌ "ZK 锁靠 TTL 过期释放" → ZK 靠会话机制:临时节点随会话断开自动删除,不需要也不使用 TTL。
- ❌ "ZK 锁绝对安全,不需要任何兜底" → 会话超时判定前的客户端假死窗口仍存在双操作风险,正确性场景仍需 fencing/幂等。
- ❌ "等待者应该监听锁目录或最小节点" → 应只监听前一位节点,否则会引发羊群效应。
🔀 发散问题
ZK 锁和 Redis 锁怎么选?
正确性优先中低并发选 ZK,效率优先高并发选 Redis,详见本文档『分布式锁如何进行技术选型?』。
为什么 ZK 写操作必须走 Leader?
ZK 通过 Zab 协议保证写的全序与强一致,详见 ZooKeeper 面试题系列中的写操作流程与 Zab 协议相关题目。
【困难】Redis 分布式锁的工作原理是什么?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / Redis 分布式锁
💎 关键结论
Redis 分布式锁就是一条演进链:SETNX 拿互斥 → 加 NX+PX 原子命令防死锁 → 看门狗续期防业务超时 → 唯一标识 + Lua 脚本安全解锁 → 自旋重试提升体验。但主从切换仍可能丢锁,所以它只适合做"效率锁",正确性要靠幂等兜底。
⚡记忆卡片
- 口诀:setnx 抢锁、NX PX 防死锁、看门狗续命、Lua 安全解锁
- 关键词:SET NX EX / 死锁 / 超时续期 / 唯一标识 / Lua 原子解锁 / 自旋重试
- 链路:setnx 互斥 → 宕机可能死锁 → 加过期时间 → 业务超时需要续期 → 误删需要校验标识 → 分步操作需 Lua 原子化
📖 核心知识
极简版本

- 加锁:Redis 中的
setnx命令,表示当且仅当 key 不存在时,才会写入 key。由于其互斥性,所以可以基于此来实现分布式锁。执行setnx key val,若返回 1,表示写入成功,即加锁成功;若返回 0,表示该 key 已存在,写入失败,即加锁失败。 - 解锁:删除 key 就意味着释放锁,即执行
del key命令。
避免死锁
极简版本有一个很大的问题:存在死锁的可能。持有锁的节点如果执行业务过程中出现异常或机器宕机,都可能导致无法释放锁。这种情况下,其他节点永远也无法再获取锁。
- 对于异常,在 Java 中,可以通过
try...catch...finally来保证:最终一定会释放锁,其他编程语言也有相似的语法特性。 - 对于机器宕机,通常的对策是:为锁加上超时机制,过期自动删除。
在 Redis 中,expire 命令可以为 key 设置超时时间,一旦过期,Redis 会自动删除 key。但 Redis 只能保证单一命令的原子性,不保证组合命令的原子性,所以不能简单用 setnx + expire。可以用一条命令实现 setnx + expire 的组合语义:
# 下面两条命令是等价的
SET key val NX PX 30000
SET key val NX EX 30参数说明:
NX:该参数表示当且仅当 key 不存在,才能写入成功PX:超时时间,单位毫秒EX:超时时间,单位秒
超时续期
为锁加超时时间后,如果应用对操作共享资源的时长估计不足,可能出现:操作尚未执行完,但是锁没了。对策是时间不够就续期:加锁后启动一个定时任务,周期性检测锁是否快要过期,如果快要过期并且操作尚未结束,就对锁进行自动续期。开源 Redis 客户端 Redisson 中已经为锁的超时续期提供了一个成熟的机制——WatchDog(看门狗),可以直接拿来主义。
安全解锁
直接 del key 存在问题:没有任何判断,任何节点都可以随意删除 key,锁可能会被其他节点释放。解决方法:为锁添加唯一性标识(可以是 UUID、雪花算法 ID 等)来进行互斥。
具体实现就是在 set key val 时,将唯一性标识 id 作为 val 写入。解锁前,先判断 key 的 value,必须和 set 时写入的 id 值保持一致,以此确认锁归属于自己。解锁的伪代码如下:
if (redis.get("key") == id)
redis.del("key");由于需要先 get 后 del,无法保证操作的原子性。为了保证原子性,可以将这段伪代码用 Lua 脚本来实现(Redis 支持原子性地执行 Lua 脚本):
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end自旋重试
有时候,加锁失败可能只是由于网络波动、请求超时等原因,稍候就可以成功获取锁。常见做法是设置一个加锁超时时间,在该时间范围内不断自旋重试加锁操作,超时后再判定加锁失败:
try {
long begin = System.currentTimeMillis();
while (true) {
String result = jedis.set(lockKey, uniqId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
// 加锁成功,执行业务操作
return true;
}
long time = System.currentTimeMillis() - begin;
if (time >= timeout) {
return false;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
// 异常处理
} finally {
// 释放锁
}小结:遗留问题
即便做了以上完善,依然存在其他问题:
- 不可重入:同一个线程无法多次获取同一把锁。
- 单点问题:Redis 主从同步存在延迟,有可能导致锁冲突。举例来说:线程一在主节点加锁,如果主节点尚未同步给从节点就发生宕机;此时,Redis 集群会选举一个从节点作为新的主节点。新的主节点没有锁的数据,若有其他线程试图加锁,就可以成功获取锁,即出现同时有多个线程持有锁的情况。解决这个问题,可以使用 RedLock 算法。
Redisson 是一个流行的 Redis Java 客户端,它基于 Netty 开发,并提供了丰富的扩展功能,如:分布式计数器、分布式集合、分布式锁 等。Redisson 支持的分布式锁有多种:Lock, FairLock, MultiLock, RedLock, ReadWriteLock, Semaphore, PermitExpirableSemaphore, CountDownLatch,可以根据场景需要去选择。一般而言,使用 Redis 分布式锁,推荐直接使用 Redisson 提供的 API,功能全面且较为可靠。
方案权衡:单节点锁 vs 主从锁 vs RedLock
| 方案 | 可靠性 | 性能 | 适用边界 |
|---|---|---|---|
单节点 SET NX EX | 节点宕机即锁服务不可用,但不会双持锁(除非重启后 key 丢失) | 最高 | 对锁服务可用性要求不高的效率类锁 |
| 主从 / 哨兵架构锁 | 主从切换窗口可能丢锁(异步复制) | 高 | 效率类锁的主流生产选择,正确性兼底靠存储层 |
| RedLock(多独立主节点) | 理论上降低丢锁概率,但受 NPC(网络延迟、GC、时钟漂移)挑战,且存在争议 | 较低(多节点写) | 不推荐:复杂度高、收益存疑;要正确性首选 ZooKeeper/etcd |
失效场景:Redis 锁的四类失效
- 主从切换丢锁:主节点加锁后未同步到从库即宕机,从库提升为新主后锁 key 不存在,另一个客户端可加锁成功,出现双持锁。
- 锁过期双持锁:业务执行超过 TTL(或续期失败),锁自动释放后他人加锁;即使有看门狗,Full GC 停顿期间续期线程也停摆,GC 恢复后锁可能已易主。
- 误删他人锁:早期实现直接
DEL,会删掉别人刚加的锁;必须校验唯一标识 + Lua 原子执行。 - 时钟问题:Redis 的过期依赖服务端时钟,时钟回拨(NTP 校正)可能让 key 提前 / 延迟过期;RedLock 对时钟的依赖更重,也是 Kleppmann 批评的核心。
🔬 扩展知识
详情
【L3】量化参考:
- Redisson 看门狗:默认锁时长
lockWatchdogTimeout = 30s,续期周期为其 1/3(10s),每次续期重置为 30s;显式指定 leaseTime 时看门狗不启动。 - 加锁命令
SET key val NX PX 30000:单次 RTT 约 0.11ms(同机房);自旋重试的轮询间隔常见 50100ms。 - 主从复制延迟:同机房正常 < 1ms,但主库宕机瞬间未同步的写入(包括刚加的锁)会丢失,丢锁窗口 ≈ 复制延迟 + 故障检测时间(哨兵故障判定默认
down-after-milliseconds=30s,可下调)。 - TTL 经验值:P99 业务执行时间的 3~10 倍;太短容易双持锁,太长则持有者宕机后锁阻塞时间过长。
【L4】Kleppmann 对"基于超时的锁"的根本批评:任何依赖 TTL 的锁都无法防御 Process Pause(GC / 宿主机卡死),因为续期线程和业务线程一起冻结。要真正保正确性,必须在资源侧引入 fencing token(单调递增令牌),由资源拒绝旧 token 的写入,而不是指望锁本身永不失效。
🏭 实战场景
详情
踩坑案例:某订单服务的"合并支付单"任务用 Redisson 分布式锁互斥,锁 TTL 默认 30s + 看门狗。某天该实例发生 Full GC 停顿 40 秒:看门狗线程停摆无法续期,锁 30s 过期后另一实例拿到锁开始处理同一批支付单;GC 恢复后原实例继续执行,两个实例并发拆分同一批单据,导致部分支付单被重复拆分。排查:对比两实例日志发现同一批单号的处理时间重叠 10 秒,且重叠前一刻有 GC 日志。修复:① 临界区写操作改为带单据状态的条件更新(UPDATE ... WHERE status = '待拆分'),旧持有者写入被拒;② 拆分动作幂等化;③ JVM 调优控制 GC 停顿 < 5s。教训:GC 停顿会同时冻结业务线程和续期线程,看门狗只能覆盖"业务变慢",覆盖不了"进程假死";临界区自身的幂等 / 条件更新才是最后防线。
场景题:大促前,团队把"每日结算"定时任务部署了 3 个实例,用 Redis 锁互斥。有同事担心 Redis 主从切换导致重复结算,提议换成 ZooKeeper 锁;另一同事认为"加个重试幂等就够了"。你怎么决策?
- 应急处理:上线前评审阶段无故障;若已上线,先在结算动作处确认幂等兜底是否已存在,再评估是否限流暂停任务观察。
- 根因分析:结算属于"错了就是资损"的正确性场景,风险点有两个:① Redis 主从切换丢锁(概率低但存在);② 持锁实例 GC 停顿 / 网络分区后锁过期双持锁。单靠 Redis 锁 + 看门狗无法消除②;而 ZooKeeper 锁虽然切换不丢锁,但同样存在"会话超时前客户端假死"的窗口——换锁只能降低概率,不能消灭风险。
- 长期方案:分层设防而非单点押宝:① 锁选 Redisson(性能与易用性)承担互斥职责;② 结算动作本身幂等(结算单号唯一索引,已结算状态拒绝重复);③ 关键状态变更用条件更新(
WHERE status = '待结算')作为 fencing 等价物;④ 结算流水 T+1 对账告警。若资源有限只能选一项,选②而不是换锁。 - 权衡:把正确性全部押在锁的可靠性上(无论 Redis 还是 ZooKeeper)都是单点思维;分布式锁在极端场景下必然失效,正确姿势是"锁防效率(99% 的并发)+ 幂等/条件更新保正确(1% 的极端)"。这也回答了两位同事:换 ZooKeeper 锁有价值但不是关键,幂等兜底才是关键。
⚠️ 常见误区
详情
常见误区:
- ❌ "setnx 和 expire 分两条命令执行就行" → 两条命令之间若客户端宕机,key 会永久存在导致死锁;必须用
SET key val NX EX time一条原子命令。 - ❌ "解锁直接 DEL 就好" → 锁可能已过期被他人持有,直接 DEL 会误删别人的锁;必须先校验唯一标识,且用 Lua 脚本保证"校验 + 删除"原子。
- ❌ "看门狗自动续期,锁就不会过期" → GC 停顿 / 进程假死时续期线程同样停摆,锁照样过期双持;看门狗只解决"业务执行变慢"。
🔀 发散问题
为什么解锁必须先校验 value 再用 Lua 脚本删除?
不校验会误删别人的锁(自己的锁过期后别人刚加的锁);分步执行 GET + DEL 则存在竞态:GET 后、DEL 前锁恰好过期易主,仍会误删。Lua 脚本在 Redis 中原子执行,把"校验归属 + 删除"变成不可分割的一步。
看门狗续期能不能完全避免锁过期双持锁?
不能。看门狗只能覆盖"业务执行变慢"的场景;当进程因 GC、宿主机卡死等原因整体停顿时,续期线程同样停顿,锁照样过期。这是 Kleppmann 批评 Redis 锁的核心论据之一:基于超时的锁无法防御 Process Pause。
Redis 锁和 fencing token 如何配合?
加锁时获取一个单调递增的 token(如 Redis INCR 计数器),写临界区资源时携带 token,资源侧拒绝小于已见最大 token 的写入。这样即使锁失效后旧持有者恢复操作,其旧 token 也会被资源侧拒绝。注意:普通 Redis 锁不自带 fencing,需要自行实现。
【中等】RedLock 分布式锁的工作原理是什么?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / RedLock
💎 关键结论
RedLock 的思路是把锁写到 5 个互相独立的 Redis 主节点上,半数以上(3 个)加锁成功且总耗时小于 TTL 才算成功,解锁时向所有节点发删除。它能缓解单点丢锁,但躲不开 NPC(网络延迟、GC、时钟漂移),争议很大,一般不推荐。
⚡记忆卡片
- 口诀:五主投票、过半才算赢、超时就不算、解锁全通知
- 关键词:多独立主节点 / 多数派 / 加锁总耗时 / 全节点解锁 / NPC
- 链路:单节点主从切换可能丢锁 → 改为同时写多个独立主节点 → 半数以上成功才加锁 → 总耗时超 TTL 则锁无意义 → 解锁需清理所有节点残留
📖 核心知识
RedLock 分布式锁,是 Redis 的作者 Antirez 提出的一种解决方案。它在普通 Redis 分布式锁的基础上进行了扩展:

其要点在于:
- (1)加锁操作不是写入单一节点,而是同时写入多个主节点,官方推荐集群中至少有 5 个主节点。
- (2)只要半数以上的主节点写入成功,即视为加锁成功。
- (3)大多数节点加锁的总耗时,要小于锁设置的过期时间。
- (4)解锁时,要向所有节点发起请求。
逐一解释以上各要点的用意:
(1)为什么要同时写入多个主节点? 为了避免单点问题,即使有部分实例出现异常,依然可以正常提供加锁、解锁能力。
(2)为什么要半数以上的主节点写入成功,才视为加锁成功? 在分布式系统中,为了达成共识,常常采用"多数派"策略来进行决策:大多数节点认可的行为,就视为整体通过。
(3)为什么加锁成功后,还要计算加锁的累计耗时? 因为操作的是多个节点,耗时比操作单个实例更久。网络情况复杂,可能存在延迟、丢包、超时等情况,网络请求越多,异常发生的概率就越大。所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经超过了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。
(4)解锁时,为什么要向所有节点发起请求? 因为网络环境的复杂性,可能会存在这种情况:向某主节点写入锁信息,实际写入成功,但是响应超时或丢包。所以,释放锁时,不管之前有没有加锁成功,需要释放所有节点的锁,以保证清理节点上残留的锁。
小结:RedLock 的局限
- RedLock 不能完全保证安全性。分布式系统会遇到三座大山:NPC
- N:Network Delay,网络延迟;
- P:Process Pause,进程暂停(GC);
- C:Clock Drift,时钟漂移。
RedLock 在遇到以上情况时,不能保证安全性。

- RedLock 加锁、解锁需要处理多个节点,代价太高。
总结来说,已知的分布式锁,无论采用什么解决方案,在极端情况下,都无法保证百分百的安全。
🔬 扩展知识
详情
【L3】RedLock 的"总耗时 < TTL"校验为什么重要:客户端拿到多数派锁后,锁的有效时间实际只剩"TTL − 加锁耗时"。若耗时接近 TTL,锁还没用就快过期了;因此 RedLock 实现还要求给每次加锁设置一个远小于 TTL 的单节点请求超时,失败节点快速放弃。
【L4】RedLock 要求 5 个节点是互相独立的主节点(不是同一主从集群),这在实际部署中很难保证——若多个实例共享同一时钟源 / 同一宿主机,时钟漂移与整机宕机会同时击穿多数派。这也是它"理论正确、工程存疑"的原因之一。
📚 延伸阅读:RedLock 官方文档
⚠️ 常见误区
详情
常见误区:
- ❌ "RedLock 用了多数派,所以能保证互斥的绝对正确" → NPC(网络延迟、GC、时钟漂移)任一发生都可能双持锁,RedLock 只是降低概率,不提供正确性证明。
- ❌ "RedLock 的 5 个节点可以用一个 Redis 主从集群的 5 个副本" → 必须是 5 个互相独立的主节点;主从切换场景下多数派同样会丢锁。
- ❌ "解锁只删加锁成功的那些节点就行" → 写入成功但响应丢包的节点上仍残留锁,必须向所有节点发起解锁清理。
🔀 发散问题
RedLock 的争议具体是什么?
Martin Kleppmann 从时钟可靠性、GC 停顿、fencing token 三个角度批评 RedLock 无法保证正确性,Antirez 撰文反驳,详见本文档『RedLock 算法有什么争议?Martin Kleppmann 和 Antirez 的争论核心是什么?』。
不想用 RedLock,又要 Redis 的生态,怎么折中?
主流做法是主从/哨兵架构 + Redisson 看门狗承担效率互斥,正确性交给临界区幂等/条件更新兜底,详见本文档『Redssion 分布式锁的工作原理是什么?』。
【中等】Redssion 分布式锁的工作原理是什么?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / Redisson
💎 关键结论
Redisson 把原生 Redis 锁的坑全填了一遍:原子加锁 + Hash 结构支持可重入、加锁失败用发布订阅或公平队列等待、看门狗每 10 秒续期防过期、Lua 脚本安全解锁。生产上用 Redis 锁,基本就是直接用 Redisson。
⚡记忆卡片
- 口诀:原子加锁可重入,订阅等待看门狗,Lua 解锁发通知
- 关键词:Hash 结构 / 可重入 / 看门狗 / Pub/Sub / 公平锁 / Lua
- 链路:原子 SET 加锁 → 失败则订阅释放消息等待 → 成功则启动看门狗周期续期 → 解锁 Lua 校验归属、递减重入次数、DEL 并 PUBLISH 唤醒等待者
📖 核心知识
Redisson 分布式锁解决了原生 Redis 锁(SET NX EX)的"锁续期、死锁、单点故障"等痛点,核心逻辑可总结为"原子加锁 → 看门狗续期 → 安全解锁 → 高可用扩展",兼顾易用性和生产级可靠性。

步骤 1:加锁(原子操作 + 可重入设计)
客户端调用 lock() / lock(time, unit) 时,Redisson 执行以下逻辑:
- (1)生成唯一标识:客户端 ID(UUID)+ 线程 ID,作为锁的 value(如
a1b2c3:123); - (2)原子加锁命令(Redis 2.6+):
# NX=不存在才设置,PX=过期时间(毫秒),RETRY_COUNT=重试次数
SET lock_key {客户端 ID}:{线程 ID}:1 NX PX 30000- 可重入逻辑:若锁已存在且 value 匹配当前客户端,执行
HINCRBY lock_key {客户端 ID}:{线程 ID} 1(Hash 结构),增加重入次数,无需重新加锁; - 过期时间:默认 30 秒(可配置),避免死锁;若指定了
lock(time, unit),则用用户指定的过期时间,关闭看门狗。
步骤 2:加锁失败 → 阻塞 / 重试(公平 / 非公平策略)
若加锁失败(锁已被其他客户端持有):
- 非公平锁:客户端通过
SUBSCRIBE订阅锁的释放消息,阻塞等待,直到收到解锁通知后重试加锁; - 公平锁:客户端先将自己加入等待队列(Redis List),仅当队列头部是自己时才尝试加锁,保证"先到先得"。
步骤 3:看门狗续期(核心痛点解决)
若加锁时未指定过期时间(调用 lock() 无参方法),Redisson 自动启动看门狗线程:
- (1)看门狗是一个后台定时线程,每隔
lockWatchdogTimeout/3(默认 10 秒)执行一次; - (2)续期逻辑:通过 Lua 脚本原子性延长锁的过期时间至 30 秒:
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
redis.call('pexpire', KEYS[1], ARGV[2])
return 1
else
return 0
end- (3)停止条件:客户端执行
unlock()解锁,或客户端宕机(看门狗线程终止,锁到期自动释放)。
步骤 4:解锁(安全校验 + 原子删除)
客户端调用 unlock() 时,Redisson 执行以下逻辑(Lua 脚本保证原子性):
- 校验锁归属:判断锁的 value 是否匹配当前客户端 ID + 线程 ID;
- 处理可重入:若重入次数 > 1,执行
HINCRBY lock_key {客户端 ID}:{线程 ID} -1,减少重入次数; - 删除锁:若重入次数 = 1,执行
DEL lock_key删除锁,并发布解锁通知(PUBLISH),唤醒等待的客户端; - 异常处理:若锁已过期 / 归属不匹配,抛出
IllegalMonitorStateException,避免误删锁。
🔬 扩展知识
详情
【L3】Redisson 用 Pub/Sub 替代自旋轮询是关键优化:原生自旋每 50~100ms 空发一次 SET,竞争激烈时对 Redis 压力大;订阅模式下等待客户端零开销,只在锁释放时被 PUBLISH 唤醒。代价是订阅连接数和消息丢失风险(丢消息时 Redisson 有兜底超时轮询)。
【L4】Redisson 的锁类型矩阵:RLock(非公平)、RFairLock(公平)、RReadWriteLock(读写)、RRedLock(RedLock 实现,随争议渐被冷落)、MultiLock(联锁,同时锁多个资源)、RSemaphore(信号量)。选型口诀:默认 RLock,排队场景 RFairLock,读多写少 RReadWriteLock。
⚠️ 常见误区
详情
常见误区:
- ❌ "指定了 leaseTime 也会有看门狗续期" → 显式指定过期时间时看门狗不启动,锁到期必然释放;只有无参
lock()才启用看门狗。 - ❌ "Redisson 解锁失败顶多抛个异常,没实际影响" → 抛
IllegalMonitorStateException意味着锁已不属于你(可能已过期易主),此时临界区可能已被他人执行,必须按"锁失效"处理而非忽略异常。 - ❌ "Redisson 锁天然可重入,跨线程跨进程都行" → 可重入依赖"客户端 ID + 线程 ID"标识,跨线程、跨进程均不可重入。
🔀 发散问题
Redis 分布式锁的可重入具体怎么实现?
Hash 结构 + Lua 脚本:field 为客户端 ID:线程 ID,value 为重入次数,加锁递增、解锁递减到 0 删除,详见本文档『Redis 分布式锁如何实现可重入?』。
看门狗 30 秒的默认值怎么调?
通过 Config#lockWatchdogTimeout 全局配置;原则是覆盖 P99 业务时长且不至于宕机后锁阻塞太久,一般与业务 P99 的 3~5 倍匹配。
【困难】分布式锁如何进行技术选型?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 分布式锁选型
💎 关键结论
选型先问一句:这把锁是保效率还是保正确?保效率选 Redis(性能最高、Redisson 生态成熟),保正确选 ZooKeeper(可靠性最高、Curator 成熟),数据库锁性能最差一般不用。RedLock 复杂度高收益存疑,基本出局。
⚡记忆卡片
- 口诀:效率 Redis、正确 ZK、数据库锁别碰它
- 关键词:数据库 / Redis / ZooKeeper / 性能 / 可靠性 / 效率锁与正确锁
- 链路:明确锁用途(效率/正确) → 对比三方案性能与可靠性 → 效率选 Redis+Redisson、正确选 ZK+Curator → 临界区幂等兜底极端场景
📖 核心知识
下面是主流分布式锁技术方案的对比,可以在技术选型时作为参考:
| 数据库 | Redis | ZooKeeper | |
|---|---|---|---|
| 要点 | 1. 维护一张锁表,为锁的唯一标识字段添加唯一性约束。 2. 只要 insert 成功,即视为加锁成功。 | set lockKey randomValue NX PX/EX time 当且仅当 key 不存在时才可以写入,并且设定超时时间,以避免死锁。 | 加锁本质上是在 zk 中指定目录创建顺序临时接节点,序号最小即加锁成功。节点删除时,有监听通知机制告知申请锁的线程。 |
| 难度 | 实现简单、易于理解 | 较为简单,但要使其更可靠,需要有一些完善策略 | 应用简单,但 zk 内部机制并不简单 |
| 性能 | 性能最差,易成为瓶颈 | 性能最高 | 性能弱于 Redis |
| 可靠性 | 有锁表的风险 | 较为可靠(需要一些完善策略) | 可靠性最高 |
| 适用场景 | 一般不采用 | 适用于高并发的场景 | 适用于要求可靠,但并发量不高的场景 |
| 开源实现 | 无 | Redisson | Apache Curator |
选型口诀总结:
- 效率锁(防重复计算/防并发浪费):Redis 单节点 / 主从 + Redisson,性能优先,偶发失效可接受。
- 正确锁(防数据损坏):ZooKeeper / etcd(基于共识算法),可靠性优先。
- 不推荐:数据库锁(性能瓶颈)、RedLock(复杂度高且收益存疑)。
🔬 扩展知识
详情
【L3】量化对比(同机房量级,供推演参考):Redis 加解锁单操作约 0.11ms,单机可达 10 万级 QPS;ZK 加解锁涉及写 Leader + 多数派同步,单操作约 110ms,单集群通常数千级 QPS;数据库锁受制于 insert/delete 的事务开销,通常仅数百至数千 QPS 且易拖垮业务库。选型时先用并发量筛掉数据库。
【L4】除了三大件,etcd 也是正确性场景的有力候选:基于 Raft、提供 Lease(租约)+ Revision(天然 fencing token)+ Watch,Go 生态首选。面试中被问到"为什么 etcd 比 ZK 更适合做锁"时,可从 Lease 自动续期与 Revision 单调递增(fencing 友好)两个角度回答。
🏭 实战场景
详情
推演场景(虚构推演,用于展示选型决策过程):某电商中台有三类并发控制需求——① 营销优惠券批量发放防重(高并发,错了可人工修复);② 库存扣减临界区(正确性要求极高);③ 每日对账任务防多实例重复执行。推演决策:① 选 Redis 锁 + Redisson,QPS 万级、偶发失效由优惠券幂等表兜底;② 选 ZooKeeper 锁(Curator InterProcessMutex),配合库存行级条件更新双保险;③ 选 Redis 锁即可,重复执行的对账本身幂等。结论:同一系统内按"效率/正确"分层用不同的锁,而不是全系统统一一种。
⚠️ 常见误区
详情
常见误区:
- ❌ "ZK 锁可靠性最高,所以所有场景都用 ZK 锁" → ZK 写走 Leader、吞吐有限,高并发效率场景用 ZK 会把集群打挂;锁选型必须匹配并发量级。
- ❌ "数据库锁最简单,先凑合用" → 锁表 insert/delete 占用业务库连接与事务,高峰期极易成为瓶颈并放大故障;它只适合低频、临时的互斥需求。
- ❌ "选了最可靠的锁就不需要幂等了" → 任何分布式锁在极端场景(客户端假死、网络分区)下都可能失效,正确性场景仍需临界区自身的幂等/条件更新兜底。
🔀 发散问题
数据库锁的致命问题有哪些?
死锁(释放失败锁记录残留)、非阻塞(insert 失败即报错、无排队)、非重入、数据库单点,且逐一修补会让方案越来越复杂,详见本文档『数据库分布式锁的工作原理是什么?』。
ZK 锁的性能瓶颈具体在哪?
加锁解锁都是 ZK 写操作,全部由 Leader 处理并同步到 Follower,吞吐受限于单 Leader,详见本文档『ZooKeeper 分布式锁的工作原理是什么?』。
效率锁和正确锁的划分依据是什么?
看失效后果:失效只造成重复计算/资源浪费属效率锁;失效会造成数据损坏/资损属正确锁,详见本文档『RedLock 算法有什么争议?Martin Kleppmann 和 Antirez 的争论核心是什么?』的实践结论。
【困难】RedLock 算法有什么争议?Martin Kleppmann 和 Antirez 的争论核心是什么?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / RedLock 争议
💎 关键结论
这场争论的本质是"锁的正确性该由谁保证"。Kleppmann 指出 RedLock 依赖不可靠的时钟和进程不暂停的假设,GC 一停顿锁就可能双持;Antirez 反驳称合理配置下可接受。最终共识:锁只保效率,正确性要靠 fencing token 或共识系统。
⚡记忆卡片
- 口诀:时钟不可信、GC 会暂停、token 来兜底
- 关键词:时钟可靠性 / NPC / fencing token / 效率锁与正确锁
- 链路:RedLock 依赖本地时钟算 TTL → GC 停顿期间锁已过期 → 旧持有者恢复后双持锁 → 资源侧需 fencing token 拒绝旧写入 → 或改用共识系统
📖 核心知识
RedLock 是 Redis 作者 Antirez 提出的多节点分布式锁算法。但《数据密集型应用系统设计》作者 Martin Kleppmann 在博客中对其提出了严厉批评,引发了一场著名的争论。
争论核心 1:时钟可靠性问题
- Kleppmann 的批评:RedLock 依赖各节点的本地时钟来计算锁的有效期。但分布式系统的时钟不可靠(时钟漂移、NTP 调整、GC 暂停),可能导致:
- 客户端 GC 暂停期间,锁已过期,其他客户端获取锁,出现两个客户端同时持锁。
- 时钟回拨,导致有效期计算错误。
- Antirez 的回应:RedLock 对时钟的依赖是"合理"的,且建议用户配置合理的 TTL 和 NTP。但 Kleppmann 认为这只是在"赌"时钟不会出问题。
争论核心 2:NPC 问题(Network Delay、Process Pause、Clock Drift)
RedLock 在以下情况下无法保证安全性:
- N(网络延迟):加锁请求因网络延迟导致耗时超过 TTL。
- P(进程暂停):GC Stop-The-World 期间锁过期。
- C(时钟漂移):不同节点时钟不同步,导致 TTL 计算错误。
争论核心 3:fencing token 问题
- Kleppmann 的建议:分布式锁应配合 fencing token(单调递增的令牌)。每次获取锁时返回一个递增的 token,客户端在访问存储时带上 token,存储层拒绝旧 token 的写入。这样即使锁过期,旧客户端的写入也会被拒绝。
- Antirez 的回应:RedLock 场景下,存储层(如 Redis)不一定支持 fencing token,且这增加了复杂度。
实践结论:
| 场景 | 推荐方案 |
|---|---|
| 对正确性要求极高 | ZooKeeper / etcd(基于共识算法,CP) |
| 对性能要求高,可容忍极端情况下的错误 | Redis 单节点锁 + Redisson 看门狗 |
| 多节点 RedLock | 不推荐,复杂度高且收益有限 |
总结来说:
- 如果锁用于效率优化(避免重复计算),偶尔失效可接受 → Redis 锁足够。
- 如果锁用于正确性保证(避免数据损坏),必须绝对可靠 → ZooKeeper / etcd。
- 没有任何分布式锁能保证 100% 安全,应根据业务场景选择合适的方案。
🔬 扩展知识
详情
【L3】Kleppmann 给出的经典反例值得完整复述:客户端 1 拿锁后进入长 GC,锁 TTL 过期;客户端 2 拿锁开始写存储;客户端 1 GC 结束,以为自己还持锁继续写,覆盖客户端 2 的数据。整个过程中锁服务没有任何异常——这说明问题不在锁的实现,而在"基于超时的锁"这一范式本身无法防御 Process Pause。
【L4】etcd 的 Lease + Revision 组合是当前 fencing 最友好的工程实现:租约到期自动释放(免 TTL 手动管理),全局递增的 Revision 天然就是 fencing token,资源侧可直接比较。这解释了为什么新系统做正确性互斥时越来越倾向 etcd 而非 RedLock。
⚠️ 常见误区
详情
常见误区:
- ❌ "争论的结论是 RedLock 有 bug,修复后就能用" → 争论针对的是范式而非实现:依赖超时 + 本地时钟的锁无法在理论上证明安全性,修 bug 解决不了。
- ❌ "fencing token 就是锁服务发个唯一 ID 就行" → token 必须单调递增且资源侧有能力比较并拒绝旧值;用 UUID 这种无序标识做不了 fencing。
- ❌ "Antirez 输了,所以 Redis 锁一无是处" → 双方都承认 Redis 锁在效率场景(防重复计算、缓存击穿)完全够用;被否定的只是用它做正确性保证。
🔀 发散问题
为什么说"效率锁"和"正确锁"是两种需求?
效率锁失效只损失一些重复计算,可接受;正确锁失效会造成数据损坏,不可接受。两者的失效代价不同,决定了能否容忍锁的概率性失效,详见本文档『分布式锁如何进行技术选型?』。
Redis 锁如何自己实现 fencing?
加锁成功后用 INCR 获取递增 token,写临界区资源时携带 token,资源侧拒绝小于已见最大 token 的写入,详见本文档『Redis 分布式锁的工作原理是什么?』的发散问题。
【中等】Redis 分布式锁如何实现可重入?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 可重入锁
💎 关键结论
可重入的关键是把锁从 String 换成 Hash:field 存"客户端 ID:线程 ID",value 存重入次数。加锁时存在则递增计数,解锁时递减到 0 才真正删除,全程用 Lua 保证原子性。Redisson 的 RLock 就是这个原理。
⚡记忆卡片
- 口诀:Hash 记身份、计数表重入、归零才删锁
- 关键词:Hash 结构 / 客户端 ID:线程 ID / 重入次数 / Lua 原子
- 链路:锁不存在则 hset 计数 1 → 锁属于自己则 hincrby +1 → 解锁时 hincrby -1 → 计数归 0 才 del → 每步都用 Lua 保证原子
📖 核心知识
可重入锁是指:同一个线程在持有锁的情况下,可以再次获取该锁而不会被阻塞。Redis 分布式锁可通过 Hash 结构 + Lua 脚本 实现可重入。
实现原理:
使用 Hash 结构存储锁信息,key 为锁名,field 为客户端 ID + 线程 ID,value 为重入次数。
加锁 Lua 脚本:
-- KEYS[1] = 锁名, ARGV[1] = 客户端ID:线程ID, ARGV[2] = 过期时间
if redis.call('exists', KEYS[1]) == 0 then
-- 锁不存在,首次加锁
redis.call('hset', KEYS[1], ARGV[1], 1)
redis.call('pexpire', KEYS[1], ARGV[2])
return nil
elseif redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
-- 锁存在且属于当前线程,重入次数 +1
redis.call('hincrby', KEYS[1], ARGV[1], 1)
redis.call('pexpire', KEYS[1], ARGV[2])
return nil
else
-- 锁被其他线程持有,加锁失败
return redis.call('pttl', KEYS[1])
end解锁 Lua 脚本:
-- KEYS[1] = 锁名, ARGV[1] = 客户端ID:线程ID
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
return nil -- 锁不属于当前线程
end
local count = redis.call('hincrby', KEYS[1], ARGV[1], -1)
if count > 0 then
-- 重入次数仍大于 0,刷新过期时间
redis.call('pexpire', KEYS[1], ARGV[2])
return 0
else
-- 重入次数归 0,删除锁
redis.call('del', KEYS[1])
return 1
end注意事项:
- 跨进程不可重入:可重入性依赖于"客户端 ID + 线程 ID"标识,跨进程(JVM)无法重入。
- 续期与重入:看门狗续期时,需同时考虑重入次数,不能简单延长 TTL。
- 锁泄漏:如果业务异常导致解锁未执行(如未用 try-finally),重入次数不会归零,锁会直到 TTL 过期才释放。
🔬 扩展知识
详情
【L3】Redisson 的 RLock 就是基于上述原理实现的,核心要点:
- 使用 Hash 结构(而非 String),field 为
UUID:threadId,value 为重入次数。 - 加锁和解锁均通过 Lua 脚本保证原子性。
- 看门狗续期时,同样校验锁归属,避免为已释放的锁续期。
- 支持
tryLock(timeout)带超时的加锁,通过 Redis 的 Pub/Sub 订阅锁释放消息,避免空转。
【L4】加锁失败时返回 pttl 是个精妙设计:等待方拿到锁的剩余存活时间,可以精确地"只等这么久再重试",避免盲目轮询;Redisson 的订阅机制则更进一步,直接等释放通知,连这次等待也省了。
⚠️ 常见误区
详情
常见误区:
- ❌ "String 类型的 value 也能存重入次数(如
id:count)" → 需要同时维护标识与计数并原子更新,拆分解析会引入竞态;Hash 结构让 field/value 语义分离,是 Redisson 的标准做法。 - ❌ "重入次数归零前锁不会过期,很安全" → 锁泄漏场景(未走 unlock 分支)下计数永不归零,只能靠 TTL 兜底释放;解锁必须放在 try-finally 中。
- ❌ "同一线程跨 JVM 也能重入" → 重入标识是"客户端 ID + 线程 ID",进程边界内有效,跨 JVM 视为不同持有者。
🔀 发散问题
看门狗续期如何与重入次数配合?
续期脚本先用 hexists 校验锁仍归属自己再 pexpire,不关心具体计数;若锁已被释放或易主,续期返回 0,避免为别人的锁续命,详见本文档『Redssion 分布式锁的工作原理是什么?』。
可重入锁会有性能问题吗?
每次重入都要一次 Redis 往返(hincrby),深度递归加锁会有累积开销;常规业务重入深度为 1~2,可忽略。
分布式 ID
【中等】有哪些生成分布式 ID 的方式?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / 分布式 ID
💎 关键结论
分布式 ID 五大流派:UUID(本地生成但无序过长)、数据库自增(有序但单点慢)、号段模式(批量取号,主流)、原子计数器(Redis INCR / ZK 顺序节点)、雪花算法(时间戳 + 机器位 + 序列号,高性能趋势递增)。选型就看四个词:唯一、有序、性能、可用。
⚡记忆卡片
- 口诀:UUID 乱、自增慢、号段批、雪花快
- 关键词:UUID / 数据库自增 / 号段 / 原子计数器 / Snowflake
- 链路:分布式下需全局唯一 ID → 本地生成(UUID)快但无序 → 中心化(DB/Redis)有序但有瓶颈 → 批量取号与位拼接(号段/雪花)兼顾性能与趋势递增
📖 核心知识
生成分布式 ID 主要有以下方式:
- UUID:UUID 是通用唯一识别码(Universally Unique Identifier)的缩写,是一种 128 位的标识符,用 16 进制表示,需要 32 个字符。UUID 会根据运行应用的计算机网卡 MAC 地址、时间戳、命令空间等元素,通过一定的随机算法产生。
- UUID 存在 5 个版本。
- UUID 不保证全局唯一性,我们需要小心 ID 冲突(尽管这种可能性很小)。
- 优点:实现简单、生成速度较快(本地生成,不依赖其他服务)。
- 缺点:无序、长度过长、不安全(基于 MAC 地址生成 UUID 的算法,可能会造成 MAC 地址泄露)。
- 数据库自增主键:大多数数据库都支持自增主键。基于此特性,可以利用事务管理控制生成唯一 ID。
- 优点:实现简单、有序、长度较小
- 缺点:性能差、存在单点问题、不安全(可以通过 ID 递增规律推算出数据量)
- 数据库号段:一次批量生成一个 segment(号段),号段的大小由 step(步长)控制。用完之后再去数据库获取新的号段。
- 原子计数器:利用一些 NoSQL 数据库提供的原子性计数器,来实现分布式 ID。
- Redis
incr/incrby:Redis 的 String 类型提供INCR和INCRBY命令将 key 中储存的数字原子递增。- 优点:高性能、有序
- 缺点:和数据库自增序列方案的缺点类似
- ZooKeeper 顺序节点:利用 ZooKeeper 数据模型中的顺序节点作为分布式 ID。
- 优点:简单、可靠性高
- 缺点:性能不高
- Redis
- Snowflake(雪花算法):Snowflake ID 生成过程包含多个组件:时间戳、机器 ID 和序列号。第一位未使用,以确保 ID 正确。此生成器不需要通过网络与 ID 生成器通信,因此速度快且可扩展。Snowflake 的实现各不相同。例如,可以将数据中心 ID 添加到"MachineID"组件中,以保证全局唯一性。
🔬 扩展知识
详情
【L3】"无序 ID 会拖垮数据库"是被反复验证的工程事实:InnoDB 主键索引按主键有序组织,UUID 这类随机主键会导致插入时频繁页分裂、缓冲池命中率下降,大表下写入性能可能下降数倍。这就是"趋势递增"成为生产 ID 硬性要求的原因。
【L4】号段模式与雪花是两条不同路线:号段是"中心化批量预取"(依赖 DB 但取号频率低),雪花是"本地位拼接"(零依赖但依赖时钟)。Leaf 的巧妙之处在于两种模式都做了工程级实现,按业务取用,详见本文档『Leaf 和 Tinyid 的原理是什么?各有什么特点?』。
趋势递增而非严格递增是有意取舍:严格递增意味着全局单点,趋势递增允许各节点并行生成,兼顾顺序性与吞吐。
【L3】"可推测性"常被忽略但很关键:连续自增 ID 暴露在 API 中,竞对遍历一遍就能拉出全量数据、推算业务量。所以对外暴露的订单号要么用雪花类(机器位打乱规律),要么在自增 ID 之外另配随机化的业务单号。
【L4】分库分表场景的隐性要求:ID 生成必须独立于分片键,否则"先分片后取号"会死锁。实践上先全局取号、再以号决定路由(号段取模)是常见组合;ShardingSphere 的 key-generator SPI 即为此设计。
选型建议:
- 订单号 / 支付流水号:雪花算法或 Leaf-Snowflake(不可推测、趋势递增、高性能)。
- 分库分表主键:雪花算法或 Leaf-Segment(全局唯一、对索引友好)。
- 链路追踪 TraceId:UUID 或雪花算法变种(仅需唯一、本地生成)。
- 用户 ID / 商品 ID:Leaf-Segment 或数据库号段(趋势递增、可读性好)。
方案对比表:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| UUID | 生成 128 位随机字符串 | 本地生成,无网络开销;全局唯一 | 无序,B+ 树插入性能差;占用空间大(36 字符) | 对顺序无要求的场景 |
| 数据库自增 | 利用 AUTO_INCREMENT | 简单,有序 | 单点瓶颈;扩展困难 | 单库小规模应用 |
| 数据库号段 | 批量从 DB 取一段 ID 缓存到本地 | 减少 DB 压力;有序 | 重启会浪费一段 ID;需保证 DB 高可用 | 中大规模应用(如美团 Leaf) |
| 雪花算法 | 时间戳 + 机器 ID + 序列号 | 有序;高性能;无依赖 | 时钟回拨问题;机器 ID 分配需管理 | 大规模分布式系统 |
| Redis INCR | 利用 Redis 原子自增 | 简单;有序 | 依赖 Redis;有网络开销 | 对性能要求不极致的场景 |
| ZooKeeper 序列节点 | 创建顺序节点获取序号 | 强一致 | 性能差;依赖 ZK | 对一致性要求高、并发低的场景 |
| Leaf(美团) | 号段模式 + Snowflake 双缓冲 | 高性能;高可用 | 实现复杂 | 超大规模应用 |
📚 延伸阅读:如果再有人问你分布式 ID,这篇文章丢给他 | 理解分布式 id 生成算法 SnowFlake | Leaf——美团点评分布式 ID 生成系统 | UUID 规范 | 百度分布式 ID | ShardingSphere 分布式主键
⚠️ 常见误区
详情
常见误区:
- ❌ "UUID 绝对唯一,不需要考虑冲突" → UUID 靠概率保证唯一(128 位随机空间),不保证绝对不冲突,作为主键仍需唯一约束兜底。
- ❌ "Redis INCR 做 ID 又快又简单,直接生产用" → Redis 重启(RDB/AOF 恢复不及时)或主从切换可能让计数回退产生重复 ID,且 ID 规律可被推测,敏感业务需评估。
- ❌ "ID 只要能唯一就行,有序无序无所谓" → 作为数据库主键时,无序 ID 会引起页分裂与索引膨胀,直接影响写入性能与存储成本。
- ❌ "性能要求高就一定选雪花" → 若机器时钟管理混乱(频繁回拨、容器漂移),号段模式反而更稳;选型要看运维能力而非单点性能指标。
- ❌ "UUID 做 TraceId 没问题,那做订单号也行" → TraceId 不入索引、不对外;订单号需要趋势递增(索引友好)+ 不可推测(安全),UUID 两条都不满足。
🔀 发散问题
雪花算法为什么能做到"不依赖网络还趋势递增"?
ID 高位是时间戳,时间天然递增,所以整体趋势递增;机器位和序列号保证同毫秒内的唯一性,全程本地计算无需协调,详见本文档『雪花算法的原理是什么?时钟回拨如何处理?』。
为什么订单号不能直接用数据库自增 ID?
自增 ID 会暴露业务量(竞对爬一下就能推算日单量),且分库分表后无法全局唯一,生产上通常用雪花类 ID 对外。
【困难】雪花算法的原理是什么?时钟回拨如何处理?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式协同 / 雪花算法
💎 关键结论
雪花算法把 64 bit 拆成 1 符号位 + 41 位毫秒时间戳 + 10 位机器 ID + 12 位序列号,本地拼接即可生成趋势递增的全局唯一 ID,单机每毫秒可发 4096 个。阿喀琉斯之踵是时钟回拨:小回拨等一等,大回拨拒绝服务,生产上推荐 Leaf 用 ZK 持久化时间戳做校验。
⚡记忆卡片
- 口诀:1-41-10-12,回拨小等大拒
- 关键词:64 bit / 时间戳 41 / 机器 ID 10 / 序列号 12 / 时钟回拨
- 链路:时间戳左移拼接机器位与序列号 → 同毫秒靠序列号区分 → NTP 校时引起回拨 → lastTimestamp > 当前时间 → 等待 / 拒绝 / 扩展位 / ZK 校验兜底
📖 核心知识
雪花算法(Snowflake) 是 Twitter 公布的分布式 ID 生成算法,生成一个 64 bit 的整数,结构如下:
| 1 bit | 41 bit | 10 bit | 12 bit |
|---|---|---|---|
| 符号位 | 时间戳(毫秒) | 工作机器 ID | 序列号 |
- 符号位(1 bit):恒为 0,保证 ID 为正数。
- 时间戳(41 bit):当前时间相对于起始时间的毫秒差。可用约 69 年(2^41 / (365×24×3600×1000) ≈ 69.73 年)。
- 工作机器 ID(10 bit):可支持 1024 个节点。可根据业务拆分为数据中心 ID + 机器 ID(如 5 + 5)。
- 序列号(12 bit):同一毫秒内的自增序列,单机每毫秒最多生成 4096 个 ID。
时钟回拨问题:
雪花算法强依赖机器时钟。如果机器时间发生回拨(如 NTP 同步、人工调整),lastTimestamp 会大于当前时间,导致:
- 生成的 ID 时间戳变小,可能与之前生成的 ID 重复。
- 序列号被重置,加剧重复风险。
时钟回拨的处理策略:
| 策略 | 原理 | 适用场景 |
|---|---|---|
| 等待追上 | 回拨时间短(如 < 5ms),线程等待时钟追上 | NTP 微调、时钟抖动 |
| 拒绝服务 | 回拨时间超过阈值,抛异常拒绝生成 | 对唯一性要求严格的场景 |
| 扩展位兜底 | 利用未使用的 bit 位(如借用机器 ID 位)记录回拨次数 | 长期回拨的场景 |
| 本地时间戳 | 不依赖系统时钟,用本地维护的逻辑时钟(单调递增) | 对时钟绝对信任度低的场景 |
| Leaf-snowflake | 美团 Leaf 方案:用 ZooKeeper 持久化每个节点的最后时间戳,启动时校验 | 生产级方案 |
🔬 扩展知识
详情
【L3】美团 Leaf 的 Snowflake 模式通过 ZooKeeper 解决时钟回拨:
- 每个应用启动时,在 ZK 上创建临时顺序节点,获取 workerId。
- 同时持久化该 workerId 上一次生成 ID 的时间戳到 ZK。
- 启动时对比 ZK 上的时间戳与本地时间,如果本地时间 < ZK 时间戳,说明发生回拨,拒绝启动并报警。
- 运行时定期更新 ZK 上的时间戳,作为下次启动的基准。
【L4】位宽分配可以按业务定制:机器数超过 1024 时可压缩序列号位(牺牲单机每毫秒吞吐),或把起始时间戳设为上线日延后 69 年大限;百度 UidGenerator 则改用"未来时间 + RingBuffer 预生成"思路,把时间位变成启动时刻的相对秒数,摆脱对实时时钟的逐毫秒依赖。
🏭 实战场景
详情
推演场景(虚构推演):某支付系统上线 2 年后偶发"订单号重复"告警,排查发现当天凌晨 NTP 把两台发号机器时钟回拨了 800ms,两台机器各自生成了相同时间戳 + 相同序列号的 ID。推演复盘:① 回拨 800ms 远超"等待追上"阈值,朴素实现的 lastTimestamp 校验只防本机回拨、防不了"两台机器同时回拨到相同值";② 正确做法:机器 ID 分配必须互斥(ZK 注册),回拨超过阈值(如 5ms)立即拒绝发号并告警,同时订单号落库有唯一索引兜底。量化参考:12 位序列号支持单机每毫秒 4096 个 ID,41 位时间戳约可用 69 年(自纪元起)。
⚠️ 常见误区
详情
常见误区:
- ❌ "雪花算法保证全局唯一,不需要唯一索引" → 时钟回拨、机器 ID 重复分配都会产生重复 ID,DB 唯一索引是最后一道防线。
- ❌ "时钟回拨只会影响回拨那台机器" → 若多台机器同时被 NTP 回拨到相同时间区间,且机器位分配有重叠,就会产生跨机重复。
- ❌ "序列号用完了可以借下一秒的时间戳继续发" → 常见实现确实"借用下一毫秒",但这会让 ID 时间位与实际时间偏离,高并发持续打满时偏差累积,需监控序列号耗尽频率。
🔀 发散问题
雪花算法的 ID 可以反解出信息吗?
可以:右移 22 位 + 纪元起始时间得到生成时刻,中间 10 位得机器 ID。这在排查"哪个节点什么时间发的 ID"时非常有用,但也意味着 ID 会暴露机器拓扑。
Leaf 为什么还要做号段模式?
雪花强依赖时钟且趋势递增有毫秒级乱序窗口,号段模式严格依赖 DB 但实现更简单、运维可视化;Leaf 两种都提供,按业务选,详见本文档『Leaf 和 Tinyid 的原理是什么?各有什么特点?』。
【中等】Leaf 和 Tinyid 的原理是什么?各有什么特点?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式协同 / Leaf 与 Tinyid
💎 关键结论
Leaf 和 Tinyid 都是"数据库号段"路线的生产级实现:一次从 DB 批取一段 ID 缓存在内存慢慢用。Leaf 的亮点是双 Buffer 无缝切换 + 额外提供雪花模式,Tinyid 的亮点是多 DB 分摊发号压力。本质上都是"把 DB 访问频率摊薄到 1/步长"。
⚡记忆卡片
- 口诀:号段批取内存发,双 Buffer 不断档
- 关键词:Leaf / Tinyid / 双 Buffer / 多 DB / step 步长
- 链路:DB 按 step 发放号段 → 应用内存本地发号 → 用到阈值异步预取下一段 → DB 短暂不可用仍可继续发号
📖 核心知识
Leaf 是美团开源的分布式 ID 生成系统,支持两种模式:
1. Leaf-Segment(号段模式)
- 基于数据库号段方案,每次从数据库批量获取一段 ID(如 1-2000),缓存在本地内存。
- 双 Buffer 优化:维护两个号段 Buffer,当前 Buffer 使用到一定比例(如 20%)时,异步加载下一个 Buffer。当当前 Buffer 耗尽,无缝切换到下一个 Buffer,避免阻塞。
- 优点:高性能(内存发号)、ID 趋势递增、对数据库压力小。
- 缺点:ID 可被推测(暴露业务量);依赖数据库可用性。
2. Leaf-Snowflake(雪花模式)
- 基于雪花算法,通过 ZooKeeper 分配和持久化 workerId。
- 解决了原始雪花算法的时钟回拨问题(如上所述)。
- 优点:不依赖数据库、ID 不可推测。
- 缺点:依赖 ZooKeeper;强依赖时钟。
Tinyid 是滴滴开源的分布式 ID 生成系统,原理与 Leaf-Segment 类似:
- 基于数据库号段模式,支持多 DB 高可用。
- 提供 HTTP 和 SDK 两种接入方式。
- 特点:支持多 DB 号段分配(通过
db_type区分),增加发号能力;支持动态调整步长。
Leaf 与 Tinyid 对比:
| 特性 | Leaf-Segment | Tinyid |
|---|---|---|
| 开源方 | 美团 | 滴滴 |
| 核心原理 | 数据库号段 + 双 Buffer | 数据库号段 + 多 DB |
| 高可用 | DB 主从 + MHA | 多 DB 路由 |
| 接入方式 | HTTP / SDK | HTTP / SDK |
| 雪花模式 | 支持(Leaf-Snowflake) | 不支持 |
| 社区活跃 | 较活跃 | 较少更新 |
🔬 扩展知识
详情
【L3】双 Buffer 的价值在"发号不阻塞":单 Buffer 方案在号段耗尽的瞬间必须同步访问 DB 取新段,这一瞬间所有发号请求排队等 DB;双 Buffer 把 DB 访问提前到 20% 水位异步完成,耗尽时只是指针切换。这和高并发缓存的"预加载"是同一思想。
【L4】Tinyid 的多 DB 方案细节:多个 DB 实例各自维护不同奇偶 / 区间的号段(如 db1 发奇数、db2 发偶数,或按业务线分段),客户端轮询取段。好处是发号能力随 DB 数量线性扩展,代价是 ID 不再全局严格连续(跨 DB 交错)。
⚠️ 常见误区
详情
常见误区:
- ❌ "号段模式下 DB 挂了立刻就不能发号了" → 双 Buffer / 已取未用的号段仍在内存,DB 短暂故障期间可以继续发号,故障窗口 ≈ 剩余号段可支撑时长。
- ❌ "Leaf-Snowflake 用 ZK 就不需要关心时钟回拨了" → ZK 解决的是启动时校验与 workerId 分配,运行中的大幅回拨仍需拒绝服务策略配合。
🔀 发散问题
号段的 step 怎么定?
原则是"内存号段能撑过 DB 一次故障恢复时长",常见取每分钟发号量的数倍;step 太小 DB 压力大,太大则故障后号段浪费多。
为什么不直接每台机器缓存自增,完全脱离 DB?
机器间需要协调起点与步长(否则重复),协调本身又回到中心化问题;号段模式正是"少量协调 + 大量本地发号"的折中,详见本文档『有哪些生成分布式 ID 的方式?』。
参考资料
- 数据密集型应用系统设计 - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】