07 分布式锁的生产实践与避坑指南
摘要:
前六篇文章系统讲解了分布式锁的理论原理与各方案的实现机制。本篇回到工程现场,聚焦那些理论文章不会告诉你、但在生产环境中真实发生过的问题。内容分为四个维度:锁粒度设计(粒度过粗和过细各有什么代价、如何找到合适的粒度)、加锁失败的降级策略(快速失败 vs 排队等待,如何设计合理的降级链路)、监控与告警(分布式锁应该暴露哪些指标、异常告警阈值如何设定)、以及典型生产故障复盘(真实案例分析:死锁、锁泄漏、锁风暴、误解锁的根因与修复方案)。
第 1 章 锁粒度设计:最容易被忽视的架构决策
1.1 什么是锁粒度
锁粒度(Lock Granularity) 是指分布式锁所保护的资源范围大小。粒度越粗,一把锁保护的资源越多;粒度越细,一把锁保护的资源越少。
用一个具体的例子来说明:假设一个电商系统需要保护库存扣减操作。锁粒度可以是:
粒度 1(最粗):全局库存锁
lock_key = "inventory_lock"
→ 所有商品的库存扣减都竞争同一把锁
→ 串行化程度最高,吞吐量最低
粒度 2:商品级锁
lock_key = "inventory_lock:product:{productId}"
→ 不同商品的库存扣减互不影响
→ 同一商品的并发请求串行化
粒度 3:商品+仓库级锁
lock_key = "inventory_lock:product:{productId}:warehouse:{warehouseId}"
→ 不同仓库的同一商品可以并发扣减
→ 粒度最细,并发度最高
粒度 4(最细):订单级锁
lock_key = "inventory_lock:order:{orderId}"
→ 每个订单独享一把锁
→ 但无法防止同一商品被多个订单同时超卖
1.2 锁粒度过粗的代价
核心问题:并发度被人为压低
粒度过粗的锁会将大量本不需要互斥的操作强制串行化。考虑上例中”全局库存锁”方案:商品 A 和商品 B 的库存扣减操作完全独立,没有任何共享资源需要保护,但全局锁强迫它们排队执行。
量化影响:假设单次库存扣减操作耗时 10ms,使用全局锁的系统 QPS 上限 = 1000ms / 10ms = 100 QPS。而使用商品级锁,如果有 1000 种商品,理论上可以支持 100,000 QPS(1000 商品 × 100 QPS)。
真实案例:某电商平台在大促活动中,将所有商品的秒杀库存扣减都用一把全局 Redis 锁保护,在压测中发现 QPS 极低(仅 200 左右),原因就是全局锁将所有商品的扣减操作完全串行化。修改为商品级锁(lock_key = "seckill:product:{productId}")后,QPS 提升了 200 倍。
1.3 锁粒度过细的代价
核心问题:数据一致性边界被打破
粒度过细的锁可能无法保护真正需要保护的一致性约束。以上例中”订单级锁”为例:每个订单都有自己的锁,但订单之间是可以并发执行的。如果两个订单同时对同一商品进行库存扣减,它们各自持有不同的订单锁,互不阻塞,最终可能导致超卖。
粒度过细的另一个问题是锁数量爆炸。如果系统中有百万级商品,“商品+用户级锁”会产生海量不同的 lock_key,虽然每把锁的竞争度都很低,但这些锁记录会占用大量内存(Redis)或 ZNode(ZooKeeper),同时增加了锁管理的复杂度。
量化参考(Redis):一个 Redis String key 约占 50100 字节(含 key 名、value、元数据)。百万级 lock_key = 约 50100MB 内存占用,通常可以接受;但亿级 lock_key = 5~10GB,可能影响 Redis 的内存和扫描性能。
1.4 锁粒度的设计原则
原则一:以”最小化互斥范围”为目标
锁的范围应该恰好覆盖”需要原子执行的操作集合”,不多也不少。如果两个操作之间完全没有共享资源(不存在数据竞争),就不应该让它们互斥。
原则二:先识别”竞争维度”,再设计 key 结构
竞争维度是指哪个维度的并发操作可能产生冲突。常见的竞争维度:
- 用户维度:同一用户的并发请求(防止重复下单)→
lock_key = "order:user:{userId}" - 资源维度:竞争同一资源(防止超卖)→
lock_key = "inventory:{productId}" - 时间维度:同一时间段内只允许一次执行(防止重复任务)→
lock_key = "task:{taskId}:{date}"
原则三:使用层次化的 key 结构,方便监控和故障排查
推荐的 key 命名规范:
{业务模块}:{操作类型}:{竞争维度1}:{竞争维度2}...
示例:
order:create:user:123456 # 用户 123456 的创建订单锁
inventory:deduct:product:P001 # 商品 P001 的库存扣减锁
task:daily_report:2024-01-01 # 日报任务的执行锁
payment:process:order:O789012 # 订单 O789012 的支付处理锁
清晰的命名规范使得通过 Redis SCAN 命令或监控系统快速定位锁的归属成为可能。
1.5 热点锁问题与分片
即使设计了合适的锁粒度,某些”热点资源”仍然会成为锁竞争的瓶颈。典型场景:爆款商品(某款限量球鞋)的库存,可能在秒杀活动中有数万个并发请求同时竞争同一把锁。
热点锁的特征:
- 同一个 lock_key 上有大量并发等待者
- 锁等待时间 P99 远高于正常值
- Redis 中对应 key 的访问频率远高于其他 key
分片锁(Lock Sharding):将一把热点锁拆分为 N 把子锁,每个请求随机选择一把子锁:
// 分片锁实现思路
public class ShardedDistributedLock {
private static final int SHARD_COUNT = 8; // 拆分为 8 个分片
public RLock getShard(String lockKey, String shardingKey) {
int shardIndex = Math.abs(shardingKey.hashCode()) % SHARD_COUNT;
String shardedKey = lockKey + ":" + shardIndex;
return redissonClient.getLock(shardedKey);
}
}
// 使用:每个订单根据 orderId 选择不同的库存锁分片
RLock lock = shardedLock.getShard("inventory:product:P001", orderId);
lock.lock();
try {
deductInventory(productId, quantity);
} finally {
lock.unlock();
}注意:分片锁无法保证全局互斥(不同分片的请求可以并发执行),因此需要配合业务层的原子操作(如 UPDATE inventory SET stock = stock - 1 WHERE stock > 0)来保证最终一致性。分片锁的作用是减少竞争,降低锁等待时间,而不是替代业务层的数据一致性保证。
第 2 章 加锁失败的降级策略
2.1 加锁失败的两种语义
加锁失败(tryLock 返回 false)有两种不同的语义,对应不同的处理策略:
语义一:竞争失败(Lock Contended)
锁被其他客户端持有,当前请求无法立即获取锁。此时有两种选择:
- 等待重试:在
waitTime内循环尝试,等到锁释放 - 快速失败(Fail-Fast):立即放弃,返回错误给用户
语义二:系统故障(System Error)
Redis/ZooKeeper 服务不可用,或网络超时,导致加锁操作本身失败。此时应该触发降级流程,而不是无限重试(重试也不会成功)。
2.2 快速失败策略
适用场景:用户发起的请求(如下单、支付),需要快速响应,不能让用户等待
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
String lockKey = "order:create:user:" + request.getUserId();
String lockValue = UUID.randomUUID().toString();
// 尝试立即加锁(waitTime = 0,不等待)
boolean acquired = distributedLock.tryLock(lockKey, lockValue, 0, 30000);
if (!acquired) {
// 快速失败:返回友好提示,引导用户稍后重试
return ResponseEntity.status(429)
.body(OrderResponse.error("操作太频繁,请稍后再试"));
}
try {
Order order = orderService.createOrder(request);
return ResponseEntity.ok(OrderResponse.success(order));
} finally {
distributedLock.release(lockKey, lockValue);
}
}快速失败策略需要配合前端友好提示:用户收到”稍后重试”提示后,知道可以在几秒后重新尝试,而不是以为系统故障。
2.3 有界等待策略
适用场景:批处理任务、异步任务,可以接受等待但需要有上限
public void processBatchTask(String taskId) {
String lockKey = "task:batch:" + taskId;
// 有界等待:等待最多 5 秒
boolean acquired = lock.tryLock(5, TimeUnit.SECONDS);
if (!acquired) {
// 记录日志,放入重试队列(而不是直接报错)
log.warn("任务 {} 获取锁超时,加入重试队列", taskId);
retryQueue.offer(new RetryTask(taskId, System.currentTimeMillis() + 60000));
return;
}
try {
executeBatchTask(taskId);
} finally {
lock.unlock();
}
}重试队列的设计要点:
- 设置最大重试次数,防止无限重试
- 使用指数退避(Exponential Backoff)延迟,避免重试风暴
- 记录每次重试的原因,便于监控
2.4 熔断降级:锁服务不可用时的兜底
当 Redis/ZooKeeper 本身不可用时,继续尝试加锁只会增加系统压力。应该结合熔断器(Circuit Breaker)进行降级:
@Service
public class DistributedLockService {
private final CircuitBreaker circuitBreaker; // Resilience4j 熔断器
public boolean tryLock(String lockKey, String lockValue, long ttlMs) {
// 通过熔断器保护加锁操作
try {
return circuitBreaker.executeSupplier(() ->
redissonClient.getLock(lockKey).tryLock(0, ttlMs, TimeUnit.MILLISECONDS)
);
} catch (CallNotPermittedException e) {
// 熔断器 OPEN 状态:Redis 可能不可用,走降级逻辑
log.warn("分布式锁服务熔断,使用本地 JVM 锁降级处理");
return localFallback(lockKey); // 单节点降级方案
}
}
private boolean localFallback(String lockKey) {
// 降级方案:使用本地 ConcurrentHashMap + ReentrantLock
// 注意:本地锁只在单节点有效,无法保证跨节点的互斥
// 但在 Redis 不可用的紧急情况下,至少能保证单节点安全
return localLockManager.tryLock(lockKey);
}
}熔断降级的注意事项:本地 JVM 锁降级只能保证单节点内的互斥,无法跨节点。在 Redis 不可用期间,如果多个节点同时降级为本地锁,仍然存在跨节点并发的问题。因此,降级方案需要结合业务的幂等性设计(数据库唯一约束等)来共同保证数据安全。
第 3 章 监控与告警:让锁的状态可观测
3.1 分布式锁应该暴露哪些指标
分布式锁是系统的关键基础设施,其运行状态应该纳入监控体系。以下是最重要的监控指标:
指标一:锁等待时间(P50/P95/P99)
lock_wait_duration_seconds{lock_key="inventory:product:*", quantile="0.99"}
等待时间的 P99 突然升高,通常预示着锁竞争加剧(可能是热点锁或锁持有时间过长)。
指标二:加锁成功率
lock_acquire_success_rate = lock_acquired_total / lock_attempt_total
加锁成功率持续低于 95%,说明系统存在严重的锁竞争或锁服务问题。
指标三:锁持有时间(P50/P95/P99)
lock_hold_duration_seconds{lock_key="payment:process:*", quantile="0.99"}
锁持有时间的 P99 突然升高,可能是临界区操作变慢(如数据库查询变慢、外部 API 超时),也可能是锁泄漏(持锁后未释放)。
指标四:加锁失败计数(按失败原因分类)
lock_acquire_failed_total{reason="timeout"} # 等待超时
lock_acquire_failed_total{reason="error"} # 锁服务异常
指标五:Watch 触发频率(ZooKeeper 方案特有)
Watch 事件频率可以反映锁竞争的激烈程度。频率过高说明锁争用严重,可能需要优化锁粒度。
3.2 用 Micrometer 暴露 Redisson 锁指标
@Aspect
@Component
public class DistributedLockMetricsAspect {
private final MeterRegistry meterRegistry;
@Around("@annotation(DistributedLock)")
public Object recordLockMetrics(ProceedingJoinPoint pjp,
DistributedLock distributedLock) throws Throwable {
String lockKey = resolveLockKey(pjp, distributedLock);
// 记录等待时间
long waitStart = System.currentTimeMillis();
boolean acquired = false;
try {
acquired = acquireLock(lockKey, distributedLock.waitTime());
long waitDuration = System.currentTimeMillis() - waitStart;
meterRegistry.timer("distributed_lock.wait_duration",
"lock_key", maskLockKey(lockKey), // 对动态部分做掩码,避免 cardinality 爆炸
"acquired", String.valueOf(acquired))
.record(waitDuration, TimeUnit.MILLISECONDS);
if (!acquired) {
meterRegistry.counter("distributed_lock.acquire_failed",
"lock_key", maskLockKey(lockKey),
"reason", "timeout")
.increment();
throw new LockAcquireException("获取分布式锁超时: " + lockKey);
}
// 记录持有时间
long holdStart = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long holdDuration = System.currentTimeMillis() - holdStart;
meterRegistry.timer("distributed_lock.hold_duration",
"lock_key", maskLockKey(lockKey))
.record(holdDuration, TimeUnit.MILLISECONDS);
}
} finally {
if (acquired) {
releaseLock(lockKey);
}
}
}
/**
* 对锁 key 中的动态部分做掩码,防止 Prometheus label cardinality 爆炸
* 例:inventory:product:P001 → inventory:product:{id}
*/
private String maskLockKey(String lockKey) {
return lockKey.replaceAll(":[0-9a-zA-Z-]{8,}", ":{id}");
}
}Prometheus 的 Cardinality 陷阱
监控系统(如 Prometheus)中,每个唯一的 label 组合都会产生一个时序(Time Series)。如果将动态的 lock_key(如包含 userId、orderId)直接作为 label,会产生海量不同的时序,导致 Prometheus 的内存暴涨(Cardinality 爆炸)。
解决方案:对 lock_key 做掩码,将动态部分替换为占位符(
{id}),只保留 key 的结构信息,而不是具体值。例如:inventory:product:P001→inventory:product:{id}。
3.3 告警阈值参考
| 指标 | 告警阈值(参考) | 告警级别 | 可能的根因 |
|---|---|---|---|
| 锁等待时间 P99 > 500ms | > 500ms(持续 2 分钟) | WARNING | 锁竞争加剧、锁持有时间过长 |
| 锁等待时间 P99 > 2s | > 2s(持续 1 分钟) | CRITICAL | 热点锁、死锁或锁泄漏 |
| 加锁成功率 < 95% | < 95%(持续 5 分钟) | WARNING | 高并发锁竞争 |
| 加锁成功率 < 80% | < 80%(持续 2 分钟) | CRITICAL | 锁服务异常或系统过载 |
| 锁持有时间 P99 > 10s | > 10s(持续 2 分钟) | WARNING | 临界区操作变慢或锁泄漏 |
| Redis 连接错误率 > 1% | > 1%(持续 1 分钟) | CRITICAL | Redis 实例故障 |
第 4 章 典型生产故障复盘
4.1 故障案例一:Redis 主从切换导致双写(超卖事故)
故障背景:某大促活动期间,秒杀系统使用 Redis 主从架构的分布式锁保护库存扣减。活动期间,Redis Master 因内存压力 OOM,被系统 Kill,Sentinel 自动将 Slave 提升为新 Master。
故障过程:
T1: 客户端 A 向 Master 加锁成功(SET lock:product:P001 "client_A" NX PX 30000)
T2: 扣减操作写入 DB:UPDATE inventory SET stock = stock - 1 WHERE product_id = 'P001'
T3: [在数据复制到 Slave 之前] Master 因 OOM 宕机
T4: Sentinel 将 Slave 提升为新 Master(Slave 上无 lock:product:P001 记录)
T5: 客户端 B 向新 Master 加锁:SET lock:product:P001 "client_B" NX PX 30000 → 成功
T6: 客户端 B 也执行扣减:UPDATE inventory SET stock = stock - 1 WHERE product_id = 'P001'
T7: 最终 stock 被扣减 2 次,但实际上应该只有 1 件商品
→ 超卖!
根本原因:Redis 主从异步复制在 Master 宕机时存在数据丢失窗口,使得新 Master 上锁记录缺失,其他客户端可以重新加锁。
修复方案:
方案 A(短期):在数据库层面增加最终防线,通过原子 SQL 防止超卖:
-- 乐观锁兜底:只有 stock > 0 时才更新,且原子检查
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 'P001' AND stock > 0;
-- 检查影响行数,如果为 0 说明库存不足
-- rows_affected = 0 → 返回"库存不足",不写订单方案 B(中期):迁移到 ZooKeeper 分布式锁,利用 ZAB 强一致性消除主从切换的数据丢失问题。
方案 C(长期):接受 Redis 锁的概率性失效,将数据库层的原子操作作为强一致性保障,Redis 锁作为”减少数据库压力”的优化手段,而非”唯一的互斥保障”。
教训:不要将分布式锁作为数据一致性的唯一保障。数据库层面的约束(唯一索引、乐观锁 CAS)应该作为最后一道防线,即使分布式锁偶尔失效,数据也不会损坏。
4.2 故障案例二:看门狗续期失效导致并发执行
故障背景:某支付系统使用 Redisson 的可重入锁,默认看门狗 TTL 30 秒。某天凌晨进行大批量对账操作,Full GC 频繁发生。
故障过程:
T0: 对账线程 Thread-1 加锁成功,看门狗启动(每 10 秒续期一次)
T10: Thread-1 正常执行,看门狗续期成功(TTL 重置为 30 秒)
T20: Thread-1 正常执行,看门狗续期成功
T25: JVM 发生 Full GC,所有线程暂停(包括看门狗线程)
T50: [锁 TTL 在 T40 时过期,此时 Full GC 已持续 25 秒,超过剩余 TTL 10 秒]
T52: Thread-2 加锁成功(Thread-1 的锁已过期)
T55: Full GC 结束,Thread-1 恢复执行
T55~T60: Thread-1 和 Thread-2 同时对同一批账单执行对账,产生重复对账记录
根本原因:Full GC 停顿时间(25 秒)超过了看门狗剩余的 TTL(10 秒),看门狗线程被 GC 暂停期间无法续期,锁自动过期。
修复方案:
方案 A(短期):增大看门狗 TTL(从 30 秒增加到 120 秒),给 GC 停顿留出更大的安全余量:
Config config = new Config();
config.setLockWatchdogTimeout(120000); // 120 秒,看门狗每 40 秒续期即使 GC 停顿 40 秒,锁仍然有 80 秒的剩余 TTL,续期可以在 GC 结束后恢复。
方案 B(中期):升级 JVM GC 算法为 ZGC,将 GC 停顿时间控制在 10ms 以内,从根本上消除 GC 停顿超过 TTL 的可能性。
方案 C(长期):在对账操作层面实现幂等性(对账记录中添加唯一约束),即使偶发的并发执行也不会产生重复记录:
CREATE UNIQUE INDEX uk_reconciliation ON reconciliation_records (batch_id, account_id);
-- INSERT IGNORE INTO reconciliation_records 即使重复也不会抛出错误教训:看门狗不是银弹,GC 停顿是它的死穴。在 Full GC 频繁的系统中,必须配合低延迟 GC 算法或业务层幂等性来共同保证安全。
4.3 故障案例三:锁泄漏(Lock Leak)
故障背景:某订单系统在某次发版后,发现系统 QPS 持续下降,订单创建接口响应时间从 100ms 升高到 5 秒以上,甚至超时。
故障排查:
# 通过 Redis CLI 扫描持续存在的锁 key
redis-cli SCAN 0 MATCH "order:create:*" COUNT 1000
# 发现大量 order:create:user:* 的 key,且 TTL 均为 -1(永不过期!)
redis-cli TTL order:create:user:123456
# 返回:-1(表示没有设置 TTL)所有正常的 Redisson 锁都应该有 TTL(由看门狗维护),TTL 为 -1 是异常状态,说明这些锁被手动 SET 写入,绕过了 Redisson 的 TTL 机制。
根因定位:某个同学在代码 review 中发现了一处”优化”代码:
// 错误代码(某次"优化"引入)
// 开发者误以为直接 SETNX 更简单,但忘记了 Redisson 锁是 Hash 结构
redisTemplate.opsForValue().setIfAbsent("order:create:user:" + userId, "locked");
// 没有设置 TTL!锁永远不会过期!这段代码使用了 Spring 的 redisTemplate 直接写入 String 类型的 key,没有设置 TTL。后续的 Redisson 加锁操作使用 exists() 检查 key 是否存在,发现 key 已存在(虽然是 String 类型而不是 Hash 类型),直接返回加锁失败,导致所有后续的加锁请求永远失败。
修复方案:
- 立即清理所有 TTL 为 -1 的异常锁 key:
# 扫描并删除 TTL = -1 的 order:create:* key(谨慎执行,先确认影响范围)
redis-cli --scan --pattern "order:create:*" | while read key; do
ttl=$(redis-cli TTL "$key")
if [ "$ttl" = "-1" ]; then
redis-cli DEL "$key"
echo "Deleted: $key"
fi
done-
代码规范:统一使用 Redisson 接口操作分布式锁,禁止通过
redisTemplate直接操作锁 key -
监控加固:增加告警规则,当
order:create:*的 key 出现 TTL = -1 时立即告警
教训:分布式锁的操作必须通过统一的封装层进行,禁止绕过锁框架直接操作 Redis/ZooKeeper。可以通过 Code Review 规范和静态代码分析工具(如 SpotBugs 自定义规则)来强制执行。
4.4 故障案例四:ZooKeeper 羊群效应导致 ZK 集群雪崩
故障背景:某平台使用朴素的 ZooKeeper 锁(监听单一锁节点,所有等待者同时竞争),某次高峰期出现 ZooKeeper 集群响应时间飙升,最终 Leader 失联,触发 Leader 选举,服务中断约 30 秒。
故障过程:
正常状态:约 500 个并发请求在等待同一把 ZK 锁
锁释放时:500 个客户端同时收到 Watch 通知
→ 500 个并发 EPHEMERAL 节点创建请求涌向 ZK Leader
→ ZK Leader 处理队列瞬间爆满(正常处理能力约 1000 TPS)
→ ZK 处理延迟飙升
→ 客户端 heartbeat 超时(因为 ZK 处理线程忙于处理锁请求,无法及时处理 heartbeat)
→ 大量 Session 超时,临时节点被删除
→ 又触发新一轮 Watch 通知
→ 雪崩!
根本原因:使用了朴素的”所有等待者监听同一节点”的 ZK 锁实现,锁释放时产生羊群效应,500 个并发唤醒请求瞬间压垮 ZK Leader。
修复方案:迁移到临时顺序节点 + 监听前驱节点的公平锁实现(使用 Apache Curator InterProcessMutex),彻底消除羊群效应。迁移后,即使有 500 个等待者,每次锁释放只唤醒 1 个客户端,ZK Leader 的负载平稳正常。
教训:不要自己实现 ZooKeeper 分布式锁,直接使用 Apache Curator。Curator 的实现已经处理了羊群效应、Watch 窗口期等所有边界情况,手工实现极易引入这类隐藏 Bug。
第 5 章 分布式锁的测试方法
5.1 单元测试:验证基本语义
@Test
public void testMutualExclusion() throws InterruptedException {
String lockKey = "test:mutex:lock";
AtomicInteger concurrentCount = new AtomicInteger(0);
AtomicBoolean mutexViolated = new AtomicBoolean(false);
int threadCount = 10;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock();
try {
// 进入临界区,验证同时只有一个线程
int current = concurrentCount.incrementAndGet();
if (current > 1) {
mutexViolated.set(true); // 互斥性被破坏!
}
Thread.sleep(50); // 模拟业务操作
concurrentCount.decrementAndGet();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
}).start();
}
latch.await(30, TimeUnit.SECONDS);
assertFalse("互斥性被破坏!", mutexViolated.get());
}5.2 混沌工程测试
在测试环境中模拟各种故障场景,验证系统的容错能力:
- Redis 主从切换:在持锁期间强制杀掉 Redis Master,验证系统行为
- 网络分区:使用
tc命令注入网络延迟,验证锁操作的超时处理 - GC 压力注入:使用
-XX:+UseSerialGC强制使用串行 GC,制造 Full GC 场景,验证看门狗续期 - 进程 Kill:在持锁期间
kill -9持锁进程,验证 TTL 过期后锁能被正确释放
参考资料
- Nygard, M.T. (2018). Release It! Design and Deploy Production-Ready Software (2nd ed.). Pragmatic Bookshelf. Chapter 5: Stability Patterns.
- Netflix Tech Blog. (2016). Lessons learned implementing distributed locking in Java. https://netflixtechblog.com/
- Uber Engineering Blog. (2019). How Uber uses distributed locking to prevent race conditions. https://eng.uber.com/
- Martin, R.C. (2008). Clean Code. Prentice Hall. Chapter 13: Concurrency.
- Redisson 官方文档:Troubleshooting. https://redisson.org/docs/troubleshooting/
- Apache Curator 官方文档:Best Practices. https://curator.apache.org/docs/
- Kleppmann, M. (2017). Designing Data-Intensive Applications. O’Reilly Media. Chapter 8 & 9.
思考题
- 分布式读写锁允许多个读操作并发执行但写操作排他。在’配置文件’场景中(频繁读取、偶尔更新),读写锁比互斥锁性能高很多。Curator 的
InterProcessReadWriteLock基于 ZooKeeper 实现——读锁创建read-前缀节点,写锁创建write-前缀节点。读锁等待前面的写锁节点删除,写锁等待前面的所有节点删除。这种实现的正确性如何保证?- 读写锁的’写饥饿’问题——如果持续有读请求,写请求可能一直等待。解决方案:写请求优先(新读请求在有等待的写请求时阻塞)。在什么业务场景下’写优先’是正确的策略?在什么场景下’读优先’更合适?
- 分布式读写锁的性能——每次读锁获取都需要与 ZooKeeper/etcd 通信。在高频读取场景中(每秒数千次),这个通信开销是否抵消了读写锁的并发优势?是否有’乐观读’(不加锁,读后验证数据是否变化)的替代方案?