【如何使用Redis实现分布式锁详解讲解】
Redis 实现分布式锁:原理与功能点解析(类似 Redisson 实现)
在分布式系统中,锁是保证多个节点访问共享资源时数据一致性的关键工具。Redis 因其单线程、高性能和原子操作特性,被广泛用于实现 分布式锁。本文将从功能点出发,讲解 Redis 如何实现一把完整的分布式锁,并与 Redisson 的实现机制对齐。
前言
要实现使用Redis实现一个分布式锁不是简单的使用
setnx
命令就可以实现的,还需要从可重入性、死锁避免、解锁时候释放的是自己锁而不是其他线程的、业务还在执行锁时间过期,锁释放,需要保证锁续期机制。以及集群模式,如何保证Redis宕机后在其他 Redis 实例中锁还是存在的问题。
一、功能目标
一个高质量的分布式锁需要具备以下特性:
- 互斥性:同一时间只有一个客户端持有锁。
- 可重入性:持有锁的客户端可以多次获取而不会阻塞。
- 死锁检测/避免:锁不能因客户端异常而永久无法释放。
- 解锁正确性:只有持有锁的客户端可以释放锁。
- 续期机制:当业务执行时间超过锁的 TTL,可以延长锁的有效期。
- 容错性:在分布式环境下保证可靠性,防止 Redis 宕机或主从切换导致数据不一致。
二、核心实现思路
Redis 实现分布式锁的关键是利用 原子操作 + Lua 脚本 + TTL + Hash + 发布订阅机制,结合客户端逻辑完成完整功能。
流程如下:
+------------------+
| 客户端线程A |
+------------------+|| lock()v
+------------------+
| Redis Lua脚本 |
+------------------+|| key不存在?/ \是 否/ \
+------------------+ +------------------+
| 设置锁 key | | key存在,ownerId?|
| HSET ownerId=1 | | = 当前线程? |
| PEXPIRE TTL | +------------------+
+------------------+ || |v v返回 "OK" 重入 HINCRBY + PEXPIRE返回 "REENTER"|v客户端持有锁|| <业务执行期间>v看门狗定时续期renew.lua 刷新 TTL|vTTL 持续有效,防止过期|v
+------------------+
| unlock() 调用 |
+------------------+|| ownerId匹配?/ \否 是/ \
返回 NOT_OWNER HINCRBY -1|| 重入计数>0?/ \是 否/ \PEXPIRE TTL DEL key + 发布 unlock 消息
1. 互斥性(Mutual Exclusion)
目标:同一时间只能有一个客户端获取锁。
实现方式:
-
使用
hsetnx+pexpire
命令保证互斥和过期。 -
Redis 单线程保证 Lua 脚本原子执行。
-
第一次获取锁时,Lua 脚本检查 key 是否存在:
- 不存在 → 成功加锁
- 已存在 → 返回锁剩余 TTL,表示锁被占用
if redis.call('exists', KEYS[1]) == 0 thenredis.call('hset', KEYS[1], ARGV[2], 1)redis.call('pexpire', KEYS[1], ARGV[1])return "OK"
end
return "BUSY"
原子性保证只有一个客户端可以首次成功加锁,从而实现互斥。
2. 可重入性(Reentrancy)
目标:同一线程或客户端可以多次获取锁。
实现方式:
-
使用 Hash 结构 存储锁信息:
- field = ownerId(如
processUUID:threadId
,其中processUUID每个JVM实例都是唯一的,threadId代表每个线程ID) - value = 重入计数
- field = ownerId(如
-
同一 ownerId 再次加锁时:
HINCRBY
增加计数- 刷新 TTL
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 "REENTER"
end
3. 死锁检测/避免(Deadlock Prevention)
目标:防止锁因异常未释放导致死锁。
实现方式:
- TTL 自动释放:锁超时自动删除,防止宕机导致永久占用。
- 看门狗机制:锁持有者仍在执行业务时,周期性刷新 TTL,防止锁被意外释放。
- 硬上限(可选):设定锁最长持有时间,超时触发告警或拒绝续期。
4. 解锁正确性(Correct Unlock)
目标:只有锁持有者能解锁,避免误删。
实现方式:
-
Lua 脚本检查 ownerId 是否匹配:
-
不匹配 → 返回
NOT_OWNER
-
匹配:
- 重入计数 >1 → 减少计数,锁仍保留
- 重入计数 =0 → 删除 key,并发布 unlock 消息通知等待者
-
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 thenreturn "NOT_OWNER"
endlocal counter = redis.call('hincrby', KEYS[1], ARGV[1], -1)
if counter > 0 thenredis.call('pexpire', KEYS[1], ARGV[2])return "STILL_HELD"
elseredis.call('del', KEYS[1])redis.call('publish', KEYS[2], 'unlock')return "UNLOCKED"
end
5. 续期机制(Lease Renewal)
目标:防止业务执行时间超过锁 TTL 时,导致锁释放触发的线程安全问题,从而延长锁有效期。
实现方式:
- 开启一个守护线程看门狗定时任务,每 lease/3 调用
renew.lua
刷新 TTL:
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 thenreturn redis.call('pexpire', KEYS[1], ARGV[1])
end
return 0
- 只有锁持有者续期,避免其他线程意外延长锁。
6. 容错性(Fault Tolerance)
目标:在分布式环境保证锁可靠性。
实现方式:
-
Redis 高可用
- Cluster 或主从 + 哨兵,保证节点故障不会丢失锁。
- 集群模式下,需要通过RedLock防止"脑裂"问题。
-
持久化
- AOF 或 RDB,防止 Redis 重启丢失锁。
-
栅栏令牌(Fencing Token)
- 每次成功加锁发放单调递增 token,用于业务防止旧锁写入覆盖新锁。
-
客户端容错
- 看门狗续期失败重试
- 异常捕获与日志告警,保证锁状态可观测
七、ownerId 生成与可重入保证
-
ownerId = processUUID + “:” + threadId
processUUID
:JVM 启动时生成一次,标识进程唯一性threadId
:线程唯一标识
-
同一线程多次加锁 → ownerId 不变 → 支持可重入
-
跨节点或线程冲突 → ownerId 不同 → 保证互斥
public class LockClient {private static final String PROCESS_UUID = UUID.randomUUID().toString();private String getOwnerId() {return PROCESS_UUID + ":" + Thread.currentThread().getId();}
}
八、总结
Redis 实现分布式锁的核心是:
- 多个Redis命令都是通过 Lua 脚本保证各个操作的原子性,防止出现线程安全问题。
- ownerId + Hash 计数保证可重入性
- TTL + 看门狗避免死锁
- 解锁前校验 ownerId 保证安全性
- 周期续期机制应对长业务执行
- 高可用部署 + 栅栏令牌 + 客户端容错保证分布式可靠性
这种设计机制与 Redisson 基本一致,适合生产环境使用。