如何避免redis分布式锁失效
✅ 一、分布式锁基本模型
目标: 保证在分布式系统中,一个共享资源(如订单创建)在任意时间只被一个客户端操作。
通用 Redis 锁结构:
SET lock_key client_id NX EX 30
NX
:只在 key 不存在时设置EX
:设置过期时间,防止死锁client_id
:唯一标识持锁客户端
风险点:
锁因业务执行时间过长而过期(Redis TTL机制)
锁 key 被 Redis 内存淘汰机制清除
多个客户端因锁失效并发执行(锁竞争穿透)
因宕机导致死锁或事务未完成
✅ 二、Redis 过期/内存淘汰导致锁失效的根本原因
问题类型 | 背后原理 | 影响 |
---|---|---|
TTL 过期 | Redis 设置的 expire 计时器触发,自动删除 key | 锁提前释放,任务仍在执行 |
内存淘汰 | Redis 内存满,启用 maxmemory-policy ,优先回收 TTL 或 LRU/LFU 的 key | 锁 key 被非预期回收 |
主从不一致 | 主节点写入但还未同步,宕机切换到从节点 | 锁“未曾存在”,导致另一个客户端获取锁 |
✅ 三、从架构角度的系统性对策
1. 锁机制应有 租约续约(Lease + Heartbeat)能力
原理: 锁的 TTL 应该是「软 TTL」,可通过“看门狗”机制自动续期,避免 TTL 到期导致锁被 Redis 删除。
✅ 实践方案
使用 Redisson 的 WatchDog 自动续期机制
//示例代码(Redisson) RLock lock = redissonClient.getLock("lock:order:123");boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS); // 尝试加锁 + 锁有效期try {if (locked) {// 临界区逻辑} } finally {lock.unlock(); // 原子校验 + 自动释放 }
或者手动实现一个 定时续期线程(仅续自己持有的锁)
架构设计视角
每个服务节点获取锁时,启动一个守护线程每隔 N 秒检查锁是否仍然是自己持有
是 → 执行
expire
重置 TTL否 → 自动退出续约线程
类似数据库的租约机制,避免“锁饿死”。
2. 锁 key 应不受 Redis 淘汰策略影响(锁隔离区设计)
原理: Redis 的
volatile-ttl
,allkeys-lru
,allkeys-random
策略会淘汰掉 key,即使 TTL 还没到。
✅ 实践方案
将 锁 key 置于专用 DB,如 Redis 的 DB 15,避免与缓存等共享
Redis 实例设定
maxmemory-policy = noeviction
或仅淘汰热点缓存空间为锁 key 使用命名前缀如
lock:xxx
,统一监控/隔离Redis 实例内存配置预留充足空间避免触发淘汰
架构设计视角
锁是“系统控制数据”而不是业务缓存,必须具备高可靠性。
因此应隔离缓存与锁数据,避免因缓存压力影响锁行为。
3. 锁释放需原子校验 owner 身份,防止误删
原理: 客户端持有锁后宕机,另一个客户端获取了锁,结果前一个客户端重启并释放了锁,导致锁被错误释放。
✅ 实践方案
使用 Lua 脚本校验并释放:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
只有持有者能释放锁
提供事务性、原子性保障
架构设计视角
锁释放必须做到幂等、可审计、可验证(owner 校验 = 数据版本号控制)
类似数据库乐观锁机制
4. Redis 单点/主从复制延迟引发的锁漂移问题
原理: Redis 主从复制是异步的。主节点设置了锁还没同步到从节点,主挂了,从提升为主,另一个客户端就能加锁了。
✅ 实践方案
使用 RedLock 算法(多个 Redis 实例,大多数节点成功加锁才视为成功)
或使用 CP 系统(如 ZooKeeper、etcd)进行分布式协调
5. 补偿机制:防止“锁过期 + 数据未提交”造成数据不一致
方案:
✅ 5.1 业务幂等性设计
保证相同请求多次执行,结果一致
数据表中使用唯一请求号或业务标识控制
✅ 5.2 事务完成后再释放锁
@Transactional
public void doTaskWithLock() {RLock lock = redissonClient.getLock("task:lock");if (lock.tryLock(10, TimeUnit.SECONDS)) {try {// 1. 数据库事务操作(如写入状态表)// 2. 推送消息、写入缓存等} finally {lock.unlock(); // 保证释放锁时业务已完成}}
}
✅ 5.3 定期审计未完成任务
宕机恢复后,检查“任务日志表”中未完成的数据
执行补偿逻辑(重新调度、回滚、告警)
✅ 6. 事务状态持久化设计
原理:
加锁后即刻将「执行中」状态持久化(写入数据库或任务表)
即使宕机后,也能知道该操作“未完成”,由后续任务修复
示例任务状态表:
任务ID | 状态 | 锁持有者ID | 创建时间 | 更新时间 |
---|---|---|---|---|
T123 | PROCESSING | svc-A | 2025-07-19 15:00 | NULL |
方便另一个实例获取锁后根据状态判断是否重试、回滚或跳过
架构设计视角
场景 | 优先方案 |
---|---|
本地部署、小集群、高性能要求 | Redisson 单节点锁 + WatchDog |
多机房、强一致、关键任务 | ZooKeeper/Etcd 等 CP 分布式协调系统 |
✅ 四、锁系统设计原则
设计原则 | 说明 |
---|---|
最小职责 | 分布式锁只控制互斥,不负责状态传递、数据同步 |
可观测性 | 所有加锁/续约/释放操作要有监控(如 lock stats、失败率、owner trace) |
高可用性 | 锁系统本身不应成为单点,可使用 Redis Sentinel 或 Redis Cluster |
可回退性 | 锁失败应快速回退、重试、或降级(防雪崩) |
隔离性 | 锁与业务缓存分离,避免资源争抢影响系统可用性 |
✅ 建议
设计点 | 建议 |
---|---|
锁续约 | Redisson WatchDog 或手动续期 |
锁 key 管理 | 独立 Redis DB + 前缀命名 + 不参与淘汰策略 |
锁释放 | Lua 原子释放 + client_id 绑定 |
Redis 容灾 | RedLock 或 CP 系统(ZK、Etcd) |
监控 | 监控锁持有者、TTL、失败率、获取时延、释放延迟等 |
可靠性保障:
目标 | 推荐实践 |
---|---|
防止死锁 | 设置锁 TTL + WatchDog 自动续约 |
防止误释放 | 使用 UUID + Lua 脚本校验 |
防止业务未完成锁就释放 | 锁释放前完成事务 + 状态持久化 |
防止宕机后数据不一致 | 幂等机制 + 任务补偿机制 |
保证系统容灾 | Redis 高可用 + 持久化配置 |