外观
Redis 分布式锁原理?
⭐ 题目日期:
阿里 - 2024/8/21,美团 - 2025/4/12
📝 题解:
Redis 分布式锁是分布式系统中实现互斥访问共享资源的常用方案,其核心目标是通过 Redis 的单线程原子性操作保证多个进程/服务对共享资源的互斥访问。以下是 Redis 分布式锁的原理、实现细节及常见问题的解析:
1. 基本原理
Redis 分布式锁的本质是利用 Redis 的 SET
命令的原子性(结合 NX
和 PX
参数)实现锁的互斥性,并通过唯一标识(如 UUID)保证锁的归属权。
关键命令
SET lock_key unique_value NX PX 30000
NX
:仅当键不存在时才设置(保证互斥性)。PX
:设置键的过期时间(防止死锁)。unique_value
:唯一标识(如 UUID),用于验证锁的归属,避免误删其他客户端的锁。
2. 实现步骤
(1) 获取锁
客户端尝试通过 SET
命令申请锁:
- 若返回
OK
,表示获取成功,获得锁并设置过期时间。 - 若返回
nil
,表示锁已被其他客户端持有,需等待或重试。
(2) 释放锁
客户端释放锁时需验证锁的归属权(通过唯一标识),并通过 Lua 脚本保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
- 为何需要 Lua 脚本?防止
GET
和DEL
操作之间的锁过期或被其他客户端修改,导致误删。
(3) 锁超时与续期
- 自动超时:Redis 锁默认有过期时间(如 30 秒),即使客户端崩溃,锁也会自动释放。
- 手动续期(看门狗机制):客户端在持有锁期间定期(如每隔 10 秒)重置过期时间,避免业务未完成时锁过期。
3. 核心问题与解决方案
(1) 死锁问题
- 原因:客户端获取锁后崩溃,未释放锁。
- 解决:为锁设置合理的过期时间(
PX
参数)。
(2) 误删锁
- 原因:客户端 A 释放锁时误删客户端 B 的锁(如 A 因 GC 停顿导致锁过期后被 B 获取)。
- 解决:使用唯一标识验证锁的归属权。
(3) 锁续期问题
- 原因:业务执行时间超过锁的过期时间。
- 解决:实现看门狗机制(如 Redisson 库的
LockWatchdog
),后台线程定期续期。
(4) 集群环境下的问题
- 主从切换风险:Redis 主节点宕机后,从节点可能未同步锁信息,导致锁丢失。
- 解决:使用 RedLock 算法(Redis 官方推荐的分布式锁算法)。
4. RedLock 算法(增强版分布式锁)
RedLock 旨在解决 Redis 单节点或主从架构下的锁不可靠问题,核心思想是在多个独立的 Redis 节点上获取锁,确保多数节点成功才算锁获取成功。
步骤
- 获取当前时间戳(精确到毫秒)。
- 依次向 N 个独立 Redis 节点请求锁,使用相同的
key
和unique_value
,设置相同的超时时间(如 5 秒)。 - 统计成功获取锁的节点数:
- 若多数节点(≥ N/2 + 1)成功,且总耗时小于锁的有效时间,则锁获取成功。
- 否则,向所有节点发送释放锁的请求。
- 锁的有效时间 = 初始锁超时时间 - 获取锁的总耗时。
优缺点
- 优点:提高锁的可靠性,容忍部分节点故障。
- 缺点:
- 实现复杂,需管理多个 Redis 实例。
- 性能较低(需多次网络通信)。
- 仍无法完全解决网络分区(脑裂)问题。
5. 常见实现库
- Redisson(Java):提供完善的分布式锁实现,支持自动续期、可重入锁等特性。
- StackExchange.Redis(.NET):结合 Lua 脚本实现基本分布式锁。
- Python (redis-py):通过
SET
命令和 Lua 脚本手动实现。
6. 示例代码(Python)
import redis
import uuid
import time
class RedisLock:
def __init__(self, redis_client, lock_key, expire_time=30000):
self.redis = redis_client
self.lock_key = lock_key
self.expire_time = expire_time
self.identifier = str(uuid.uuid4())
def acquire(self, timeout=10):
end = time.time() + timeout
while time.time() < end:
# 尝试获取锁
if self.redis.set(self.lock_key, self.identifier, nx=True, px=self.expire_time):
return True
time.sleep(0.001) # 短暂等待后重试
return False
def release(self):
# 使用 Lua 脚本保证原子性
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
return self.redis.eval(script, 1, self.lock_key, self.identifier)
7. 使用场景与权衡
- 适用场景:
- 高并发下对共享资源的互斥访问(如秒杀库存扣减)。
- 分布式任务调度(确保任务仅被执行一次)。
- 不适用场景:
- 对一致性要求极高的场景(如金融交易)建议使用 ZooKeeper 或 etcd。
- 对性能要求极高的场景(Redis 锁的网络开销可能成为瓶颈)。
总结
Redis 分布式锁的核心是通过原子操作和唯一标识实现互斥性与安全性,但在实际应用中需注意 锁续期、集群可靠性、时钟漂移 等问题。对于高可用场景,建议使用 RedLock 算法或成熟的库(如 Redisson),并根据业务需求权衡一致性与性能。