JAVA常用分布式锁Redisson
1. 加锁过程
底层命令与数据结构
-
Redis 数据结构:使用 Hash 结构存储锁信息,Key 为锁名称,Field 为客户端唯一标识(如
UUID + 线程ID
),Value 为锁的重入次数。 -
Lua 脚本原子性:通过 Lua 脚本在 Redis 中原子性执行加锁逻辑:
if (redis.call('exists', KEYS[1]) == 0) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil; end; return redis.call('pttl', KEYS[1]);
-
若锁不存在(
exists
为 0)或属于当前线程(hexists
为 1),则增加重入次数并刷新过期时间。 -
若锁被其他线程占用,返回锁的剩余生存时间(TTL)。
-
可重入性
-
同一线程多次获取锁时,重入次数递增,确保不会因多次加锁导致死锁。
2. 锁自动续期(Watchdog 机制)
-
后台线程续期:加锁成功后,启动一个 Watchdog 线程(看门狗),定期(默认每 10 秒)检查锁是否仍被持有。
-
续期条件:仅当客户端仍持有锁且业务未完成时,通过
pexpire
命令将锁的过期时间重置为初始值(默认 30 秒)。 -
崩溃容错:若客户端崩溃,Watchdog 线程停止,锁最终因过期自动释放,避免死锁。
3. 释放锁
释放逻辑
-
Lua 脚本原子释放:
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) thenreturn nil; end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) thenredis.call('pexpire', KEYS[1], ARGV[2]);return 0; elseredis.call('del', KEYS[1]);redis.call('publish', KEYS[2], ARGV[1]);return 1; end; return nil;
-
减少重入次数,若次数归零则删除锁,并通过 Pub/Sub 通知等待线程。
-
确保只有锁的持有者能释放锁,避免误删。
-
4. 锁竞争与等待
-
自旋重试:若锁被占用,客户端进入循环,间隔性尝试加锁。
-
Pub/Sub 订阅通知:通过订阅锁释放事件(
redisChannel
),避免频繁轮询。当锁释放时,Redis 发布消息通知等待线程竞争锁,减少无效请求。
5. 高可用与容错
Redis 部署模式
-
单节点模式:简单但存在单点故障风险。
-
主从/集群模式:使用
RedissonMultiLock
实现 RedLock 算法(需多个独立 Redis 节点):-
向所有节点顺序申请锁。
-
当多数节点加锁成功且总耗时小于锁超时时间时,认为加锁成功。
-
规避主从切换导致锁丢失的问题,但需权衡性能和一致性。
-
6. 关键注意事项
-
业务执行时间:业务逻辑必须在锁的过期时间内完成,否则锁可能提前释放。
-
时钟同步问题:在 RedLock 中,若 Redis 节点间时钟不同步,可能导致锁失效。
-
网络延迟:极端情况下,锁可能被多个客户端同时持有(需结合业务幂等性处理)。
总结
Redisson 分布式锁通过 Lua 脚本的原子性、可重入设计、Watchdog 自动续期和 Pub/Sub 通知机制,实现了高效的分布式锁管理。其核心优势在于:
-
避免误删锁(仅持有者可释放)。
-
支持可重入,适应复杂业务逻辑。
-
自动续期防止业务未完成时锁过期。
-
通过 RedLock 支持高可用场景,但需谨慎权衡一致性与性能。