跳到主要内容

Redis 分布式锁

问题

如何用 Redis 实现分布式锁?有哪些常见问题?Redisson 的看门狗机制是什么?RedLock 靠谱吗?

答案

一、基础实现

# 加锁:SET key value NX EX(原子操作)
SET lock:order:123 "request-id-xxx" NX EX 30
# NX:不存在才设置(互斥)
# EX 30:30 秒过期(防死锁)

# 解锁:Lua 脚本保证原子性
EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:order:123 "request-id-xxx"
为什么解锁必须用 Lua 脚本?

如果分开执行 GET + DEL,可能出现:

  1. 客户端 A 锁快过期了,GET 检查是自己的锁
  2. 锁过期了,客户端 B 获取了锁
  3. 客户端 A 执行 DEL,把客户端 B 的锁删了

Lua 脚本保证 GET + DEL 原子执行。

二、常见问题与解决方案

问题说明解决方案
死锁持锁进程崩溃,锁不释放设置过期时间
误删别人的锁过期后被重新获取的锁锁值设唯一标识,解锁前校验
锁过期但业务未完成业务执行时间 > 锁过期时间看门狗自动续期
可重入同一线程多次获取同一把锁需要额外计数器
主从切换丢锁主库加锁成功但未同步到从库RedLock

三、Redisson(推荐方案)

Redisson 是 Java 的 Redis 客户端,封装了完整的分布式锁实现。

看门狗(Watchdog)机制

  • 默认锁过期时间 30 秒
  • Watchdog 每 10 秒(leaseTime / 3)检查并续期
  • 客户端宕机后 Watchdog 也停止,锁最终超时释放

Redisson 支持的锁类型

锁类型说明
RLock可重入锁(最常用)
RFairLock公平锁(按请求顺序获取)
RReadWriteLock读写锁
RMultiLock联锁(多个锁同时获取)
RRedLockRedLock 实现
RSemaphore信号量

四、RedLock 算法

Redis 主从切换时可能丢锁。RedLock 使用 多个独立 Redis 节点 来提高安全性:

加锁步骤

  1. 获取当前时间 T1
  2. 依次向 N 个独立 Redis 节点发送加锁请求
  3. 如果超过半数节点(N/2 + 1)加锁成功,且总耗时 < 锁过期时间
  4. 加锁成功,实际有效期 = 过期时间 - 加锁耗时
  5. 否则向所有节点释放锁
RedLock 的争议

Martin Kleppmann(《DDIA》作者)对 RedLock 提出了质疑:

  • GC 暂停、网络延迟可能导致锁失效
  • 依赖系统时钟,时钟跳变会出问题

Antirez(Redis 作者)进行了反驳。

实践建议

  • 大多数场景使用单节点 + Watchdog 续期即可
  • 如果需要更强一致性,考虑使用 ZooKeeper 或 etcd 的分布式锁

五、最佳实践

1. 锁粒度尽量小(lock:order:123 而非 lock:order)
2. 设置合理的过期时间(略大于业务执行时间)
3. 使用唯一标识(UUID/RequestID)防止误删
4. 解锁用 Lua 脚本保证原子性
5. 生产环境建议使用 Redisson 等成熟框架
6. 锁内业务要做到幂等(防止重试导致重复执行)

常见面试问题

Q1: Redis 分布式锁和 ZooKeeper 分布式锁的区别?

答案

对比项RedisZooKeeper
一致性AP(可能丢锁)CP(强一致)
性能
实现复杂度简单复杂
锁释放过期时间临时节点(会话断开自动删)
可重入需额外实现ZK 原生支持
适用场景对一致性要求不极端强一致性要求

Q2: 如果 Redis 加锁成功但还没来得及设过期时间就宕机了?

答案
这就是为什么必须使用 SET key value NX EX 一条命令完成。如果分两步(SETNX + EXPIRE),SETNX 成功后宕机,锁就永远不过期。

Q3: 可重入锁怎么实现?

答案
用 Hash 结构存储锁和计数:

# 加锁(Lua 脚本)
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[1], 1) -- 新建锁,计数=1
redis.call('expire', KEYS[1], ARGV[2])
return 1
elseif redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
redis.call('hincrby', KEYS[1], ARGV[1], 1) -- 计数+1
redis.call('expire', KEYS[1], ARGV[2])
return 1
else
return 0 -- 锁被别人持有
end

相关链接