
## 核心问题:Redisson分布式锁为何用HINCRBY自增减一
- 分布式锁4大挑战
- 身份隔离:需识别同一客户端/线程多次加锁
- 可重入性:需安全记录重入次数
- 并发安全:避免竞态条件
- 优雅释放:防止误删/提前解锁
- HINCRBY核心作用
- 原子性:Redis单线程模型保障操作无冲突
- 计数管理:加锁+1、解锁-1,记录重入次数
- 状态隔离:结合Hash结构,以UUID:threadId为字段
- 释放判断:计数归零才删除锁Key
## Redisson实现原理
- 数据结构:Redis Hash类型
- Key:锁名称(redisson_lock:{mylock})
- Field:客户端/线程唯一标识(UUID:threadId)
- Value:重入次数(整数)
- 核心操作
- 加锁:Lua脚本+ HINCRBY +1,首次加锁初始化计数
- 解锁:Lua脚本+ HINCRBY -1,计数归零则删Key
- 看门狗:未指定leaseTime时自动续期
## 替代方案缺陷
- 先GET再INCR再SET:非原子操作,计数混乱
- String类型INCR:无法区分不同客户端
- 多Key记录:管理复杂,内存开销大
- WATCH+MULTI+EXEC:高并发下重试频繁,性能差
## 实践应用
- 依赖引入:Maven依赖(redisson 3.24.1)
- 核心API:RLock接口(tryLock/unlock)
- 关键配置:合理设置超时时间、启用看门狗
- 注意事项:加解锁次数匹配、避免长时间持锁
在分布式系统中,多个服务实例可能同时访问共享资源。为保证数据一致性,需使用分布式锁进行同步控制。然而,传统单机锁(如Java中的synchronized或ReentrantLock)无法跨JVM生效,因此必须引入分布式协调机制。
KEY: "redisson_lock:{mylock}"
VALUE: Hash 结构
Field: "UUID:threadId" → 表示锁的持有者
Value: 重入次数(整数)
| 4大挑战 | 解法 | HINCRBY 的作用 |
|---|---|---|
| 如何识别同一客户端的多次加锁? | 使用线程唯一标识作为 Hash 字段(如 UUID:threadId) |
提供独立命名空间,实现不同线程间的状态隔离 |
| 如何实现重入计数? | 将 Value 设为整型,记录进入次数 | 支持通过 HINCRBY 安全累加(+1)或递减(-1) |
| 如何保证并发安全? | 直接依赖 Redis 单线程模型下的原子命令 | HINCRBY 天然避免竞态条件,无需额外同步机制 |
| 如何优雅释放锁? | 只有当计数归零时才删除 Key | 利用 -1 操作后判断结果值,确保不误删其他重入的锁 |
| 缩写片段 | 完整单词 | 含义 |
|---|---|---|
| H | Hash | 哈希(Redis 的哈希类型) |
| INCR | Increment | 增量 / 增加(动词) |
| BY | By | 以… 为单位(介词) |
HINCRBY 本质是 Hash Increment By 的缩写(核心三个单词),字面意为:对哈希(Hash)中的字段值,按指定数值(BY 后跟的参数)进行增量(INCR)操作。

graph TB
Start[客户端调用 lock/tryLock] --> CheckLeaseTime{是否指定 leaseTime?}
CheckLeaseTime -->|是| TryLock1[执行 Lua 加锁脚本]
CheckLeaseTime -->|否| TryLock2[使用 watchdog 默认时间]
TryLock1 --> EvalLua1[Lua: 检查锁是否存在]
TryLock2 --> EvalLua1
EvalLua1 --> LuaCheck{锁状态?}
LuaCheck -->|不存在或当前线程持有| Acquire[HINCRBY 增加计数<br/>PEXPIRE 设置过期]
LuaCheck -->|被其他线程持有| ReturnTTL[返回剩余 TTL]
Acquire --> AcquireSuccess{加锁成功}
AcquireSuccess -->|指定 leaseTime| NoWatchdog[不启动看门狗]
AcquireSuccess -->|未指定 leaseTime| StartWatchdog[启动看门狗任务]
StartWatchdog --> WatchdogSchedule[每 internalLockLeaseTime/3<br/>执行续期]
WatchdogSchedule --> RenewCheck{锁还持有?}
RenewCheck -->|是| RenewLua[Lua: HEXISTS + PEXPIRE<br/>延长到 watchdogTimeout]
RenewCheck -->|否| StopWatchdog[停止看门狗]
RenewLua --> WatchdogSchedule
ReturnTTL --> Subscribe[订阅解锁通知 channel]
Subscribe --> Wait[等待信号或超时]
Wait --> Retry{是否超时?}
Retry -->|否| TryAgain[重新尝试获取锁]
Retry -->|是| Failed[获取锁失败]
TryAgain --> EvalLua1
NoWatchdog --> Unlock[业务执行完毕]
StopWatchdog --> Unlock
Unlock --> UnlockLua[执行 Lua 解锁脚本]
UnlockLua --> UnlockCheck{是否当前线程持有?}
UnlockCheck -->|否| Error[抛出 IllegalMonitorStateException]
UnlockCheck -->|是| DecrCount[HINCRBY 减少计数]
DecrCount --> CountCheck{计数 > 0?}
CountCheck -->|是| ResetExpire[PEXPIRE 重置过期<br/>可重入锁未完全释放]
CountCheck -->|否| DelLock[DEL 删除锁<br/>PUBLISH 通知等待者]
DelLock --> CancelWatchdog[取消看门狗任务]
CancelWatchdog --> End[解锁完成]
ResetExpire --> End
Failed --> End
Error --> End
graph LR
A[加锁成功<br/>未指定 leaseTime] --> B[scheduleExpirationRenewal]
B --> C[LockRenewalScheduler.renewLock]
C --> D[创建/获取 LockTask]
D --> E[添加到 name2entry Map]
E --> F[启动定时任务]
F --> G[每 watchdogTimeout/3 执行]
G --> H[批量续期 Lua 脚本]
H --> I{锁还持有?}
I -->|是| J[延长过期时间]
I -->|否| K[从任务中移除]
J --> G
K --> L[解锁时取消任务]
| 特性 | 非公平锁 (RedissonLock) | 公平锁 (RedissonFairLock) |
|---|---|---|
| 获取策略 | 抢占式,先到先得 | FIFO 队列,严格按申请顺序 |
| 数据结构 | 1 个 Hash | 3 个结构:Hash + List + ZSet |
| 性能 | 高,吞吐量大 | 相对较低,需维护队列 |
| 公平性 | 无保证,可能饥饿 | 严格保证顺序 |
| Redis 命令 | HEXISTS, HINCRBY, PEXPIRE | 额外使用 LPUSH, RPUSH, ZADD, ZREM |
| 适用场景 | 高并发、对顺序无要求 | 需要严格顺序的场景 |
| 锁类型 | Redis Key | 用途 | 内容 |
|---|---|---|---|
| 非公平锁 | {lockName} |
锁本身 | Hash: {threadId: 重入次数} |
| 非公平锁 | redisson_lock__channel:{lockName} |
通知channel | 所有线程订阅 |
| 公平锁 | {lockName} |
锁本身 | Hash: {threadId: 重入次数} |
| 公平锁 | redisson_lock_queue:{lockName} |
等待队列 | List: 按顺序存储等待的threadId |
| 公平锁 | redisson_lock_timeout:{lockName} |
超时集合 | ZSet: {threadId: timeout} |
| 公平锁 | redisson_lock__channel:{lockName}:{threadId} |
专属通知channel | 每个线程独立订阅 |
根据超时时间唤醒,或者订阅唤醒