随着系统架构从单机向分布式演进,线程同步问题不再局限于单个 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);
}
}
}
这种方式不仅解决了误删问题,也为实现可重入锁提供了基础:只需在获取锁时判断当前标识是否属于自己,若属于则自增计数即可。
2. 保证原子性:使用 Lua 脚本
将 SETNX 和 EXPIRE 封装在一个 Lua 脚本中,利用 Redis 的单线程执行特性保证原子性。同样,释放锁时的校验与删除也应通过脚本完成。
加锁脚本示例:
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
return redis.call('expire', KEYS[1], tonumber(ARGV[2]))
end
return 0
解锁脚本示例:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0
在 Java 中调用这些脚本,可以彻底消除因网络波动或服务重启导致的非原子性风险。
3. 处理超时:看门狗续期机制
如果业务执行时间不确定,单纯延长 TTL 会导致其他线程长时间等待。更优雅的方案是'看门狗'机制:启动一个守护线程定期检查锁的状态,若发现即将过期则自动续期。
实现要点:
- 获取锁成功后启动定时任务。
- 每隔一段时间(如 TTL 的 2/3)检查锁是否存在且仍属于当前线程。
- 若存在则发送
PEXPIRE命令续期。 - 业务结束释放锁时,同时关闭守护线程。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
if (threadId.equals(jedis.get(key))) {
jedis.expire(key, 30);
}
}, 25, 25, TimeUnit.SECONDS);
try {
// 执行业务逻辑
} finally {
if (threadId.equals(jedis.get(key))) {
jedis.del(key);
}
scheduler.shutdown();
}
这种机制确保了只要业务还在运行,锁就不会意外释放,有效避免了因线程阻塞导致的并发问题。
总结
构建高可靠的分布式锁并非一蹴而就,需要在互斥性、原子性和时效性之间找到平衡。基于 Redis 的方案中,通过唯一标识防止误删,利用 Lua 脚本保证原子操作,配合看门狗机制处理超时,能够覆盖绝大多数生产场景。在实际落地时,还需结合具体业务对性能的要求,选择合适的锁粒度与超时策略,确保系统在分布式环境下的稳定运行。


