随着系统架构从单机向分布式演进,线程同步问题不再局限于单个 JVM 内部。在多个节点协同工作的场景下,传统的 synchronized 或 ReentrantLock 已无法满足跨进程、跨机器的资源竞争控制需求。我们需要一种能够协调不同节点间状态的机制——分布式锁。
分布式锁的核心挑战与特性
分布式锁本质上是一种用于控制共享资源访问的机制,确保同一时刻只有一个客户端能持有锁。它通常依赖外部组件(如 Redis、Zookeeper)来实现高可用和一致性。一个健壮的分布式锁方案需要满足以下关键特性:
- 互斥性:保证任意时刻只有一个线程持有锁。
- 可重入性:允许同一线程多次获取锁而不死锁。
- 锁超时:设置过期时间防止死锁,避免异常导致锁无法释放。
- 高性能与高可用:加解锁操作需高效,且服务本身具备容灾能力。
Redis 实现思路与基础缺陷
Redis 因其高性能和低延迟,常被选作分布式锁的实现载体。最直观的做法是利用 SETNX(Set if Not Exists)命令配合过期时间。
基础实现流程
- 获取锁:尝试执行
SET key value NX EX seconds。若 key 不存在则写入成功并设置 TTL;若存在则失败。 - 释放锁:业务完成后执行
DEL key删除键值对。 - 防死锁:依靠 TTL 自动清理未释放的锁。
虽然逻辑简单,但这种粗糙实现在生产环境中隐患重重:
- 非原子性问题:
SETNX和EXPIRE分两步执行。若中间发生崩溃,锁可能没有过期时间,导致永久死锁。 - 误删风险:若线程 A 持有锁期间阻塞,TTL 到期后锁被释放,线程 B 获取了锁。当线程 A 恢复后执行
DEL,会误删线程 B 的锁。 - 业务超时:若业务执行时间超过 TTL,锁会自动释放,引发并发冲突。
健壮方案的构建路径
要解决上述问题,我们需要引入唯一标识符、Lua 脚本以及看门狗机制。
1. 防止误删:引入唯一标识
为了解决误删锁的问题,我们在获取锁时存储一个唯一标识(如 UUID),并在释放前校验该标识是否匹配。只有持有者才能解锁。
String threadId = UUID.randomUUID().toString();
// 尝试加锁,value 为唯一标识
boolean locked = jedis.set(key, threadId, SetParams.setParams().nx().ex(30));
if (locked) {
try {
// 执行业务逻辑
} finally {
// 校验标识后再释放
if (threadId.equals(jedis.get(key))) {
jedis.del(key);
}
}
}


