分布式并发
⬅️ 10-调优排障与事故复盘 | ➡️ 12-大厂面试速查
1. 为什么需要分布式锁 —— 从根上想清楚(⭐⭐⭐)
1.1 核心矛盾
高可用 → 多副本/多实例 → 共享状态的并发修改 → 需要互斥
但第一反应不应该是加锁,而是问:
| 问题 | 如果答案是 Yes |
|---|---|
| 能否改为无锁设计?(幂等 + CAS + 版本号) | 不需要分布式锁 |
| 能否按 key 分区,让一个 key 只被一个节点处理? | 不需要分布式锁 |
| 能否容忍偶尔重复执行?(效率场景) | 用简单的 Redis SET NX,不需要强一致锁 |
| 必须严格互斥,且一旦违反就是资损? | 需要分布式锁 + fencing + 存储端校验 |
Staff 视角:面试中上来就讲 Redis SET NX 的候选人很多;能先分析"是否真的需要锁"的很少。真正的高并发系统设计原则是 avoid coordination(避免协调),锁是最后手段。
1.2 单机锁为什么失效
- 每个 JVM 的锁只能保护本进程内的线程互斥
- 跨 JVM = 跨进程 → 需要一个外部的、所有参与者都能看到的协调点
- 网络引入新问题:消息丢失、延迟不可预测、时钟不一致、节点宕机
1.3 分布式锁的本质约束(不可能三角)
┌─────────────────────────────────────────────┐
│ 安全性 (Mutual Exclusion) │
│ 任意时刻最多一个 client 持有锁 │
├─────────────────────────────────────────────┤
│ 活性 (Liveness) │
│ 锁最终会被释放(不会死锁) │
├─────────────────────────────────────────────┤
│ 容错性 (Fault Tolerance) │
│ 锁服务部分节点失败时仍可用 │
└─────────────────────────────────────────────┘
关键洞察:在异步网络模型下(消息延迟无上界),安全性和活性不能同时严格保证(FLP 不可能定理的工程推论)。所有分布式锁实现都是在这三者间做 trade-off。
2. 为什么是 Redis —— 方案对比与选型决策(⭐⭐⭐)
2.1 四种主流实现对比
| 维度 | Redis (SET NX) | ZooKeeper | etcd | 数据库 (SELECT FOR UPDATE) |
|---|---|---|---|---|
| 性能 | 10w+ QPS/节点 | ~1w QPS | ~1w QPS | 取决于 DB,但锁行级竞争高时急剧下降 |
| 一致性模型 | AP(主从异步复制) | CP(ZAB 共识,写需半数) | CP(Raft 共识) | 取决于隔离级别 |
| 锁丢失风险 | 主从切换时可能丢 | 极低(session 断 = 锁释放) | 极低(Lease 续约) | 极低(事务保证) |
| 运维成本 | 低(大多已有 Redis) | 高(独立集群 + 会话管理) | 中(K8s 环境自带) | 低(复用业务 DB) |
| 适用场景 | 效率锁、高QPS、短临界区 | 选主、配置变更、长任务 | K8s 生态内的协调 | 事务内天然互斥 |
| 客户端复杂度 | 低 | 中(Watcher 机制有坑) | 中 | 最低 |
2.2 选 Redis 的真实原因(工程视角)
面试中不能只说"Redis 快",要说清楚为什么在你的场景下选 Redis:
- 已有基础设施 — 支付系统已经有 Redis 集群做缓存,不需要额外引入 ZK/etcd。
- 锁持有时间短 — 支付扣款临界区 <100ms,锁 TTL 设 5s,主从切换丢锁的时间窗口(1-2s)内业务已完成。
- 可以容忍极端情况 — 通过 fencing token + DB 唯一约束兜底,即使锁失效也不会资损。
- QPS 要求高 — 大促峰值 5w TPS 锁请求,ZK 扛不住。
2.3 什么时候不该选 Redis
| 场景 | 为什么不选 Redis | 应该选什么 |
|---|---|---|
| 分布式选主(长期持有) | 主从切换 = 脑裂 = 双主 | ZK/etcd(session 机制天然保护) |
| 严格一致性场景(银行核心系统) | Redis 异步复制 = 可能双写 | 数据库行锁 + 事务 |
| 已经在事务里了 | 多加一次网络 RT 没必要 | SELECT ... FOR UPDATE |
| 锁粒度极细 + 超高并发(百万级/s) | Redis 也扛不住 | 无锁设计(分区 + 幂等) |
3. Redis 分布式锁深度实现(⭐⭐⭐)
3.1 正确的获取与释放
# 获取 —— 三要素缺一不可
SET lock:order:12345 <uuid> NX PX 5000
# lock key 粒度要细 ↑ ↑NX ↑过期兜底
# value 必须唯一(UUID/线程ID+时间戳)
-- 释放 —— 必须 Lua 原子操作(否则 GET + DEL 之间锁可能已过期被别人拿走)
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
3.2 每个设计决策背后的"为什么"
| 决策 | 为什么 | 不这么做会怎样 |
|---|---|---|
| NX(不存在才设) | 保证互斥 | 两个 client 同时拿到锁 |
| PX(过期时间) | 防止持有者崩溃后永不释放 | 死锁 |
| value = UUID | 释放时验证身份 | A 的锁过期后被 B 拿到,A 醒来把 B 的锁删了 |
| Lua 脚本释放 | GET+DEL 原子化 | 竞态:GET 返回 match → 锁过期 → B 获取 → A DEL 删了 B 的锁 |
| key 粒度细 | 减少锁冲突 | lock:order 一把大锁 = 序列化所有订单 |
3.3 续期机制(Watchdog)的陷阱
// Redisson 默认行为
lockWatchdogTimeout = 30s // 锁 TTL
renewInterval = 10s // 每 10s 续期一次
// 问题 1:业务线程挂了但 JVM 没挂
// → Watchdog 在 Netty EventLoop 里跑,会持续续期
// → 锁永不释放 → 其他节点永远拿不到锁 → 等于死锁
// 解法:设续期上限(如最多续 3 次 = 90s),超过强制释放 + P1 告警
// 问题 2:Full GC 导致 STW > renewInterval
// → 锁过期 → 别的 client 拿到 → GC 结束后两个 client 都认为自己持有锁
// 解法:fencing token(第 4 节)
// 问题 3:手动设置了 leaseTime 则 Watchdog 不启动
// → 很多人不知道这个行为 → 锁到期自动释放、业务还没做完
3.4 实战中的锁设计模板
public class RedisDistributedLock {
private static final int MAX_RETRY = 3;
private static final long RETRY_INTERVAL_MS = 50;
private static final long LOCK_TTL_MS = 5000;
/**
* 关键设计决策:
* 1. 不用 Watchdog → 明确 TTL,超时即失败,靠幂等兜底
* 2. 快速失败 + 少重试 → 高并发下重试太多 = 放大锁压力
* 3. 随机退避 → 避免惊群
*/
public Optional<String> tryLock(String lockKey) {
String token = UUID.randomUUID().toString();
for (int i = 0; i < MAX_RETRY; i++) {
Boolean acquired = redis.execute(SET, lockKey, token, NX, PX, LOCK_TTL_MS);
if (Boolean.TRUE.equals(acquired)) {
return Optional.of(token);
}
sleep(RETRY_INTERVAL_MS + ThreadLocalRandom.current().nextLong(30));
}
return Optional.empty(); // 快速失败,让调用方决策
}
}
4. Redlock 争议 —— 深度理解而非背诵结论(⭐⭐⭐)
4.1 算法核心
在 N(通常 5)个完全独立的 Redis 节点上执行 SET NX PX:
- 记录开始时间
- 依次尝试 N 个节点
- 成功数 ≥ N/2 + 1 且 已耗时 < TTL → 获取成功
- 失败则在所有节点上释放
4.2 Martin Kleppmann 的攻击(逐条分析)
攻击 1:GC Pause / 进程暂停
Timeline:
Client A: acquire lock (token=33) → GC pause 30s → resume → write to DB
Client B: (lock expired) → acquire lock (token=34) → write to DB
❌ 两个 client 都写了
- 这不是 Redlock 特有的问题,任何基于租约的分布式锁都有这个问题。
- 根本原因:锁的有效性是基于时间的,而进程无法感知自己暂停了多久。
攻击 2:时钟跳变
- Redlock 依赖 wall-clock 计算锁有效时间。
- 如果某节点时钟跳前 → 该节点上的锁 TTL 提前耗尽 → 锁被别人拿走。
- NTP 调整可能造成跳变(虽然现代系统用 slew 而非 step,但不保证)。
攻击 3:网络延迟
- Client 花 4.9s 在 5 个节点上获取锁(TTL=5s)→ 锁已经快过期了 → 有效窗口极短。
- Redlock 算法考虑了这个(要减去获取耗时),但在极端延迟下有效窗口可能为负。
4.3 Antirez(Redis 作者)的反驳
- 时钟漂移:现代 Linux 服务器随机漂移 < 几十ms/s,相对于秒级 TTL 影响极小。
- GC pause:如果你的 GC pause > 锁 TTL,那问题不在锁而在你的 GC。
- “Redlock 提供的安全性比单 Redis 锁强得多,在工程实践中足够安全。”
4.4 Staff 级别的结论(不是非黑即白)
┌─────────────────────────────────────────────────────────┐
│ 分布式锁的两种用途(Martin 的关键区分): │
│ │
│ 1. Efficiency(效率):防止重复计算/重复通知 │
│ → 偶尔失败可接受 → 单 Redis SET NX 就够了 │
│ │
│ 2. Correctness(正确性):防止资损/数据不一致 │
│ → 锁本身不够 → 必须配合 fencing token + 存储端校验 │
│ → 无论用 Redis/ZK/etcd 都需要 fencing │
└─────────────────────────────────────────────────────────┘
结论:Redlock 的争论本质上暴露了一个更深的真理 —— 分布式锁不应该是正确性的唯一防线。
5. Fencing Token —— 真正的资损防线(⭐⭐⭐)
5.1 工作原理
5.2 实现方式
-- 方案 1:DB 列存储 fencing token
ALTER TABLE payment_order ADD COLUMN lock_fencing_token BIGINT DEFAULT 0;
UPDATE payment_order
SET status = 'PAID', lock_fencing_token = 34
WHERE order_id = 'ORD123'
AND lock_fencing_token < 34; -- 只接受更大的 token
-- affected_rows = 0 → 说明有更新的 holder 已经写过了
// 方案 2:Redis 自增做 fencing token
// 获取锁时同时获取 token
Long fencingToken = redis.incr("fencing:" + lockKey);
5.3 Fencing 的局限性
- 并非所有存储都支持 fencing:消息队列、外部 API 调用无法做 fencing → 需要幂等设计。
- Token 需要传递到所有下游:如果临界区内调用了 3 个服务,每个都要校验 token → 侵入性强。
- 实践中的折中:只在最终写入点(通常是 DB)做 fencing,中间步骤靠幂等。
6. 引入分布式锁的弊端与规避(⭐⭐⭐)
面试官经常问:“分布式锁有什么问题?” 只答"性能差"= 不及格。
6.1 七大弊端
| # | 弊端 | 具体影响 | 真实案例 |
|---|---|---|---|
| 1 | 性能瓶颈 | 锁 = 串行化 → 吞吐上限 = 锁服务 QPS | 支付扣款锁粒度太粗 → 大促 TPS 打到 Redis 上限 |
| 2 | 可用性降级 | 锁服务挂了 = 全链路不可用 | Redis 主从切换 3s → 所有获取锁的请求失败 |
| 3 | 死锁/活锁 | TTL 设置不当/续期失败 | Watchdog 续期失败但业务还在跑 → 两个 client 进临界区 |
| 4 | 延迟放大 | 获取锁的等待时间 + 重试 | P99 从 50ms 涨到 500ms(排队等锁) |
| 5 | 复杂度爆炸 | 锁获取/释放/续期/异常处理 | 代码量膨胀 3x,Bug 面积暴增 |
| 6 | 锁粒度困境 | 粗 = 低并发;细 = 锁爆炸 | 按 userId 锁 → 百万用户 = 百万 key |
| 7 | 测试困难 | 分布式竞态难以复现 | 压测正常,生产出问题(因为压测没模拟网络分区) |
6.2 规避策略
设计原则:能不加锁就不加锁 → 能用乐观锁就不用悲观锁 → 必须悲观锁就缩短持有时间
| 策略 | 做法 | 效果 |
|---|---|---|
| 分区避免 | 按 user_id hash 分区,每个分区由单一消费者处理 | 彻底消除锁(Kafka 分区消费模式) |
| 乐观并发 | DB version 字段 + CAS 更新 | 无锁,失败重试(适合冲突率 < 5%) |
| 幂等设计 | 唯一索引 + upsert | 重复执行无副作用 → 不怕锁失效 |
| 锁降级 | 正确性场景用锁,效率场景用简单去重 | 减少 80% 的锁使用 |
| 异步化 | 消息队列 + 单消费者顺序执行 | 用队列替代锁(本质是把并发变串行,但吞吐可控) |
7. 高并发系统如何设计 —— 减少对锁的依赖(⭐⭐⭐)
7.1 架构演进:从重锁到无锁
7.2 支付系统实战:扣款的四种并发控制方案
场景:用户余额扣款,要求不超扣。
方案 A:分布式锁(最直觉但最差)
// ❌ 不推荐:锁粒度在 accountId 级别,但 Redis 成为瓶颈
lock("account:" + accountId);
try {
balance = db.getBalance(accountId);
if (balance >= amount) db.deduct(accountId, amount);
} finally { unlock(); }
问题:Redis QPS 上限就是系统吞吐上限。
方案 B:数据库乐观锁(推荐中低并发)
-- 一条 SQL 搞定,无需外部锁
UPDATE account
SET balance = balance - #{amount}, version = version + 1
WHERE account_id = #{accountId}
AND balance >= #{amount}
AND version = #{expectedVersion};
-- affected_rows = 0 → 重试(乐观锁冲突)或拒绝
问题:高竞争(同一账户每秒 1000+ 请求)→ 重试风暴。
方案 C:分区 + 本地顺序处理(推荐高并发)
┌→ Partition 0 → Consumer 0 (单线程顺序扣款)
请求 → accountId hash ├→ Partition 1 → Consumer 1
└→ Partition 2 → Consumer 2
- 同一 accountId 一定路由到同一 partition → 单消费者 → 无并发
- 吞吐 = partition 数 × 单 consumer 处理能力
- 本质:把分布式并发问题降级为单机顺序问题
方案 D:余额预扣 + 异步确认(支付宝方案)
1. 预扣:UPDATE balance = balance - amount WHERE balance >= amount (单条 SQL,数据库保证)
2. 执行业务(调下游)
3. 成功 → 确认扣款;失败 → 回滚(balance + amount)
- 不需要分布式锁:一条 UPDATE 就完成了互斥
- 关键:把互斥逻辑下沉到数据库层(数据库行锁是最可靠的分布式锁)
7.3 设计决策树
8. 幂等与去重 —— 锁的最佳搭档(⭐⭐⭐)
8.1 为什么幂等比锁更重要
锁保证"同一时刻只有一个",但无法保证"只执行一次" —— 网络重试、消息重投、用户双击都会导致重复请求。
分布式锁负责:并发控制(mutual exclusion)
幂等负责:重复执行安全(at-least-once → exactly-once 语义)
两者互补,不能替代。
8.2 幂等键设计精要
// ❌ 错误:timestamp 参与 → 重试时时间变了 → 生成不同的 key → 幂等失效
key = hash(userId + orderId + amount + timestamp)
// ✅ 正确:用业务唯一标识 → 重试时生成相同 key
key = hash(userId + orderId + amount + idempotencyId)
// idempotencyId 由客户端生成,一次业务动作固定不变
8.3 去重方案深度对比
| 方案 | 适用 QPS | 一致性 | 过期处理 | 实际使用场景 |
|---|---|---|---|---|
| DB 唯一索引 | < 5k | 强一致 | 不过期(永久去重) | 支付单号去重 |
| Redis SET NX + TTL | < 50k | 弱(过期后可重入) | TTL 过后允许重新执行 | 短时间防重(防双击) |
| Redis + DB 双写 | < 30k | 强 | Redis 去重 + DB 最终确认 | 支付扣款 |
| Bloom Filter | 百万级 | 有误判(false positive) | 不支持删除 | 消息 ID 去重(可容忍误判) |
| 本地缓存 + DB | 极高 | 最终一致 | 本地 Caffeine 短 TTL + DB 持久化 | 高频查重 |
8.4 支付场景幂等表设计
CREATE TABLE idempotency_record (
idempotency_key VARCHAR(64) PRIMARY KEY, -- 幂等键
biz_type VARCHAR(32) NOT NULL, -- 业务类型
request_hash VARCHAR(64) NOT NULL, -- 请求体摘要(防止不同请求复用 key)
response TEXT, -- 首次执行的响应(重复请求直接返回)
status ENUM('PROCESSING','DONE','FAILED'),
created_at TIMESTAMP,
expired_at TIMESTAMP -- 过期时间(可选清理)
);
-- 执行流程:
-- 1. INSERT ... ON DUPLICATE KEY → 插入成功 = 首次,执行业务
-- 2. 插入失败(duplicate)→ 查 status
-- DONE → 直接返回 response(幂等)
-- PROCESSING → 说明上次没完成(崩溃)→ 检查业务状态后决定重试或返回
-- FAILED → 允许重试
9. 顺序保证与分区一致性(⭐⭐)
9.1 三种顺序
| 类型 | 含义 | 实现 | 代价 |
|---|---|---|---|
| 全序 | 所有事件有唯一全局顺序 | 单 leader + 序号发号器 | 性能瓶颈(单点) |
| 分区序 | 同一分区内有序 | Kafka partition / DB 分片 | 跨分区无序 |
| 因果序 | 有因果关系的事件有序 | 向量时钟 / Lamport 时间戳 | 并发事件无序(可接受) |
9.2 实际选择
- 支付场景:按 accountId 分区序就够了 —— 同一账户的操作有序即可,不同账户之间不需要全局有序。
- 需要全序的场景:全局 sequence(如流水号) → 用 Snowflake / DB 自增 / Redis INCR。
9.5 Redis Cluster 模式下的分布式锁陷阱(⭐⭐⭐)
Staff 候选人很少意识到:Redis Cluster ≠ 单节点 Redis,锁行为会有本质差异。
9.5.1 Hash Slot 迁移导致锁丢失
场景:集群扩缩容触发 slot 迁移
1. Client A: SET lock:order:123 uuid NX PX 5000 → 写入 Node1 (slot 8888)
2. 运维触发 rebalance → slot 8888 从 Node1 迁移到 Node2
3. 迁移过程中 key 的 TTL 可能被重置或在网络抖动中丢失
4. Client B: 向 Node2 获取同一把锁 → 成功 → 双持有
应对:
- 迁移期间冻结写入(
CLUSTER SETSLOT MIGRATING),但会影响可用性 - 关键锁用
{hash_tag}固定 slot,减少被迁移的概率 - 根本解:fencing token 兜底
9.5.2 MOVED/ASK 重定向的延迟放大
// 客户端以为 10ms 拿到锁,实际:
// 1. 请求 Node1 → MOVED → 重定向 Node3 → 额外 2ms RTT
// 2. ASK 重定向(迁移中间态)→ 可能重试多次
// 锁的有效时间 = TTL - (获取耗时 + 重定向耗时)
// 在 slot 迁移期间,有效窗口可能大幅缩短
9.5.3 Cluster 模式下 Lua 脚本的限制
-- ❌ 这段在 Cluster 模式下会报错(跨 slot 操作)
local lock = redis.call("GET", "lock:order:123")
local counter = redis.call("INCR", "fencing:order:123")
-- CROSSSLOT Keys in request don't hash to the same slot
-- ✅ 用 hash tag 保证同一 slot
local lock = redis.call("GET", "{order:123}:lock")
local counter = redis.call("INCR", "{order:123}:fencing")
9.6 锁与事务边界的经典 Bug(⭐⭐⭐)
这是实际 code review 中出现率最高的分布式锁 bug,很多 Senior 都会踩。
9.6.1 锁在事务提交前释放
// ❌ 经典 Bug:Spring @Transactional 的代理陷阱
@Transactional
public void deduct(String accountId, BigDecimal amount) {
String token = lock.tryLock("account:" + accountId);
try {
Account account = accountDao.findById(accountId);
account.setBalance(account.getBalance().subtract(amount));
accountDao.update(account);
} finally {
lock.unlock("account:" + accountId, token);
// ❌ 锁释放了,但事务还没提交!
// 另一个线程拿到锁,读到旧数据(READ COMMITTED 下读已提交)
// 事务提交顺序:Thread2 commit → Thread1 commit → Thread1 覆盖 Thread2
}
}
// 事务在方法返回后才由 AOP 代理提交
// ✅ 正确做法 1:锁的范围大于事务
public void deduct(String accountId, BigDecimal amount) {
String token = lock.tryLock("account:" + accountId);
try {
transactionalService.doDeduct(accountId, amount); // 内部 @Transactional
} finally {
lock.unlock("account:" + accountId, token); // 事务已提交,安全释放
}
}
// ✅ 正确做法 2:用 TransactionSynchronization 在事务提交后释放锁
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
lock.unlock(lockKey, token);
}
});
9.6.2 锁与连接池耗尽的恶性循环
锁等待 → 线程阻塞 → 线程池满 → DB 连接池也满 → 健康检查超时
→ K8s 重启 Pod → 锁过期 → 别人拿到锁 → 重启的 Pod 回来又抢锁
→ 雪崩
Staff 认知:分布式锁的等待时间应计入线程池和连接池的容量规划。推荐 tryLock 超时 < 线程池队列容量能承受的排队时间。
9.7 跨数据中心/多活场景下的锁(⭐⭐)
9.7.1 为什么多活架构下分布式锁更难
┌──────────────┐ ┌──────────────┐
│ DC-East │◄──30ms──►│ DC-West │
│ Redis-East │ 异步复制 │ Redis-West │
│ App-East │ │ App-West │
└──────────────┘ └──────────────┘
问题:
- 跨 DC 获取锁 RTT ≥ 60ms → 吞吐暴跌
- 异步复制 = 跨 DC 可能双持有
- 网络分区 = 脑裂 = 两边各自认为自己是主
9.7.2 多活场景的正确做法
| 策略 | 做法 | 适用 |
|---|---|---|
| 单元封闭 | 按 userId 路由到固定 DC,锁在本 DC 闭环 | 支付系统主流方案(蚂蚁 CRG) |
| 全局锁降级 | 正常走本地 DC 锁;跨 DC 操作用 DB 行锁(强一致但慢) | 少量跨单元场景 |
| Paxos/Raft 锁 | etcd 或 ZK 跨 DC 部署(写延迟高) | 选主/配置变更(低频) |
Staff 结论:多活场景下应避免跨 DC 锁,通过单元化路由将锁收敛到本 DC。
9.8 锁的可观测性与熔断降级(⭐⭐⭐)
9.8.1 必须监控的指标
# 锁核心指标(Prometheus 示例)
- name: distributed_lock_acquire_duration_seconds
type: histogram
help: "获取锁耗时分布"
labels: [lock_name, result] # result: success/timeout/error
buckets: [0.005, 0.01, 0.05, 0.1, 0.5, 1.0]
- name: distributed_lock_hold_duration_seconds
type: histogram
help: "锁持有时间分布(检测长持有 = 潜在问题)"
- name: distributed_lock_timeout_total
type: counter
help: "获取锁超时次数(飙升 = 锁竞争加剧)"
- name: distributed_lock_watchdog_renewal_total
type: counter
labels: [lock_name, result] # result: success/expired
help: "续期次数(expired 飙升 = 临界区过长)"
9.8.2 告警规则
| 告警 | 条件 | 含义 |
|---|---|---|
| P1 | lock_hold_duration_p99 > TTL * 0.8 |
临界区接近超时,续期也救不回来 |
| P2 | lock_timeout_rate > 10% |
锁竞争过高,需要优化粒度或架构 |
| P1 | lock_acquire_errror_rate > 5% |
Redis 锁服务可能故障 |
| P2 | lock_hold_duration_p50 持续上升 |
慢 SQL / 慢依赖导致临界区膨胀 |
9.8.3 锁的熔断降级
// 当锁服务不可用时的降级策略
public Result deduct(DeductRequest req) {
try {
String token = lockService.tryLock(req.getAccountId(), 100, TimeUnit.MILLISECONDS);
if (token != null) {
return doDeductWithLock(req, token);
}
// 获取锁超时 → 降级到 DB 乐观锁
return doDeductOptimistic(req);
} catch (LockServiceUnavailableException e) {
// 锁服务熔断 → 降级
metrics.increment("lock.circuit_breaker.open");
return doDeductOptimistic(req); // 乐观锁兜底
}
}
降级层次:
- Redis 分布式锁(首选,高性能)
- DB 乐观锁(降级,中性能,强一致)
- DB 悲观锁(兜底,低性能,最强保证)
- 拒绝服务 + 告警(最终兜底)
9.9 生产事故模式库(⭐⭐⭐)
Staff 面试中,能讲出真实事故 + 根因 + 修复方案 = 强信号。
事故 1:Watchdog 续期导致的"不死锁"
现象:某个消费者实例持有锁 > 2 小时,其他实例全部阻塞
根因:业务代码 catch 了 InterruptedException 没中断线程 → 线程 stuck 在 HTTP 调用
→ 但 JVM 没挂 → Redisson Watchdog 持续续期 → 锁永不释放
修复:
1. 加 leaseTime 上限(90s),禁用无限续期
2. 加锁持有时间监控告警
3. Code review 规范:禁止吞掉 InterruptedException
事故 2:锁粒度错误导致大促雪崩
现象:大促开始 5 秒后,支付成功率从 99.9% 跌到 30%
根因:锁 key = "lock:payment"(全局一把锁,而非按订单/用户粒度)
→ 所有支付请求排队 → 请求堆积 → 超时 → 用户重试 → 放大效应
修复:
1. 锁 key 改为 "lock:payment:{userId}"
2. 收敛临界区(只锁扣款 SQL,不锁整个下单流程)
3. 增加锁获取超时监控
事故 3:锁释放后事务未提交导致脏读
就是 9.6.1 描述的场景,在生产环境表现为:
现象:用户余额偶现负数(概率约 0.01%)
根因:@Transactional + finally unlock → 事务提交前锁已释放
→ 并发请求读到旧余额 → 两笔都扣款成功 → 超扣
修复:将锁范围提到事务之外(调用层加锁,Service 层加事务)
10. 面试题精选(⭐⭐⭐)
Q1 阿里:「Redis 分布式锁超时了但业务还没执行完,怎么办?」(⭐⭐⭐)
分层回答(由浅到深):
第一层:续期(大部分人只答到这里)
- Redisson Watchdog 每 10s 续期,防止超时。
第二层:续期的问题 + 兜底
- 续期不能无限 → 如果业务线程死循环或 blocked,Watchdog 续期 = 永久持锁 = 死锁。
- 解法:续期设上限(如 leaseTime = 30s × 最多续 3 次 = 90s),超过强制释放。
第三层:即使锁丢了也要安全
- Fencing token + 存储端校验 → 即使锁过期,旧持有者的写入也会被拒绝。
- 业务幂等 → 即使重复执行也无副作用。
第四层(Staff):根本问题是临界区太长
- 如果业务需要 30s+ 才能完成 → 不适合用分布式锁 → 改为 Saga/消息驱动。
- 正确架构:锁只保护短促的状态变更(如 DB UPDATE),不应该锁住整个长业务流程。
Q2 蚂蚁:「支付场景用 Redis 锁还是 ZooKeeper 锁?」(⭐⭐⭐)
不要背表格,讲具体决策过程:
"我们支付系统选 Redis 锁,原因有三:
- 锁持有时间 < 200ms(扣款 SQL + 记账),在 TTL 5s 内一定能完成。主从切换窗口丢锁的概率极低。
- 配合 fencing token → 即使极端情况(概率 < 0.001%)丢锁,存储端也能拒绝脏写。
- QPS 要求高(5w/s),ZK 的写性能(~1w QPS)满足不了。
如果是选主场景(如定时任务 leader election),我们用 ZK —— session 机制保证持有者崩溃后锁自动释放,比 Redis TTL 更加确定。"
追问:「Redis 主从切换丢锁具体是怎么丢的?」
1. Client A 向 Master 获取锁成功
2. Master 还没把 SET 命令同步给 Slave 就宕机
3. Slave 提升为新 Master(不含那个锁 key)
4. Client B 向新 Master 获取同一个锁 → 成功
5. A 和 B 同时持有锁 ❌
Redlock 通过多数节点投票缓解,但不能根治(时钟问题)。
Q3 字节:「设计一个高并发扣减库存系统,不用分布式锁」(⭐⭐⭐)
这道题考的就是"避免锁"的设计能力:
方案:Redis 预扣 + DB 最终一致
1. 预热阶段:库存加载到 Redis(INCRBY stock:sku123 1000)
2. 扣减:DECR stock:sku123 → 返回值 ≥ 0 则成功(原子操作,无需锁)
3. 异步落库:扣减成功后发消息 → 消费者批量更新 DB
4. 兜底:定时对账 Redis vs DB,修复差异
优势:
- Redis DECR 单机 10w+ QPS
- 无锁(DECR 本身是原子的)
- 允许超卖?不会:DECR 返回负数时回滚(INCR 回去)
不足 + 应对:
- Redis 宕机丢数据 → 定时持久化 + 对账修复
- 异步落库延迟 → 用户看到扣了但 DB 还没减 → 最终一致(秒级)
Q4:「Redlock 和 Kleppmann 的观点你怎么看?」(⭐⭐⭐)
"我的观点是双方都对但在说不同的事:
- Antirez 在说工程上 Redlock 足够好(概率极低会出问题)
- Martin 在说理论上它不满足 linearizable(有可能违反 safety)
关键 insight 是 Martin 提出的分类:
- Efficiency lock → 单 Redis 就够,Redlock 是过度工程
- Correctness lock → Redlock 也不够,必须 fencing
所以在工程上,Redlock 存在的意义不大 —— 效率场景不需要它,正确性场景它也解决不了根本问题。我们选择:单 Redis 锁 + fencing + 幂等。"
11. 快问快答(⭐)
- Redis 单节点锁主从切换会丢锁吗? —— 会。异步复制 = 写入 master 未同步到 slave。
- setnx 和 set nx 区别? ——
SETNX是旧命令不支持同时设 EX/PX;SET key val NX PX ms是原子的。 - ZooKeeper 锁用什么节点? —— 临时顺序节点(ephemeral sequential)+ watch 前一个节点(避免惊群)。
- etcd 分布式锁? —— Lease 做 TTL + Revision 做 fencing token(天然单调递增)。
- 可重入锁怎么实现? —— Redis Hash:field=threadId,value=重入次数。
HINCRBY+HEXISTS。 - 锁的性能上限? —— 单 Redis 实例 ~10w QPS;超过就该想"是否不该用锁"。
- 公平锁? —— Redis List 做队列 / ZK 顺序节点天然公平。但分布式公平锁代价极大(慎用)。
12. 本章小结
带走一句话:分布式锁不是银弹,高并发系统的终极目标是减少 coordination —— 分区让数据天然隔离,幂等让重复执行安全,fencing 让锁失效也不致资损。
最后冲刺:各大厂面试风格 + 评分标准 → 12-大厂面试速查
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)