Redis 中的锁:核心实现、类型与最佳实践

Redis 中的锁:核心实现、类型与最佳实践

文章目录

  • [Redis 中的锁:核心实现、类型与最佳实践](#Redis 中的锁:核心实现、类型与最佳实践)
    • [一、Redis 锁的核心基础:原子性与过期机制](#一、Redis 锁的核心基础:原子性与过期机制)
    • [二、Redis 锁的主要类型](#二、Redis 锁的主要类型)
      • [类型 1:单机版 Redis 锁(基础版)](#类型 1:单机版 Redis 锁(基础版))
      • [类型 2:Redis 分布式锁(Redlock 红锁,Redis 官方推荐)](#类型 2:Redis 分布式锁(Redlock 红锁,Redis 官方推荐))
      • [类型 3:Redis 可重入锁(基于 Redisson 实现)](#类型 3:Redis 可重入锁(基于 Redisson 实现))
        • 适用场景
        • [核心实现(以 Redisson 为例)](#核心实现(以 Redisson 为例))
        • [核心代码(Redisson 可重入锁)](#核心代码(Redisson 可重入锁))
        • 核心优势
      • [类型 4:Redis 读写锁(RReadWriteLock)](#类型 4:Redis 读写锁(RReadWriteLock))
        • 适用场景
        • [核心规则(基于 Redisson)](#核心规则(基于 Redisson))
        • [核心代码(Redisson 读写锁)](#核心代码(Redisson 读写锁))
    • [三、Redis 锁的常见问题与解决方案](#三、Redis 锁的常见问题与解决方案)
      • [问题 1:死锁](#问题 1:死锁)
      • [问题 2:锁的误删](#问题 2:锁的误删)
      • [问题 3:锁提前释放](#问题 3:锁提前释放)
      • [问题 4:竞态条件](#问题 4:竞态条件)
      • [问题 5:Redis 主从同步延迟导致的锁失效](#问题 5:Redis 主从同步延迟导致的锁失效)
      • [问题 6:锁的粒度太大](#问题 6:锁的粒度太大)
    • [四、Redis 锁的选型建议](#四、Redis 锁的选型建议)
    • [五、Redis 锁与其他分布式锁的对比](#五、Redis 锁与其他分布式锁的对比)
    • [六、Redis 锁的最佳实践](#六、Redis 锁的最佳实践)
    • 总结

Redis 作为高性能的内存键值数据库,凭借 单线程原子性高读写速度丰富的命令集 ,成为分布式系统中实现分布式锁的主流方案。Redis 锁主要解决 分布式环境下的资源竞争 问题,保证多个服务 / 节点对同一资源的操作互斥性,核心分为 单机版 Redis 锁分布式 Redis 锁 (适配 Redis 集群 / 主从架构),同时衍生出多种优化实现。

一、Redis 锁的核心基础:原子性与过期机制

Redis 实现锁的核心依赖两个特性,也是区别于普通内存锁的关键:

  1. 命令原子性 :Redis 单线程执行命令,且提供 SETNXSET 带多参数等原子命令,避免锁的 "加锁 - 判空" 操作出现竞态条件(比如 A 线程判空后,B 线程同时完成加锁)。
  2. 过期自动释放 :为锁设置过期时间,避免持有锁的节点因宕机、网络异常等原因无法手动释放锁,导致死锁

核心基础命令

Redis 锁的实现围绕以下核心命令展开,其中 **SET 多参数命令 ** 是目前最优的基础加锁命令:

命令 作用 适用场景 缺点
SETNX key value 仅当 key 不存在时设置值,成功返回 1,失败返回 0 早期单机锁加锁 无法原子性设置过期时间,加锁和设过期分两步会出现死锁风险
EXPIRE key seconds 为 key 设置过期时间 配合 SETNX 设过期 非原子操作,与 SETNX 配合时可能出现 "加锁成功但设过期失败"
SET key value NX EX seconds 原子性完成:NX(仅不存在时设置)+ EX(设过期秒数),PX 为毫秒 主流加锁方式 无核心缺点,是单机锁的标准加锁命令
DEL key 删除 key,释放锁 手动释放锁 若误删其他节点持有的锁,会导致锁失效(需加锁标识
GET key 获取锁的 value 验证锁的持有者 单独使用非原子,需配合GETSET/EVAL实现原子校验 + 释放
GETSET key newvalue 原子性获取旧值并设置新值 锁的过期续约(看门狗) 略复杂,需配合过期时间使用
EVAL script key [key...] arg [arg...] 执行 Lua 脚本 复杂原子操作(如校验 + 释放) 需编写 Lua 脚本,无原生命令简洁

关键结论禁止单独使用SETNX ,必须使用SET key value NX EX/PX实现原子加锁 + 设过期,这是 Redis 锁的基础规范。

原因:SETNX加锁与设过期非原子操作,易导致死锁

SETNXEXPIRE是两个独立的 Redis 命令,无法保证原子性。单独用SETNX加锁会出现极端的异常场景:

线程 A 执行SETNX成功(加锁),但还没来得及执行EXPIRE(设过期),就因为机器宕机、网络中断、进程被杀等原因终止;

此时lock_key被创建但没有过期时间,成为 "永久锁";

其他所有线程都无法通过SETNX获取该锁,导致死锁,资源永远无法被释放。

这种场景在分布式系统中无法避免(比如节点重启、网络抖动),一旦发生就会导致业务阻塞,是生产环境的重大隐患。

二、Redis 锁的主要类型

根据 Redis 的部署架构和实现复杂度,Redis 锁分为单机版分布式版官方标准化版,适配从单节点到高可用集群的不同场景,安全性和实现复杂度依次提升。

类型 1:单机版 Redis 锁(基础版)

适用场景

Redis 单节点部署,无主从 / 集群,适用于低并发、对高可用要求不高的场景(如小型单体应用的分布式部署)。

核心实现逻辑

加锁 :使用SET lock_key 唯一标识 NX EX 30(30 为过期时间,单位秒),唯一标识(如 UUID + 节点 ID)用于验证锁的持有者,避免误删。

解锁 :先校验锁的 value 是否为当前节点的唯一标识,再删除 key,必须通过 Lua 脚本实现原子性(否则校验和删除分两步会出现误删)。

重试:加锁失败时,可通过自旋(循环重试)或阻塞等待实现重入。

核心代码(伪代码)
java 复制代码
// 加锁
String lockKey = "resource:lock";
String lockValue = UUID.randomUUID().toString() + "-" + nodeId;
// NX:仅不存在时加锁,PX 30000:过期30秒,原子操作
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (!lockSuccess) {
    return "加锁失败,资源被占用";
}

// 业务逻辑
try {
    doBusiness();
} finally {
    // 解锁:Lua脚本保证原子校验+删除
    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class), Arrays.asList(lockKey), lockValue);
}
优缺点
  • 优点:实现简单、性能高,无额外组件依赖;
  • 缺点:Redis 单节点宕机后,整个锁服务不可用;不支持重入(需额外扩展)。

类型 2:Redis 分布式锁(Redlock 红锁,Redis 官方推荐)

适用场景

Redis 主从 / 哨兵 / 集群部署,对锁的高可用要求高(如电商库存扣减、订单创建等核心业务),解决单机版锁的 "单点故障" 问题。

核心设计思想

由 Redis 作者 Antirez 提出,基于多个独立的 Redis 主节点 (无主从关系,通常 3/5 个)实现分布式锁,要求在超过半数的节点上成功加锁,才认为整体加锁成功;解锁时需删除所有节点的锁。

核心实现步骤(以 5 个独立 Redis 节点为例)
  1. 生成唯一标识:同单机版,用于标识锁持有者;
  2. 逐个加锁 :依次向 5 个 Redis 节点发送SET lock_key 唯一标识 NX EX 超时时间命令,设置统一的加锁超时时间(远小于锁的过期时间,避免某个节点宕机导致阻塞);
  3. 判断加锁成功 :若超过半数节点(≥3 个) 加锁成功,且总耗时≤锁的过期时间,则加锁成功;否则,立即向所有节点发起解锁,加锁失败;
  4. 执行业务:加锁成功后执行互斥业务;
  5. 统一解锁:向所有 5 个节点发送解锁命令(Lua 脚本),无论节点是否加锁成功。
优缺点
  • 优点:解决 Redis 单点故障,高可用;锁的安全性远高于单机版;
  • 缺点 :实现复杂,需维护多个独立 Redis 节点;性能比单机版低(需逐个节点加锁);存在时钟漂移风险(节点间时间不同步可能导致锁提前过期)。
注意事项
  • 红锁要求 Redis 节点完全独立,不能是主从 / 哨兵架构(主从同步存在延迟,主节点宕机后从节点升主,可能出现多个节点持有锁);
  • 加锁超时时间必须远小于锁的过期时间(建议为锁过期时间的 1/5~1/3),避免网络延迟导致总耗时过长。

类型 3:Redis 可重入锁(基于 Redisson 实现)

适用场景

需要锁重入 的业务(如方法 A 加锁后,调用的方法 B 也需要对同一资源加锁,无需重复加锁),是对基础 Redis 锁的功能扩展,原生 Redis 不支持重入,需通过第三方框架实现。

核心实现(以 Redisson 为例)

Redisson 是 Redis 的 Java 客户端,基于 Redis 实现了可重入锁、公平锁、读写锁 等多种分布式锁,底层通过Lua 脚本Hash 结构实现重入:

  1. 加锁时,用 Hash 结构存储锁key -> {持有者标识: 重入次数}
  2. 首次加锁:创建 Hash,重入次数设为 1,同时设置过期时间;
  3. 重入加锁:校验持有者标识为当前节点,原子性将重入次数 + 1;
  4. 解锁:校验持有者标识,重入次数 - 1,若次数为 0 则删除 Hash(释放锁),否则仅更新次数;
  5. 看门狗机制 :若业务未执行完成,锁的过期时间将到期,Redisson 会通过定时任务自动续约锁的过期时间(默认每 10 秒续约,锁默认过期 30 秒),避免锁提前释放。
核心代码(Redisson 可重入锁)
java 复制代码
// 初始化Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

// 获取可重入锁
RLock lock = redisson.getLock("resource:lock");
// 加锁(可设置过期时间,无参则启用看门狗)
lock.lock(30, TimeUnit.SECONDS);
try {
    doBusiness();
    // 方法内重入加锁,无需等待
    reenterMethod(lock);
} finally {
    // 解锁,重入时会递减次数,次数为0则释放锁
    lock.unlock();
}

// 重入方法
public void reenterMethod(RLock lock) {
    lock.lock();
    try {
        doSubBusiness();
    } finally {
        lock.unlock();
    }
}
核心优势
  • 原生支持可重入、公平锁、读写锁,满足复杂业务需求;
  • 内置看门狗机制,自动续约锁,无需手动设置过期时间;
  • 适配 Redis 单机、主从、集群、哨兵等所有部署架构;
  • 提供阻塞、非阻塞、自旋等多种加锁方式。

类型 4:Redis 读写锁(RReadWriteLock)

适用场景

读多写少 的资源竞争场景(如商品详情页缓存、文章内容查询),实现读共享、写互斥,提升并发量(若用普通互斥锁,读操作也会互斥,降低性能)。

核心规则(基于 Redisson)
  1. 读锁:多个节点可同时获取读锁,无互斥;若已有写锁,则读锁获取失败;
  2. 写锁:排他锁,仅一个节点可获取;若已有读锁 / 写锁,则写锁获取失败;
  3. 锁升级:不支持(读锁不能直接升级为写锁,需先释放读锁);
  4. 锁降级:支持(写锁可降级为读锁,无需释放写锁,直接获取读锁)。
核心代码(Redisson 读写锁)
java 复制代码
RReadWriteLock rwLock = redisson.getReadWriteLock("resource:rwlock");
// 获取读锁
RLock readLock = rwLock.readLock();
// 获取写锁
RLock writeLock = rwLock.writeLock();

// 写操作:排他锁
writeLock.lock(30, TimeUnit.SECONDS);
try {
    // 更新资源,如修改商品库存
    updateResource();
} finally {
    writeLock.unlock();
}

// 读操作:共享锁
readLock.lock(30, TimeUnit.SECONDS);
try {
    // 查询资源,如查询商品库存
    getResource();
} finally {
    readLock.unlock();
}

三、Redis 锁的常见问题与解决方案

Redis 锁的实现中,若忽略细节,会出现死锁、误删、锁提前释放、并发安全等问题,以下是核心问题及通用解决方案:

问题 1:死锁

原因:持有锁的节点宕机 / 网络异常,无法手动释放锁,且锁未设置过期时间。

解决方案

  1. 加锁时必须原子性设置过期时间SET NX EX/PX),即使节点宕机,Redis 也会自动释放锁;
  2. 高可用场景下使用 Redlock,避免单节点宕机导致锁无法释放;
  3. 业务侧增加兜底任务,清理过期未释放的异常锁。

问题 2:锁的误删

原因 :节点 A 的锁因过期自动释放,节点 B 获取锁,此时节点 A 执行完业务,直接DEL锁,误删节点 B 的锁。

解决方案

  1. 加锁时设置唯一的锁标识(如 UUID + 节点 ID / 线程 ID),解锁前先校验标识是否为当前节点所有;
  2. 解锁操作必须原子性 (通过 Lua 脚本实现 "校验 + 删除"),禁止分两步执行GETDEL

问题 3:锁提前释放

原因:业务执行时间超过锁的过期时间,Redis 自动释放锁,其他节点获取锁,导致多个节点同时执行业务。

解决方案

  1. 预估合理的锁过期时间,预留足够的业务执行时间(如常规执行 10 秒,设过期 30 秒);
  2. 使用看门狗机制(Redisson 内置),定时为未执行完的业务续约锁的过期时间;
  3. 业务侧优化性能,减少锁持有时间(锁的粒度尽可能小,仅在资源竞争处加锁)。

问题 4:竞态条件

原因:非原子的加锁 / 解锁操作,导致多个节点同时尝试加锁 / 解锁。

解决方案

  1. 加锁使用 Redis 原生原子命令(SET NX EX/PX),禁止分两步执行SETNXEXPIRE
  2. 解锁使用 Lua 脚本实现原子操作;
  3. 高并发场景下,加锁失败后使用自旋重试(短时间重试,避免频繁请求),并设置重试上限。

问题 5:Redis 主从同步延迟导致的锁失效

原因:主节点加锁成功,未将锁同步到从节点,主节点宕机后从节点升主,新主节点无锁,其他节点可重新加锁。

解决方案

  1. 避免在主从 / 哨兵架构下使用单机版 Redis 锁,改用Redlock(基于独立 Redis 节点);
  2. 使用 Redisson 的主从锁(RedissonMasterSlaveLock),适配主从架构,底层通过 Lua 脚本保证主从同步后的锁一致性;
  3. 降低 Redis 主从同步的延迟(如使用主从直连、开启同步确认)。

问题 6:锁的粒度太大

原因:对整个资源加锁(如对所有商品的库存加一把锁),导致并发量急剧下降。

解决方案

  1. 细粒度加锁 :按资源维度拆分锁(如按商品 ID 分锁,lock:stock:1001lock:stock:1002);
  2. 分段锁:对大资源分段加锁(如将库存分为 10 段,每段一把锁,扣减库存时仅锁定对应段),提升并发量。

四、Redis 锁的选型建议

Redis 锁的选型核心围绕Redis 部署架构业务并发量功能需求 (重入、读写分离)和高可用要求展开,以下是不同场景的最优选型:

场景 推荐锁类型 实现方式 核心优势
Redis 单节点,低并发,无重入需求 单机版 Redis 锁 原生SET NX EX+Lua 脚本解锁 实现简单、性能最高
Redis 单节点 / 主从 / 集群,需要重入 / 公平锁 / 读写锁 可重入锁 / 读写锁 Redisson 客户端 功能丰富、内置看门狗、适配所有架构
Redis 集群,高可用核心业务(如订单、库存) Redlock 红锁 Redisson 的 RedLock 解决单点故障,锁安全性最高
读多写少的资源竞争场景 读写锁 Redisson 的 RReadWriteLock 读共享写互斥,提升并发量
分布式任务调度,避免任务重复执行 单机版 Redis 锁 / Redlock 原生命令 + 自旋重试 轻量、可靠,支持任务重试

五、Redis 锁与其他分布式锁的对比

分布式锁的实现方案还有ZooKeeper数据库 ,Redis 锁与它们相比,核心优势是性能 ,劣势是一致性(Redis 为最终一致性,ZooKeeper 为强一致性),以下是核心对比:

特性 Redis 锁 ZooKeeper 锁 数据库锁
性能 极高(内存操作,QPS 可达 10W+) 中等(基于 ZAB 协议,磁盘持久化) 低(磁盘 IO,行锁 / 表锁性能有限)
实现复杂度 中等(原生简单,Redlock/Redisson 复杂) 中等(基于临时节点 / 有序节点) 简单(基于行锁 / 唯一索引)
高可用 高(Redlock / 集群) 极高(ZooKeeper 集群,过半可用) 中等(数据库主从,需处理锁同步)
死锁风险 低(自动过期 + 看门狗) 极低(临时节点,会话断开自动删除) 高(需手动释放,易出现死锁)
一致性 最终一致性 强一致性 强一致性
适用场景 高并发、对一致性要求适中的场景 分布式协调、对一致性要求高的场景 低并发、小型系统,无需额外组件

六、Redis 锁的最佳实践

  1. 最小锁粒度:仅对资源竞争的核心代码块加锁,避免对整个方法 / 业务流程加锁,减少锁持有时间;
  2. 原子性操作 :加锁用SET NX EX/PX,解锁用 Lua 脚本,禁止非原子的分步操作;
  3. 唯一锁标识:必须设置节点 / 线程唯一的锁标识,防止误删其他节点的锁;
  4. 合理过期时间:预估业务执行时间,设置足够的过期时间,高并发场景配合看门狗机制;
  5. 避免自旋过度 :加锁失败后,自旋重试需设置重试次数 / 重试间隔,避免频繁请求 Redis 导致性能下降;
  6. 兜底机制:增加异常锁清理任务,定期清理过期未释放的锁,防止极端情况的死锁;
  7. 优先使用成熟框架:避免手动实现 Redlock / 可重入锁,优先使用 Redisson 等成熟客户端,内置各种优化和特性;
  8. 避免锁穿透:加锁前先校验资源是否存在,避免对不存在的资源频繁加锁;
  9. 监控与告警:监控 Redis 锁的加锁成功率、持有时间、过期次数,对异常情况(如加锁失败率过高、锁持有时间过长)及时告警。

总结

Redis 锁是分布式系统中最常用的分布式锁方案,核心围绕原子性过期机制 展开,从基础的单机版锁到高可用的 Redlock,再到功能丰富的可重入锁 / 读写锁,满足了不同场景的需求。原生 Redis 仅提供基础的锁能力 ,实际项目中推荐使用Redisson客户端,其封装了各种 Redis 锁的实现,解决了死锁、误删、看门狗等核心问题,且适配 Redis 所有部署架构。

使用 Redis 锁的核心原则:保证原子性、最小锁粒度、避免死锁、适配业务的并发和一致性要求

相关推荐
Prince-Peng2 小时前
技术架构系列 - 详解Redis
数据结构·数据库·redis·分布式·缓存·中间件·架构
虾说羊2 小时前
redis中的哨兵机制
数据库·redis·缓存
_F_y3 小时前
MySQL视图
数据库·mysql
2301_790300963 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
3 小时前
java关于内部类
java·开发语言
好好沉淀3 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin3 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder3 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
吨~吨~吨~3 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea