Redis 锁是并发安全的吗?怎么保证不被抢夺?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官抛出这个问题,不仅仅是让你背个结论,更想考察你以下几个方面的能力:

  1. 基础掌握:是否知道 Redis 实现锁的基本命令,比如 SET NX 的原子性操作。
  2. 安全意识:知不知道一个 “裸” 的 Redis 锁有哪些安全隐患?这直接关系到生产环境的代码质量。
  3. 深度思考:锁被 “抢夺” 的根本原因是什么?是客户端自己的 Bug,还是 Redis 本身的不可靠(比如主从切换)导致的?
  4. 实战经验:有没有读过或者用过像 Redisson 这样的成熟框架?知不知道 “看门狗” 续期机制和 Redlock 算法是用来解决什么问题的。
  5. 技术选型:在什么场景下用 Redis 锁就够了,什么场景下必须用更重量级但更安全的 ZooKeeper 锁。

核心答案

直接说结论:一个正确实现的 Redis 锁,在大多数场景下是并发安全的;但如果实现得不够严谨,或者在某些极端场景(如主从切换、GC 停顿),就可能被意外 “抢夺”

保证不被抢夺的关键在于 “占坑” 的原子性“身份” 的唯一性“续命” 的可靠性。通常我们会借助 Redisson 这类客户端,通过 “看门狗” 机制和 Lua 脚本来确保锁的安全。

深度解析

下面我们一步步拆解,一个 Redis 锁从简单到可靠,都经历了哪些挑战,以及如何应对。

1. 原子性:从 setnxSET NX 的进化

  • 错误示范:最早的写法是先 setnx,再 expire。这俩命令不是原子的,如果在 setnx 成功后、expire 设置前,服务宕机了,这个锁就变成了 “不死锁”,永远没人能解开。
  • 正确姿势:从 Redis 2.8 开始,我们可以用一条命令搞定:
    SET lock_key unique_value NX PX 30000
    

    这条命令保证了 “加锁” 和 “设过期时间” 是一个原子操作,从根本上避免了死锁。

2. 防误删:给锁一个 “身份证”

  • 问题:如果线程 A 的锁因为业务执行太久自动过期了,线程 B 拿到了锁。这时候线程 A 代码执行完,直接来了个 DEL lock_key,就会把线程 B 的锁给删了,这就乱套了。

  • 解决:加锁时,unique_value 必须是一个全局唯一的 ID(比如 UUID)。解锁时,不能直接 DEL,得先判断这个锁是不是自己的。

  • 代码示例:这里必须用 Lua 脚本保证 “判断身份 + 删除锁” 的原子性。

    // Lua 脚本
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end
    

3. 锁过期与续期(WatchDog 看门狗)

  • 痛点:设置了 30 秒过期,但业务执行了 40 秒。锁提前释放,其他线程就进来了,并发安全被破坏。
  • 最佳实践:不要假设业务一定能在固定时间内执行完。使用 Redisson 这样的客户端,它提供了一个叫 WatchDog(看门狗)的机制。
    • 默认情况下,加锁成功后,会启动一个后台线程。
    • 这个线程每隔 10 秒(可配置)检查一次,如果锁还在,就把锁的过期时间重新设置为 30 秒。
    • 这就相当于给锁“自动续费”了,直到业务主动 unlock

4. 主从架构下的隐患与 Redlock

  • 场景:当 Redis 采用主从架构时,如果线程 A 在主库上加锁成功,主库在把这条锁数据同步给从库之前突然宕机了。这时候从库升为主库,由于没收到这条同步,线程 B 来加锁也能成功!这样就同时有两个线程持有了同一把锁,在高并发扣减库存这类场景下,就会酿成事故。
  • 解决方案:Redlock 算法
    • 原理:同时向 5 个(通常是奇数个)独立的 Redis 实例请求加锁。只有当超过半数(N/2+1)的实例返回成功时,才认为加锁成功。
    • 代价:Redlock 算法比较复杂,而且性能开销较大,因为要跟多个 Redis 实例通信。
    • 取舍:如果你的业务对数据的正确性要求极高(比如金融、支付),且无法容忍主从切换导致的锁失效,那么 ZooKeeper 基于临时顺序节点的分布式锁可能是比 Redlock 更稳妥的选择。因为 ZK 的强一致性协议能从根本上保证锁的安全。

总结

总的来说,Redis 锁的安全性不是二进制的是非题,而是一个在正确使用下逐步逼近 “安全” 的过程

  • 要保证不被意外抢夺,你需要做到:
    1. 原子性加锁SET NX PX)。
    2. 唯一性标识(用 UUID 做 value)。
    3. 原子性解锁(Lua 脚本判断)。
    4. 自动续期(避免业务超时)。
  • 如果你能容忍极端情况下的锁失效(比如大部分互联网业务场景),那么基于 Redisson 的 Redis 锁就是你的首选,因为它足够快。
  • 如果你连 0.001% 的失效概率都不能接受(比如金融核心系统),那么请选择 ZooKeeper。