Redisson分布式锁的实现原理与加锁机制(含可视化)

概述

Redisson 基于 Redis 提供了健壮的分布式锁实现,核心由原子化 Lua 脚本、可重入计数、看门狗自动续期、自旋重试、以及在集群环境下的路由与脚本缓存优化共同构成。本文在校正与扩展原有资料的基础上,系统性梳理其原理与工程实践,并附带配色优化的 Mermaid 图以提升可读性。

简介与项目背景

  • 业务痛点:多实例并发下的资源互斥、临界区保护、幂等控制。
  • 传统问题:仅依赖 setnx/expire 容易出现“非原子”与“误释放”问题。
  • Redisson 解法:以 Lua 保证原子性,以哈希结构记录重入,以看门狗解决长耗时,以自旋保证获得锁,以 EVALSHA 提升脚本执行性能。

名词解释

  • 客户端ID:通常为 UUID + “:” + threadId,用于标识“谁持有锁”。例如 UUID.randomUUID() 与线程ID拼接。
  • 可重入锁:同一线程在持有锁的情况下可以再次进入临界区,计数 +1;释放时计数 -1 直至 0 才真正释放。
  • 看门狗(watchdog):锁持有期间的后台续期任务,默认每隔 10 秒检查并延长过期。
  • EVAL/EVALSHA:Redis 执行 Lua 脚本的两种方式;EVALSHA 通过脚本摘要命中缓存,减少传输与编译开销。参见 EVALSHA
  • TTL/PTTL:键剩余存活时间(毫秒级为 PTTL);用于判定何时可以重试加锁。

架构原理图

Redis Cluster

spin

spin

Client1 Redisson

Master

Client2 Redisson

Slave

Master

Slave

Watchdog Auto-Renew

Watchdog Auto-Renew

Lock Key K

加锁机制(核心流程校正与拆解)

  1. 选路到 Master:在 Redis Cluster 中,客户端根据 key 的哈希槽选择目标 Master 节点,避免跨节点操作。
  2. 原子加锁 Lua 逻辑:使用 Lua 保证“检查 + 设置 + 过期 + 重入计数”的整体原子性:
    • 若 key 不存在:创建哈希结构,字段为客户端ID,值为重入计数 1;同时设置过期(默认 30s)。
    • 若 key 存在且字段为本客户端ID:执行可重入,计数 +1,并刷新过期。
    • 若 key 存在但持有者非本客户端:返回剩余 TTL(PTTL),用于指导下一次重试间隔。
  3. 自旋与退避:客户端循环尝试加锁,可根据 PTTL 实现“精准睡眠”,避免无意义忙等。
  4. 看门狗续期:锁成功持有后,后台任务每 10 秒续期一次,确保业务超过初始过期时间仍不丢锁。
  5. 释放锁:减计数;当减至 0 时删除 key;非持有者不可释放,避免“误删他人锁”。

Lua 逻辑语义解读(无代码版)

  • exists(KEYS[1]) == 0 → hset(KEYS[1], ARGV[2], 1); pexpire(KEYS[1], ARGV[1]); return nil
  • hexists(KEYS[1], ARGV[2]) == 1 → hincrby(KEYS[1], ARGV[2], 1); pexpire(KEYS[1], ARGV[1]); return nil
  • 其他情况 → return pttl(KEYS[1]) 以指导等待时长

EVALSHA 优化机制与伪代码示意

  • 首次执行:发送完整 Lua,Redis 缓存并返回 SHA1。
  • 后续执行:优先以 SHA1 执行;若未命中缓存,再回退完整 Lua 并重建缓存。
  • 客户端侧:预计算并本地缓存内置 Lua 的 SHA1 摘要,减少运行时计算。
  • 集群侧:每个 Master 独立缓存脚本;客户端需针对不同节点维护脚本摘要命中。
  • 伪代码引用:redis.evalsha() / redis.eval() / 异常 NoScriptException

续期机制(Watchdog)工作原理

  • 触发条件:当线程成功获得锁且仍在执行临界区。
  • 执行频率:默认每 10 秒(可配置),调用 pexpire 刷新 TTL 至设定值(如 30 秒)。
  • 结束条件:线程完成业务并释放锁,或线程中断/应用停止(需配合健壮的释放逻辑)。
  • 设计要点:续期必须与“持有者身份”绑定,防止非持有者续期导致锁僵死。

可重入锁实现要点

  • 结构选择:使用 Redis Hash,field = 客户端ID,value = 重入计数。
  • 加锁路径:本客户端持有时计数 +1;初次持有时设置过期。
  • 释放路径:本客户端计数 -1;当计数为 0 → del 整个 key。
  • 并发安全:所有操作在 Lua 中原子执行,避免并发条件竞争。

释放锁的安全性

  • 核验持有者:仅当 hexists(KEYS[1], ARGV[2]) == 1 才允许 hincrby -1 或 del。
  • 防误删:若不是持有者,直接返回,避免删除他人锁导致“临界区暴露”。
  • 最终删除:计数归零时 del key;否则继续保持锁与 TTL。

时序图(获取锁→续期→释放)

Watchdog Redis Master Redisson 客户端1 Watchdog Redis Master Redisson 客户端1 alt [K 不存在或可重入] [被他人持有] 请求加锁(K) 1 路由至 Master 并执行 Lua 2 成功(nil) 3 获得锁 4 每10秒 pexpire(K, TTL) 5 返回 PTTL 6 自旋等待(依据 PTTL) 7 释放锁 8 Lua 验证持有者并减计数/删除 9 成功 10

容错与边界场景

  • 时钟漂移与长 GC:续期线程被阻塞可能导致 TTL 过期,需合理设置过期与续期间隔,并监控 GC 暂停。
  • 网络分区:客户端误以为释放失败或续期失败,建议增加重试与幂等保护。
  • 主从复制延迟:锁写入在主库,读写一致性以主为准,避免从库读取造成误判。
  • 进程崩溃:看门狗停止,TTL 到期自动释放,保证锁不会永久占用。
  • 阻塞临界区:单点长事务应尽量分解,缩短持锁时间,避免对系统整体吞吐的影响。

性能与实践建议

  • 设置合理 TTL:初始 30s 仅为默认;结合业务耗时与续期频率动态评估。
  • 精准退避:根据返回的 PTTL 进行 sleep,降低无效重试。
  • 指标与日志:记录“加锁成功率、等待时长、持锁时长、续期次数、释放失败数”等关键指标。
  • 统一封装:对外暴露统一的加锁接口,内部包含重试、续期、释放、异常处理与监控。

高级话题与对比

  • 公平锁 vs 非公平锁:Redisson 默认非公平;公平锁需要排队队列语义,吞吐与延迟权衡。
  • 读写锁:读锁共享、写锁互斥;适用于读多写少场景,但实现复杂度更高。
  • RedLock(多主多副本):理论争议较大,工程实践需谨慎评估网络分区与延迟,参考官方讨论。

与 Java API 的对应关系(名称引用)

参考资料与权威文献

  • Redis 官方 Lua/EVAL/EVALSHA 文档:https://redis.io/docs/latest/develop/reference/scripting/
  • Redis Keyspace TTL/PTTL 文档:https://redis.io/commands/pttl/
  • Redisson 官方文档与源码(GitHub):https://github.com/redisson/redisson
  • Redis Cluster 介绍与槽位:https://redis.io/docs/latest/operate/cluster/
  • RedLock 原论文与讨论(antirez):https://redis.io/docs/latest/develop/use/patterns/distributed-locks/

速记口(便于复述与面试)

  • 一句概括:Lua 原子 + Hash 重入 + Watchdog 续期 + EVALSHA 缓存 + 自旋退避 + 主库路由。
  • 关键流程:exists → hset/hexists → hincrby → pexpire → 返回 pttl → 自旋 → 释放减计数/删除。
  • 风险点:长 GC/网络分区/主从延迟/错误释放;监控与退避不可少。
  • 配置要点:合理 TTL 与续期频率;精准自旋;健壮释放;统一封装与度量。

结语

Redisson 分布式锁以工程化手段解决了“原子性、持有者识别、长耗时续期、重入与释放安全”等关键问题。理解其在集群中的脚本缓存与路由细节、在边界场景下的容错策略,才能做到知其然并知其所以然,在真实生产环境中稳定发挥作用。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐