阶段:分布式与高并发篇 | 建议时长:5 天 | 前置:05-中间件篇(Redis、ZooKeeper、RocketMQ)
学习目标
- 讲清”单机 → 分布式”的动机:不是追新技术,而是单机在性能、容量、可用性上撞墙后的必然选择;
- 能结合注册中心(ZooKeeper/Nacos)讲 CAP 定理与 CP/AP 取舍,说清 BASE 理论与强/最终一致性的区别 ⭐;
- 讲清一致性哈希为什么能让节点扩缩容时只迁移少量数据(哈希环 + 虚拟节点)⭐;
- 掌握分布式 ID 四种方案,吃透雪花算法 64 位结构与时钟回拨处理 ⭐;
- 从实现、优缺点、选型三个维度对比数据库/Redis/ZooKeeper 三种分布式锁 ⭐⭐;
- 理解 2PC、TCC、本地消息表、RocketMQ 事务消息、Seata AT 的原理与适用场景,会做选型 ⭐⭐;
- 说出一次 RPC 调用的完整链路(动态代理 → 序列化 → 网络传输 → 反序列化 → 反射调用)。
核心知识点
1. 为什么需要分布式
单机架构的三堵墙,每一堵都对应一个”分布式化”动作:
| 单机瓶颈 | 具体表现 | 对应解法 |
|---|---|---|
| 性能墙 | 单机 QPS/连接数有上限,CPU 与内存物理封顶 | 横向加机器 + 负载均衡 |
| 容量墙 | 单表几千万行后索引膨胀、单机磁盘放不下 | 分库分表、数据分片 |
| 可用性墙 | 单机故障 = 全站故障,发布即停服 | 多副本 + 故障转移(Redis/MySQL 主从、服务多实例) |
代价同样明确:数据一致性变难、运维复杂度上升、排查链路变长。所以原则是”先优化单机与架构,能不分布式就不分布式”,这与下一篇的高并发演进路径是同一条主线。
2. CAP 定理与取舍 ⭐
- C(Consistency)一致性:所有节点在同一时刻读到相同的数据;
- A(Availability)可用性:每个请求都能得到(非错误的)响应;
- P(Partition tolerance)分区容错性:网络分区(节点间断连)发生时系统仍能工作。
三个关键结论:
- 分布式系统中网络分区必然发生,P 必选,所以真实选择是 CP 还是 AP;
- 分区发生时:选 CP 就拒绝部分请求保数据一致(分布式锁、选主场景);选 AP 就返回可能旧的数据保可用(服务发现场景);
- 分区恢复后通过同步、补偿达到最终一致。
注册中心的取舍(呼应 05-中间件篇):
| 注册中心 | 倾向 | 具体表现 |
|---|---|---|
| ZooKeeper | CP | 主节点宕机选举期间(可达 30~120 秒)服务不可用,宁停服不脏读 |
| Eureka | AP | 节点对等,任一节点可用即可完成注册发现,可能读到旧实例列表 |
| Nacos | AP/CP 可切换 | 临时实例走 Distro 协议(AP),持久实例走 Raft(CP),默认 AP |
BASE 理论:选 AP 时的工程化落地方案 —— BA(Basically Available,基本可用:降级、限流)、S(Soft State,软状态:允许中间态,如”支付处理中”)、E(Eventually Consistent,最终一致)。
一致性级别:
- 强一致:写完任意节点后,立即可读到最新值,代价高、性能差;
- 最终一致:允许短暂不一致,经过一段时间同步后保证一致,互联网业务主流选择;
- 两者之间还有”读己之写""单调读”等会话级保证,面试提一句即可。
3. 一致性哈希 ⭐
普通 Hash 取模的问题:hash(key) % N,N 从 3 变 4 时绝大多数 key 的落点都会变,缓存几乎全部失效——这就是缓存集群扩容的经典痛点。
哈希环方案:
- 把 2^32 的哈希空间想成一个环,节点(按 IP/主机名 hash)与数据(按 key hash)都映射到环上;
- 数据沿顺时针找到第一个节点存取;
- 增删节点时,只有”该节点逆时针方向到上一个节点之间”的数据需要迁移(约 1/N),其余全部不动。
数据倾斜与虚拟节点:节点少时在环上分布不均,可能大部分数据落到同一节点。给每个物理节点生成 100~200 个虚拟节点散布在环上,数据先落在虚拟节点再映射回物理节点,均匀性大幅提升。
应用:分布式缓存节点分布、负载均衡(Dubbo 的一致性哈希策略)、分库分表备选方案。注意:Redis Cluster 用的不是一致性哈希,而是 16384 个 hash slot,面试要能区分这两者。
4. 分布式 ID ⭐
分库分表后自增主键只能保证单表唯一,必须引入全局 ID。核心诉求:全局唯一、趋势递增(利于 B+ 树顺序插入)、信息安全和性能可用性。
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| UUID | 128 位随机数 | 本地生成、无依赖、性能高 | 无序导致 B+ 树页分裂、太长(36 字符)、无业务含义 |
| 数据库号段 | 批量取一段号缓存在应用内存,用完再取 | 趋势递增、DB 压力小(一次取 1000 个) | 依赖 DB、应用重启浪费号段、多机房扩展难 |
| 雪花算法 | 64 位:时间戳 + 机器 ID + 序列号 | 本地生成、趋势递增、单机每秒 400 万+ | 依赖机器时钟(回拨问题)、workerId 需要分配 |
| Redis 自增 | INCR + 初始值/步长 | 简单、有序 | 强依赖 Redis 可用性,需持久化防重启丢数据 |
雪花算法结构(必背):
| 1 bit 符号位 | 41 bit 时间戳(毫秒级,可用约 69 年) | 10 bit 机器 ID(可拆 5+5 机房+机器,共 1024 台) | 12 bit 序列号(同一毫秒内 0~4095) |
时钟回拨问题:机器时间被 NTP 校准往回调后,同一毫秒可能被”重复使用”,生成重复 ID。处理思路:小幅度回拨(如 ≤5ms)等待追平;回拨过大直接抛异常告警,或切换备用 workerId。动手实践第 1 题要求实现。
5. 分布式锁方案对比 ⭐⭐
单机用 synchronized/ReentrantLock,应用多实例部署后 JVM 锁失效,需要跨进程互斥——典型如防重复下单、定时任务防多机并发执行。
方案一:数据库
- 唯一索引插入法:
INSERT INTO lock_table(lock_key) VALUES(?),插入成功即获锁,删除记录即释放; - 悲观锁法:
SELECT ... FOR UPDATE排他锁。 - 缺点:性能差(DB 是瓶颈)、无过期自动释放、宕机要自己处理死锁。仅适合低频场景或没有其他中间件时兜底。
方案二:Redis(主流)
- 雏形:
SET lockKey uniqueValue NX EX 30,一条命令原子完成”不存在才设置 + 过期时间”; - 三大坑:① 业务没执行完锁先过期 → Redisson 看门狗自动续期;② 误删他人锁 → value 存唯一标识,释放时 Lua 校验后再删;③ 不可重入 → Redisson 用 Hash 结构记录持有线程与重入次数;
- Redisson 关键点:加锁/解锁都是 Lua 脚本保证原子性;不传 leaseTime 时默认锁 30 秒,后台线程每 10 秒(1/3)检查并续期,业务没执行完锁不会失效。
RLock lock = redissonClient.getLock("order:lock:" + userId);
try {
// 不指定 leaseTime → 启用看门狗自动续期;第一个参数是最长等待时间
if (lock.tryLock(3, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
if (lock.isHeldByCurrentThread()) { // 只释放自己持有的锁
lock.unlock();
}
}
方案三:ZooKeeper
- 客户端在
/locks下创建临时顺序节点; - 判断自己是否是序号最小的节点:是则获锁;否则只 watch 前一个节点的删除事件(避免羊群效应);
- 释放 = 删除自己的节点;客户端宕机时临时节点自动消失,锁天然不会死锁;
- 代价:写请求要走 ZAB 过半提交,性能不如 Redis;过半机制的容灾换算(3 台容 1、4 台也只容 1)见《06-ZooKeeper与Nacos》。
对比与选型:
| 维度 | 数据库 | Redis | ZooKeeper |
|---|---|---|---|
| 性能 | 低 | 高(内存操作) | 中 |
| 可靠性 | 高(落库) | 中(主从切换瞬间可能丢锁) | 高(CP) |
| 实现复杂度 | 低 | 中(Redisson 已解决) | 中 |
| 自动过期 | 需自己实现 | 过期时间 + 看门狗续期 | 临时节点(会话断开即释放) |
| 适用场景 | 低频、无其他中间件 | 高并发业务锁(秒杀、防重) | 强一致要求(选主、元数据) |
一句话选型:业务高并发选 Redisson;强一致、不能出错选 ZK;没得选才用数据库。
Redlock 与它的争议(深挖加分项):Redis 官方提出 Redlock——向 N 个(通常 5 个)互相独立的 Redis 主节点依次加锁,过半数成功且总耗时小于锁有效期才算加锁成功,意在摆脱”单主从切换丢锁”的隐患。争议焦点(Kleppmann 的质疑):
- 依赖时钟:Redlock 靠各节点本地时钟判断锁是否过期;若某节点时钟跳变(NTP 校时、虚拟机迁移),锁可能在业务未完成时就失效。
- 进程暂停(GC/调度)无法被锁阻止:持锁进程暂停跨越了锁租期,醒来后仍会继续写共享资源——此时锁已被别人拿走,互斥被破坏。这不是 Redis 独有的问题,任何基于租期的锁都躲不掉,只能用 fencing token(每次获锁拿到单调递增的令牌,写资源时带上;存储端拒绝比已见令牌更小的写入)来兜底。
- 工程结论:对正确性要求极高的场景,别把安全性全押在分布式锁上——用 fencing token 或让下游操作幂等(如”更新前先校验版本号”)才是根本防线。因此 Redlock 在实践中用于”提高可用性、降低丢锁概率”,而不是”保证绝对互斥”。
6. 分布式事务方案与选型 ⭐⭐
跨库/跨服务的写操作无法用本地事务保证。先想清楚:能不用就不用——能合并到单库就合并,能靠业务流程避开的就避开。
2PC(两阶段提交)/3PC:协调者先问所有参与者”能否提交”(Prepare),全部同意再统一提交(Commit)。
- 问题:同步阻塞、协调者单点、第二阶段部分节点宕机会导致数据不一致;3PC 增加超时与 CanCommit 预询问缓解阻塞,但未根治。属于一致性协议层面的考点,工程上很少直接用。
TCC(Try-Confirm-Cancel):业务层的两阶段,每个参与方实现三个方法。
- Try:预留资源(冻结库存/冻结金额);Confirm:确认扣减;Cancel:释放预留;
- 三个经典问题:空回滚(Try 没到、Cancel 先到)、幂等(重试导致重复执行)、悬挂(Cancel 之后迟到的 Try 又落地);解决方式是事务状态表 + 幂等控制;
- 特点:不依赖数据库回滚、性能好,但业务侵入极大(每个接口要写三套逻辑),适合资金类核心链路。
本地消息表:业务操作与”待发送消息”记录在同一个本地事务里落库,后台任务扫描消息表投递 MQ,投递成功标记完成,失败重试;消费方保证幂等。简单可靠,缺点是与业务耦合、消息表需要定时清理。
RocketMQ 事务消息:本地消息表的中间件化。
- 发送半消息(对消费者不可见)→ 2. 执行本地事务 → 3. 根据结果提交/回滚半消息;Broker 定时回查生产者本地事务状态兜底。解决”本地事务与发消息的原子性”。
Seata AT 模式原理 ⭐(国内面试高频):
- 一阶段:代理数据源解析业务 SQL,生成前后镜像(before/after image)写入
undo_log,与业务数据在同一个本地事务提交,同时向 TC(事务协调器)注册分支; - 二阶段提交:异步批量删除 undo_log,几乎无性能损耗;
- 二阶段回滚:根据 undo_log 校验数据未被别人改过,用前镜像生成反向 SQL 补偿;
- 全局锁:保证一阶段已提交的本地数据,在全局事务回滚前不被其他全局事务修改(隔离性来源);
- 优点:业务几乎零侵入(一个
@GlobalTransactional注解);代价:全局锁带来并发性能损耗。
选型对比表:
| 方案 | 一致性 | 性能 | 侵入性 | 适用场景 |
|---|---|---|---|---|
| 2PC/3PC | 强 | 差 | 低(协议层) | 数据库层面,工程少用 |
| TCC | 最终一致(较强) | 好 | 极高(手写三接口) | 资金类核心链路 |
| 本地消息表 | 最终一致 | 好 | 中 | 中小系统的 MQ 场景 |
| RocketMQ 事务消息 | 最终一致 | 好 | 低 | 依赖 RocketMQ 的异步场景 |
| Seata AT | 最终一致(准实时) | 中(全局锁) | 低 | 大多数微服务跨库场景 |
7. RPC 基础
把本地方法调用变成跨网络调用,中间要做五件事——一次 RPC 调用流程:
- 动态代理:调用方拿到的是接口的代理对象,方法调用被拦截,组装请求(接口名、方法名、参数、版本号);
- 序列化:把请求对象序列化成字节流(Kryo/Protobuf/Hessian 等;JSON 慢但可读);
- 网络传输:客户端通过 Netty(TCP)发送,服务端接收;
- 反序列化:服务端还原请求,根据接口名找到本地实现类;
- 反射调用:
Method.invoke执行真实方法,结果原路序列化返回。
注册中心(Nacos/ZK)负责服务地址的注册与发现,负载均衡在调用方(客户端)完成。
Dubbo 与 OpenFeign 一句话:都是远程调用方案——Dubbo 是自研 RPC 框架(默认 Dubbo 协议 + 注册中心,服务治理能力强);OpenFeign 本质是基于 HTTP 的声明式调用客户端(伪 RPC:HTTP + 动态代理),是 Spring Cloud 技术栈的默认选择。
动手实践
-
手写雪花算法(含时钟回拨抛异常)
- 实现 64 位结构生成器 + 简单 workerId 分配(配置文件或数据库);
- 时钟回拨处理:≤5ms 自旋等待追平;>5ms 抛
IllegalStateException并打告警日志; - 验收:10 个线程各生成 10 万个 ID,去重后仍是 100 万(无重复);手动把系统时间回拨 10 秒,程序稳定抛异常而不是生成重复 ID。
-
封装 Redisson 分布式锁工具类
- 提供
tryLock(key, waitTime, Supplier)模板方法:加锁 → 执行业务 → finally 中释放;进阶用 AOP + 自定义@DistributedLock注解实现; - 验收:两个终端同时运行抢锁程序,同一时刻只有一个执行业务;把业务耗时设为 40 秒(超过默认 30 秒锁时长),观察看门狗续期期间锁不失效。
- 提供
-
对比测试三种”锁”(可选)
- 分别用数据库唯一索引、原生
SET NX EX、Redisson 跑同一段”查库存→扣减”逻辑; - 验收:用 JMeter 100 并发压测,观察前两种出现超卖或请求堆积,Redisson 版扣减结果精确;把现象与原因写成注释留在代码里。
- 分别用数据库唯一索引、原生
高频面试题
Q:CAP 定理是什么?实践中怎么取舍?⭐
- P 必选(网络分区无法避免),实际是在 CP 与 AP 之间做选择;
- 涉及钱、锁、选主的场景选 CP:宁可拒绝服务,不能读到脏数据(ZooKeeper、etcd);
- 服务发现、登录态、信息流场景选 AP:宁可返回旧数据,不能整体不可用(Eureka、Nacos 默认);
- 分区恢复后靠同步与补偿达到最终一致,这正是 BASE 理论;
- 能举注册中心的例子(ZK 选举期间不可用 vs Eureka 返回旧列表)是明显加分项。
Q:一致性哈希解决什么问题?虚拟节点的作用?⭐
- 解决节点数变化时普通取模导致几乎全量数据迁移的问题;
- 哈希环 + 顺时针寻址,增删节点只影响相邻区段的数据(约 1/N);
- 节点少时数据倾斜,用虚拟节点(每个物理节点 100~200 个)打散到环上,让分布均匀;
- 应用:分布式缓存、负载均衡策略;能对比出 Redis Cluster 用的是 hash slot 而非一致性哈希更好。
Q:讲讲雪花算法的结构?时钟回拨怎么处理?⭐
- 64 位 = 1 符号位 + 41 位时间戳(约 69 年)+ 10 位机器 ID + 12 位毫秒内序列号(4096);
- 时钟回拨会让同一毫秒被重复使用而生成重复 ID;
- 处理:小回拨(毫秒级)等待追平;大回拨抛异常告警,或切换备用 workerId;
- 缓解手段:NTP 用渐变同步(slew)而非阶跃、预留回拨保护位、生产用美团 Leaf 等成熟方案。
Q:Redis、ZooKeeper、数据库三种分布式锁怎么选?⭐
- Redis:性能最高,Redisson 解决了续期、误删、重入三大坑;代价是主从切换瞬间可能丢锁,适合高并发业务锁;
- ZK:临时顺序节点 + watch 前一个节点,会话断开自动释放、不会死锁,CP 更可靠,性能一般;
- 数据库:实现最简单,但性能差、无自动过期、要自己处理宕机,只在低频场景兜底;
- 结论:默认 Redisson;强一致选 ZK;数据库是最后的选择。
Q:分布式事务有哪些方案?怎么选型?⭐
- 用三个维度评估:一致性强度、性能、业务侵入性;
- 强一致:2PC(同步阻塞,工程少用)、TCC(侵入大但可控,适合资金链路,要处理空回滚/幂等/悬挂);
- 最终一致:本地消息表(简单通用)、RocketMQ 事务消息(半消息 + 回查)、Seata AT(零侵入但有全局锁开销);
- 选择优先级:能合并成单库本地事务 > 消息最终一致 > Seata AT > TCC,数据越重要用侵入性越强的方案;
- 一定先反问业务场景再选型,体现”能不用就不用”的工程判断。
Q:Seata AT 模式的原理?
- 一阶段:代理数据源解析 SQL,把前后镜像写入 undo_log,随本地事务一起提交,并向 TC 注册分支;
- 二阶段:全局提交则异步删除 undo_log;全局回滚则校验后镜像无脏写,用前镜像生成反向 SQL 补偿;
- 全局锁保证回滚窗口内数据不被其他全局事务修改;
- 对比 TCC:AT 无业务侵入,但依赖数据库支持且并发受全局锁限制。
推荐资料
| 资料 | 说明 | 链接 |
|---|---|---|
| pdai 架构知识体系 | CAP、一致性、分布式锁、事务的体系化索引 | https://pdai.tech/md/arch/arch-z-overview.html |
| JavaGuide RPC&Dubbo 入门 | RPC 原理与 Dubbo 架构详解 | https://javaguide.cn/distributed-system/rpc/rpc-intro.html |
| JavaGuide ZooKeeper 入门 | ZAB、Watcher、临时节点与 ZK 分布式锁 | https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html |
| JavaGuide 消息队列入门 | 事务消息与 MQ 基础概念 | https://javaguide.cn/high-performance/message-queue/message-queue.html |
| JavaGuide 高可用系统设计指南 | 限流/降级/超时重试,衔接下一篇 | https://javaguide.cn/high-availability/high-availability-system-design.html |
| JavaGuide 后端面试通关计划 | 冲刺期分布式部分的复习节奏 | https://javaguide.cn/interview-preparation/backend-interview-plan.html |
| pdai 面试知识体系 | 分布式面试真题索引 | https://pdai.tech/md/interview/x-interview.html |