分布式锁最适合用真实业务来理解。抢券场景:多个用户同时抢优惠券,系统要保证库存不能被扣成负数。
如果代码部署在单机上,synchronized 可能够用;但服务一旦集群部署到多个 Tomcat 实例,本地锁就只能锁住当前 JVM,挡不住其他节点上的线程。
这就是 Redis 分布式锁的使用场景:集群环境下,让多个服务实例争抢同一把锁。
一、为什么抢券会超卖?
普通抢券逻辑大概是:
java
Integer num = redisTemplate.opsForValue().get("num");
if (num == null || num <= 0) {
throw new RuntimeException("优惠券已抢完");
}
num = num - 1;
redisTemplate.opsForValue().set("num", num);
两个线程可能同时读到库存 1,然后都判断库存充足,最终都扣减成功。
Redis 库存 线程2 线程1 Redis 库存 线程2 线程1 查询库存 = 1 查询库存 = 1 扣减为 0 扣减为 0
问题不在 Redis,而在"查询库存"和"扣减库存"不是一个不可被打断的整体。
二、SETNX 实现分布式锁
Redis 实现分布式锁主要依赖 SETNX,也就是 SET if Not eXists:key 不存在时才能设置成功。
现在更推荐用一条 SET 命令同时完成加锁和设置过期时间:
text
SET lock:coupon threadId NX EX 10
含义是:只有 lock:coupon 不存在时才设置成功,并且 10 秒后自动过期。
成功
失败
尝试获取锁
SET NX EX 是否成功
执行业务
释放锁
等待或重试
释放锁时也要小心,必须确认这把锁是自己的,再删除。否则线程 A 的锁过期后,线程 B 获取了新锁,线程 A 再执行删除就可能误删线程 B 的锁。
所以释放锁通常要用 Lua 脚本保证"判断锁标识 + 删除锁"是原子操作。
三、Redisson 做了哪些封装?
Redisson 是 Redis 分布式锁的成熟封装。课件里提到几个重点:
- 底层基于
SETNX和 Lua 脚本保证原子性。 - 使用 WatchDog 给锁自动续期。
- 使用 Hash 结构记录线程标识和重入次数,实现可重入。
- 支持锁重试等待。
四、WatchDog:锁怎么自动续期?
如果业务执行时间超过锁过期时间,锁提前释放,就会有并发问题。Redisson 的 WatchDog 会在持有锁的线程还没执行完时自动续期。
Redis WatchDog 业务线程 Redis WatchDog 业务线程 加锁成功 定期检查锁仍被当前线程持有 续期锁过期时间 业务完成后释放锁
默认情况下,WatchDog 会周期性续期,避免业务还没执行完锁就过期。
五、可重入锁:为什么用 Hash?
可重入指的是同一个线程已经拿到锁后,可以再次获取同一把锁。
Redisson 会用 Hash 结构记录线程 id 和重入次数:
text
heimalock
thread-1 -> 2
同一个线程再次加锁时,重入次数加 1;释放锁时,重入次数减 1;只有减到 0 才真正删除锁。
线程第一次加锁
重入次数 = 1
线程再次加锁
重入次数 = 2
释放一次,重入次数 = 1
再次释放,删除锁
六、主从一致性和 RedLock
Redis 主从架构下,分布式锁还有一个风险:线程在 Master 上加锁成功,但锁还没同步到 Slave,Master 就宕机了。Slave 被提升为新 Master 后,其他线程可能又获取到同一把锁。
RedLock 的思路是:不要只在一个 Redis 实例上加锁,而是在多个独立 Redis 节点上加锁,只要超过 n / 2 + 1 个节点成功,就认为加锁成功。
但课件也提醒:RedLock 性能较差,不是默认推荐方案。如果业务真的要求极强一致,建议考虑 ZooKeeper 这类更适合强一致锁的方案。
七、面试回答模板
可以这样答:
我们项目里在抢券、幂等性或集群定时任务场景用过分布式锁。Redis 分布式锁底层主要依赖 SET NX EX 加锁,并用 Lua 脚本保证释放锁时判断和删除的原子性。实际项目里一般使用 Redisson,它封装了 WatchDog 自动续期、可重入锁、锁重试等能力。Redisson 的可重入基于 Hash 结构记录线程标识和重入次数。至于主从一致性问题,RedLock 可以缓解但性能较差,如果业务必须强一致,应该考虑 ZooKeeper。