Redis 分布式锁是解决分布式系统中 并发资源竞争 的常用方案,通过 Redis 的原子操作实现多节点间的互斥访问。其核心目标是:在分布式环境下,确保同一时刻只有一个客户端能持有锁,从而安全地操作共享资源。
一、分布式锁的核心要求
实现一个可靠的 Redis 分布式锁,需满足以下条件:
- 互斥性:同一时刻只有一个客户端持有锁。
- 安全性:锁只能被持有它的客户端释放(防误删)。
- 避免死锁:即使持有锁的客户端崩溃,锁也能在一定时间后自动释放(超时机制)。
- 高可用:Redis 集群环境下,锁服务需具备容错能力(如主从切换时锁不丢失)。
- 性能:加锁和解锁操作需高效(低延迟、高吞吐量)。
二、基础实现方案(基于 Redis 命令)
1. 加锁:SET NX EX
(推荐原子操作)
使用 Redis 的 SET
命令的 NX
(Only if Not Exists) 和 EX
(Expire) 选项,原子性地完成"判断锁是否存在 + 设置锁 + 设置过期时间":
bash
# 语法:SET key value NX EX seconds
SET lock:resource_name <唯一随机值> NX EX 30
- 参数说明:
lock:resource_name
:锁的 key(需包含具体资源名,如lock:order:1001
)。<唯一随机值>
:客户端生成的唯一标识(如 UUID),用于解锁时验证身份(防误删)。NX
:仅当 key 不存在时才设置(保证互斥性)。EX 30
:自动过期时间(30 秒,避免死锁)。- 返回值:
- 成功:
OK
(表示加锁成功)。 - 失败:
nil
(表示锁已被其他客户端持有)。
2. 解锁:Lua 脚本(原子操作)
解锁需满足 "验证唯一标识 + 删除锁" 两步原子性,否则可能误删其他客户端的锁(如:客户端 A 锁超时自动释放,客户端 B 加锁,此时客户端 A 才执行解锁,可能删除 B 的锁)。
必须通过 Lua 脚本实现原子操作:
lua
-- 解锁脚本:判断 value 是否匹配,匹配则删除
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
- 调用方式(以 Python 为例):
python
import redis
import uuid
r = redis.Redis()
lock_key = "lock:resource_name"
client_id = str(uuid.uuid4()) # 客户端唯一标识
# 加锁
lock_acquired = r.set(lock_key, client_id, nx=True, ex=30)
# 解锁
if lock_acquired:
unlock_script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
r.eval(unlock_script, 1, lock_key, client_id) # 1 表示 KEYS 的数量
3. 避免死锁:超时时间与锁续期
- 合理设置过期时间:需大于业务操作的最大耗时(如操作需 10 秒,过期时间设为 30 秒)。
- 锁续期(Watchdog) :若业务操作耗时可能超过过期时间,需启动后台线程,定期(如每 10 秒)为锁续期(重置过期时间),操作完成后停止续期。
▶ 示例:Redisson 客户端的watch dog
机制。
三、进阶方案:解决集群环境下的高可用问题
基础方案依赖单节点 Redis,若节点故障(如主从切换时主库宕机,从库未同步到锁数据),可能导致 锁丢失 或 重复加锁。以下是两种高可用方案:
1. Redlock 算法(Redis 官方推荐)
由 Redis 作者 Antirez 提出,基于 多个独立 Redis 节点(通常 5 个)实现分布式锁,核心思想:
- 客户端向 过半(≥3 个)独立节点 发送加锁请求(使用
SET NX EX
)。 - 若过半节点加锁成功,且总耗时 ≤ 锁过期时间的 1/3,则认为加锁成功。
- 解锁时需向 所有节点 发送解锁请求(无论加锁是否成功)。
优势 :不依赖单节点,即使部分节点故障,仍能保证锁的可用性(满足 CAP 中的 AP,但牺牲部分一致性)。
缺点:实现复杂,性能较低(需访问多个节点)。
2. 基于 Redis Cluster 的方案
利用 Redis Cluster 的 主从复制 + 哨兵 保证高可用,但需注意:
- 主库宕机后,从库升级为主库前,可能存在短暂的 锁丢失窗口(主库未同步锁数据到从库)。
- 可通过 延迟从库同步 降低风险,但无法完全避免(适合对一致性要求不极致的场景)。
四、常见问题与解决方案
1. 问题:锁超时与业务耗时不匹配
- 风险:若业务操作耗时超过锁过期时间,锁自动释放,其他客户端可能重复加锁。
- 解决:
- 预估合理的过期时间(略大于最大业务耗时)。
- 实现 锁续期 (如 Redisson 的 Watchdog 机制:加锁时启动定时任务,每
expire/3
秒续期一次,业务完成后取消任务)。
2. 问题:误删其他客户端的锁
- 风险 :解锁时未验证唯一标识,直接执行
DEL
,可能删除其他客户端的锁。 - 解决 :必须用 Lua 脚本原子性执行"验证 + 删除"(见上文"解锁"部分)。
3. 问题:Redis 主从切换导致锁丢失
- 场景:客户端 A 向主库加锁,主库宕机,从库未同步锁数据即升级为主库,客户端 B 再次加锁成功,导致"双活"。
- 解决:
- 采用 Redlock 算法(多节点加锁,避免单节点依赖)。
- 业务层增加 最终一致性校验(如分布式事务的补偿机制)。
4. 问题:并发加锁时的"惊群效应"
- 场景:大量客户端等待一个锁释放,锁释放瞬间所有客户端同时抢锁,导致 Redis 压力激增。
- 解决:
- 加锁失败后,通过 随机延迟重试(如 50~200ms 随机值),分散抢锁请求。
- 使用 阻塞式锁 (如 Redisson 的
RLock.tryLock(30, 10, TimeUnit.SECONDS)
,内部通过信号量唤醒等待线程,避免轮询)。
五、生产级客户端推荐(避免重复造轮子)
手动实现分布式锁易出错(如 Lua 脚本编写、续期逻辑等),推荐使用成熟的客户端:
1. Redisson(Java)
- 支持单机、主从、哨兵、Cluster 等多种 Redis 部署模式。
- 内置 Watchdog 自动续期 、Redlock 算法 、可重入锁 、公平锁 等高级特性。
- 示例代码:
java
RLock lock = redissonClient.getLock("lock:resource_name");
try {
// 尝试加锁,最多等待 10 秒,锁过期时间 30 秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
// 执行业务操作
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 自动验证唯一标识并解锁
}
}
2. Python:redis-py + 自定义封装 / RedLock-py
- 基础方案 :用
redis-py
实现SET NX EX
+ Lua 脚本解锁。 - Redlock 方案 :使用第三方库
redlock-py
(实现 Redlock 算法)。
python
from redlock import RedLock
with RedLock("lock:resource_name", connection_details=[{"host": "localhost", "port": 6379}], ttl=30000):
# 执行业务操作
六、总结
Redis 分布式锁的核心是通过 原子命令 (SET NX EX
、Lua 脚本)保证互斥性和安全性,通过 过期时间 避免死锁,通过 Redlock 或集群方案 提高可用性。
最佳实践:
- 锁 key 需包含具体资源名,避免全局锁竞争。
- 客户端唯一标识必须随机且唯一(UUID 或随机字符串 + 客户端 ID)。
- 解锁必须使用 Lua 脚本原子操作。
- 优先使用成熟客户端(如 Redisson),避免重复造轮子。
- 根据业务场景选择单节点(简单高效)或 Redlock(高可用)方案。
通过以上策略,可实现一个既可靠又高效的 Redis 分布式锁,满足分布式系统的并发控制需求。