redis锁有哪些

开场个人观察

Redis 锁是很多 Java 项目都会遇到的话题。比如供应链系统里,多个服务同时扣减库存;ERP 里同一张单据不能被重复审核;定时任务在多台机器上部署,但同一批数据只能被一个节点处理。这些场景都需要某种“互斥”。

但 Redis 锁也是容易写错的地方。很多人第一版会写成 setnx lock 1,看起来能用,线上一遇到超时、宕机、重试、主从切换,就会暴露问题。分布式锁不是“抢到 key 就完事”,它要考虑 owner、过期时间、释放原子性、业务幂等和异常恢复。

这篇笔记按常见 Redis 锁类型梳理:简单 SETNX 锁、带过期时间的锁、带唯一标识和 Lua 释放的锁、可重入锁、Redisson 看门狗锁、RedLock 思路。重点还是落到项目里怎么避免锁丢失和死锁。

Redis分布式锁安全流程

核心观点

一个相对安全的 Redis 锁,至少要满足四个条件。

第一,加锁要原子。不能先 SETNX 再单独 EXPIRE,因为中间如果进程宕机,锁可能永不过期。

第二,锁要有唯一 owner。释放锁时必须确认锁是自己加的,不能误删别人的锁。

第三,释放要原子。检查 owner 和删除 key 要放在 Lua 脚本里执行,避免检查后锁过期、别人重新加锁、自己又误删。

第四,业务要幂等。分布式环境里很难只靠锁保证绝对一次执行,业务本身也要能处理重复请求。

实践方法

最朴素的锁是 SETNX

1
SETNX order:lock:1001 1

它的优点是简单,缺点也明显:如果没有过期时间,持锁进程挂了就可能死锁。

稍微好一点的写法是原子设置锁和值和过期时间:

1
SET order:lock:1001 requestId NX PX 30000

这里 NX 表示 key 不存在才设置,PX 30000 表示 30 秒过期,requestId 表示锁的持有者。这个写法解决了加锁和设置过期时间的原子性问题。

释放锁时不要直接:

1
DEL order:lock:1001

因为你的业务可能执行超过 30 秒,锁已经过期,另一个线程重新拿到锁。如果你这时直接删除,就会删掉别人的锁。

更安全的释放方式是 Lua:

1
2
3
4
5
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

这段脚本把“判断 owner”和“删除 key”放到 Redis 单线程执行里,避免中间被打断。

如果业务执行时间不确定,可以使用带看门狗机制的锁。Redisson 的思路是:加锁成功后,如果业务线程还活着,就定期延长锁过期时间;业务结束后主动释放。这样可以避免业务正常运行但锁提前过期。

不过看门狗也不是万能的。如果任务本身可能跑几分钟甚至几十分钟,我更倾向于拆小任务,或者使用任务状态表控制流程,而不是让一个 Redis 锁长时间占着。

可重入锁适合一个线程在同一调用链里重复进入同一把锁的场景。它一般会记录线程标识和重入次数。优点是符合 Java ReentrantLock 的使用习惯,缺点是实现复杂,依赖客户端框架管理。

RedLock 是一种尝试在多个 Redis 节点上获取多数锁的算法。它的目标是减少单点 Redis 故障对锁安全性的影响。但它实现和时钟假设更复杂,业务上是否需要要谨慎评估。多数普通后台系统,用单 Redis 主节点加合理业务幂等,或者直接用数据库唯一约束/状态机,反而更容易解释和维护。

业务例子

以供应链库存预占为例。用户下单后,系统要对 SKU 做库存预占,不能让两个请求同时把同一批库存扣成负数。

可以按 SKU 加锁:

1
stock:lock:sku1001

流程是:

  1. 请求生成唯一 requestId
  2. 使用 SET key requestId NX PX 30000 加锁。
  3. 查询库存可用量。
  4. 写入库存流水和订单预占记录。
  5. 更新库存。
  6. 提交数据库事务。
  7. 用 Lua 判断 owner 后释放锁。

这里还要做一层幂等:订单号和 SKU 的预占记录要有唯一约束。如果请求重试,即使再次进入,也不会重复预占。

再比如 ERP 单据审核,同一张采购单不能同时被两个人审核。可以用 po:audit:lock:{poNo} 做锁,但最终仍然要在数据库里校验单据状态:只有 待审核 才能流转到 已审核。Redis 锁减少并发冲突,数据库状态机保证最终正确。

踩坑提醒

第一个坑,是没有过期时间。没有 TTL 的锁,遇到进程宕机就可能永远释放不了。

第二个坑,是锁过期时间太短。业务还没执行完,锁先没了,其他线程进来就会并发执行。过期时间要结合业务耗时和最大抖动设置。

第三个坑,是直接删除锁。释放前不判断 owner,很容易误删别人的锁。

第四个坑,是只依赖锁不做幂等。网络重试、消费者重复消费、接口超时重放都可能导致同一业务重复进入。锁只是降低并发概率,幂等才是最后防线。

第五个坑,是大锁。比如用一个 global:lock 锁住所有库存操作,会严重影响吞吐量。锁粒度要尽量贴近业务资源,比如按 SKU、订单号、仓库维度拆分。

总结

Redis 锁常见形态从简单 SETNX 到带 TTL、带 owner、Lua 释放、可重入锁和 Redisson 看门狗锁。项目里最推荐的底线写法是:SET key requestId NX PX ttl 加锁,Lua 判断 owner 后释放,业务做好幂等和状态校验。

锁不是越复杂越安全。真正可靠的方案,通常是“短锁 + 合理 TTL + 原子释放 + 业务幂等 + 监控告警”。只要这几件事到位,大多数后台系统就已经比裸 SETNX 稳很多。