1. 加锁过程
底层命令与数据结构
- redis 数据结构:使用 hash 结构存储锁信息,key 为锁名称,field 为客户端唯一标识(如
uuid + 线程id
),value 为锁的重入次数。 - lua 脚本原子性:通过 lua 脚本在 redis 中原子性执行加锁逻辑:
if (redis.call('exists', keys[1]) == 0) then redis.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) then redis.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) then return nil; end; local counter = redis.call('hincrby', keys[1], argv[3], -1); if (counter > 0) then redis.call('pexpire', keys[1], argv[2]); return 0; else redis.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 支持高可用场景,但需谨慎权衡一致性与性能。
到此这篇关于java常用分布式锁redisson的文章就介绍到这了,更多相关java分布式锁redisson内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论