一、实际问题
分布式锁要解决的核心问题,其实就一句话:在多实例部署下,保证同一时刻只有一个节点能执行某段业务逻辑。
举个最经典的例子------库存扣减。假设你的商品服务部署了 3 个实例,用户下单时调用扣库存接口。如果没有锁,3 个实例可能同时读到库存为 1,各自判断"还有货",然后都去扣减,结果就是超卖了。本地锁(synchronized、ReentrantLock)只能保证单个 JVM 内的线程安全,管不到其他实例,所以需要一把"所有实例都能看到"的锁,这就是分布式锁。
再往深了想,你会发现分布式锁本质上是在解决跨进程的互斥访问问题。只要是多节点部署 + 共享资源操作,都可能用到它,常见的实际场景包括:
防重复提交/重复操作------用户连点两次"支付"按钮,两个请求可能打到不同实例上,分布式锁保证同一笔订单同一时刻只能被一个请求处理。
定时任务的幂等执行------多个服务节点都跑了同一个定时任务,分布式锁保证同一时刻只有一个节点真正执行任务逻辑,避免重复跑批。
限流与资源竞争------比如对第三方接口的调用频率有上限,多个实例需要共享一个计数器,用分布式锁串行化访问。
状态机的单向流转------订单从"待支付"变成"已支付",这个状态变更必须是原子的,不能被并发请求搞乱。
本质上,分布式锁就是单机锁在分布式环境下的延伸。单机锁靠操作系统提供的内存互斥原语(CAS、monitor 等)来实现,而分布式锁则需要一个所有节点都能访问到的共享协调方(Redis、ZooKeeper、数据库等)来充当"裁判"。选 Redis 是因为它快、简单;选 ZooKeeper 是因为它强一致;选数据库则是因为大多数系统已经有了,不需要额外引入组件------本质上都是在"性能"和"一致性"之间做权衡。
Q1:Redis 如何实现分布式锁?
候选人: 嗯,在redis中提供了一个命令SETNX(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。
回答解释:
最直观的思路是利用 Redis 的 SETNX(SET if Not Exists)命令。由于 Redis 采用单线程模型处理命令,当多个客户端同时对同一个 key 执行 SETNX 时,只有一个客户端能成功设置值,其余客户端都会失败。在 key 未被删除或过期之前,其他客户端都无法获取到锁,这就实现了互斥。
# 最原始的加锁方式
SETNX lock_key unique_value
EXPIRE lock_key 30
但这里有一个明显的问题:加锁和设置过期时间是两条独立的命令,不具备原子性。如果 SETNX 成功后进程崩溃,EXPIRE 还没来得及执行,锁就永远不会释放,其他客户端将永远被阻塞。
所以,redisson底层还会用到lua脚本,以保证原子性。
Q2:如何控制锁的有效时长?业务没执行完锁就过期了怎么办?
候选人 :嗯,的确。redis的SETNX指令不好控制这个问题。我们当时采用的是redis的一个框架Redisson实现的。在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成的时候,Redisson会引入一个看门狗机制。就是说,每隔一段时间就检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。当业务执行完成之后,需要使用释放锁就可以了。还有一个好处就是,在高并发下,一个业务有可能会执行很快。客户1持有锁的时候,客户2来了以后并不会马上被拒绝。它会自旋不断尝试获取锁。如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
回答解释:
如果直接在SETNX创建锁的时候手动设定锁的过期时间,我们就需要猜测业务执行花费的时间,存在一个两难:设短了,业务没执行完锁就释放,导致并发问题;设长了,即使客户端崩溃,其他线程也要等很久才能获取锁。
所以,我们要用到Redisson
Redisson 的解决方案:看门狗(Watchdog)机制。

Redisson的解决方案是,用一个Watch dog来盯着这把锁。线程执行完业务代码,释放锁的同时,也会把狗放掉,如果线程死了,锁将会在狗的releaseTime后释放掉,避免产生死锁。
如果在这个时候,又有一个线程来申请这把锁,Redisson不会直接将这给线程给拒绝掉,而是让其不断的循环请求锁,在一定循环次数后才会停止。在高并发环境下,这样做的好处就是可以提高锁的利用效率,提升性能。
使用 Redisson 加锁时,如果不手动指定 leaseTime,Redisson 会默认启用看门狗。其核心逻辑如下:
-
默认锁的超时时间为 30 秒(lockWatchdogTimeout)。
-
加锁成功后,Redisson 会启动一个后台定时任务(看门狗),每隔 lockWatchdogTimeout / 3(即 10 秒)执行一次续期操作。
-
续期时会检查当前线程是否仍然持有锁,如果是,则将锁的过期时间重置为 30 秒。
-
当业务执行完毕主动释放锁后,看门狗定时任务也会被取消。
RLock lock = redisson.getLock("myLock");
// 不指定 leaseTime,自动启用看门狗
lock.lock();
try {
// 执行业务逻辑,看门狗会自动续期
doBusiness();
} finally {
lock.unlock();
}
看门狗续期的底层是通过 Lua 脚本实现的,保证了判断锁归属和重置过期时间的原子性:
-- 看门狗续期的核心 Lua 脚本(简化版)
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
redis.call('pexpire', KEYS[1], ARGV[1])
return 1
end
return 0
Q3:Redisson 的分布式锁是可重入的吗?
候选人:嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。
回答解释:
可重入的意思是:同一个线程可以对同一个锁多次加锁而不会死锁。实现原理是:
-
锁在 Redis 中以 Hash 结构存储,key 是锁名称,field 是线程唯一标识(UUID + threadId),value 是重入次数。
-
当同一线程再次加锁时,Redisson 通过 Lua 脚本判断 Hash 中是否已存在当前线程的 field,如果存在则将 value 加 1。
-
每次 unlock() 时 value 减 1,减到 0 时真正删除锁并通知等待线程。
-- 可重入加锁 Lua 脚本(简化版)
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
-- 当前线程已持有锁,重入次数 +1
redis.call('hincrby', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return 1
else
-- 锁不存在,首次加锁
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return 1
end
return 0
end
对应的 Redis 数据结构示意:
HGETALL myLock → field: "a8b7c6d5-1234:thread-1" value: 3 -- 同一线程重入了 3 次
可重入设计有效避免了同一线程在嵌套调用中因重复加锁而导致的死锁问题。
Q4:Redis 分布式锁能解决主从一致性问题吗?
候选人:这个是不能的。比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时如果当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用Redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个Redis实例上创建锁,应该是在多个Redis实例上创建锁,并且要求在大多数Redis节点上都成功创建锁,红锁中要求是Redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。
回答解释:
不能。 这是 Redis 分布式锁的一个已知缺陷。
Redis 的主从复制是异步的。考虑以下时序:
线程1 → 在 Master 上加锁成功
↓ (异步复制尚未完成)
Master 宕机,Slave 被提升为新 Master
线程2 → 在新 Master 上加锁成功 ← 此时两个线程同时持有锁!
RedLock(红锁) 是 Redisson 针对这个问题提出的方案:
- 部署 N 个独立的 Redis 实例(推荐 5 个)。
- 客户端向所有实例尝试加锁,只有当超过半数(N/2 + 1)的实例加锁成功时,才认为锁获取成功。
- 这样即使某个节点宕机,也不会影响锁的整体安全性。
然而,RedLock 在实际生产中存在很大争议:
- 性能开销大:每次加锁/解锁都要与 N 个实例通信。
- 运维成本高:需要维护多个独立 Redis 实例。
- 时钟依赖问题:算法依赖各节点时钟同步,如果某节点发生时钟跳跃,可能导致锁失效。Martin Kleppa(Raft 作者)曾撰文详细论证过这一问题。
- 官方已标记为废弃:Redisson 官方文档中已将 RedLock 标记为不推荐使用。
Q5:如果业务要求强一致性,该怎么办?
如果业务场景对数据一致性有严格要求(如金融交易、订单状态流转),建议放弃 Redis 分布式锁,转向 ZooKeeper。
ZooKeeper 基于 ZAB 协议(类 Paxos),天然保证强一致性:
-
所有节点通过选举产生 Leader,写请求必须由 Leader 处理。
-
数据变更需要多数派节点确认后才算成功,不会出现两个节点同时持有一把锁的情况。
-
使用临时顺序节点实现锁,还能天然支持可重入和公平锁。
// Curator 框架实现 ZK 分布式锁(示例)
InterProcessMutex lock = new InterProcessMutex(client, "/locks/myLock");
lock.acquire(10, TimeUnit.SECONDS);
try {
doBusiness();
} finally {
lock.release();
}
简单来说:追求高性能选 Redis,追求强一致选 ZooKeeper。