Redis缓存中如何实现悲观锁的悲观策略?

来源:Nginx教程作者:芒果头衔:草根站长
导读:本期聚焦于芒果创作的《Redis缓存中如何实现悲观锁的悲观策略?》,敬请观看详情。当高并发请求同时修改同一缓存数据时,为何直接读取再写入会导致脏数据?Redis本身不提供内置锁机制,需要借助原子命令模拟悲观锁行为。悲观策略的核心在于假设冲突必然发生,因此线程在操作前必须先获取独占锁,阻塞其他竞争者直到当前事务完成。常用方案包括SETNX加过期时间、Redlock分布式锁以及Lua脚本保证原子性。不同方案在单节点与集群环境下表现差异明显,错误设置锁超时可能引发死锁或误删他人锁。理解锁释放的归属校验与自动续期机制,才能在生产环境安全使用Redis悲观锁。

在分布式系统里,Redis经常被当作共享缓存和轻量级协调组件使用。当多个服务实例同时尝试修改同一个缓存键值,例如库存扣减或账户余额更新,如果不加控制,后到的写操作会覆盖先到的计算结果,产生超卖或数据不一致。悲观锁的悲观策略认为并发冲突一定会发生,所以任何读写前都必须先拿到锁,只有持有锁的线程能操作资源,其余请求排队或快速失败。

Redis缓存中如何实现悲观锁的悲观策略?

基于SETNX的基础悲观锁实现

最简单的Redis悲观锁利用SETNX命令,它的含义是set if not exists,只有当键不存在时才能设置成功,借此模拟加锁动作。为避免持有锁的进程崩溃导致锁永远不释放,通常要附带过期时间,但早期Redis版本中SETNXEXPIRE不是原子操作,需要改用一条带参数的SET命令同时完成值与超时设置。

下面是一个使用Java与Jedis客户端的加锁示例,通过唯一标识区分锁的持有者,在释放时借助Lua脚本保证只删自己的锁。这种方式在单节点Redis中足够简单有效,但需要注意锁过期时间必须大于业务执行时间,否则会出现锁提前失效、多个线程同时进入临界区的问题。

// 尝试获取悲观锁,value为唯一客户端ID
String lockKey = "stock_lock";
String clientId = UUID.randomUUID().toString();
// 原子设置锁及过期时间,单位毫秒
String result = jedis.set(lockKey, clientId, "NX", "PX", 30000);
if ("OK".equals(result)) {
    try {
        // 执行缓存修改业务
        int stock = Integer.parseInt(jedis.get("stock"));
        jedis.set("stock", String.valueOf(stock - 1));
    } finally {
        // 释放锁的Lua脚本,避免误删其他客户端锁
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(clientId));
    }
}

上述方案的缺陷在于,如果业务执行时间偶发超过30秒,锁自动释放后别的线程加锁成功,原线程在finally中通过Lua比对clientId发现不一致就不会删锁,但此时它已修改过缓存,造成锁失效期间的并发写。因此生产环境往往需要引入看门狗线程定期续期,或者采用Redisson等封装好的可重入锁。

Redlock算法与集群环境悲观策略

当Redis以主从或集群模式部署,单节点加锁在故障切换时可能丢失锁记录,因为从节点还未同步主节点的锁键就被提升为主。Redlock提出向多个独立的Redis主节点申请锁,只要大多数节点加锁成功且总耗时小于锁有效期,就认为获取了分布式悲观锁。这种策略牺牲了一定性能来换取更高的可靠性。

Redlock的实现流程是客户端依次向N个节点发送SET加锁命令,计算获取锁耗费的时间,若超过半数节点成功且剩余有效期充足,则持锁;释放时向所有节点发送释放指令。下表对比了单节点锁与Redlock的差异:

方案节点要求优点缺点
单节点SETNX1个Redis实例延迟低、实现简单主从切换丢锁、单点故障
Redlock≥3个独立主节点容错性强、适合集群网络开销大、时钟漂移敏感

在实际使用中,Redlock也受到过质疑,例如依赖系统时钟且如果出现STW暂停可能导致锁过期。因此对于强一致性要求的金融场景,有些团队会改用ZooKeeper或etcd的临时顺序节点做悲观锁,而Redis悲观策略更适合能容忍极小概率不一致、追求高吞吐的缓存层控制。

用Lua脚本强化悲观锁的原子边界

悲观策略不只是加锁解锁,更关键的是把“判断锁归属+修改缓存+释放锁”或“加锁+初始化缓存”等步骤封装为原子操作。Redis执行Lua脚本时单线程串行化,不会被其他命令插入,这正好弥补了多条命令之间可能被抢占的漏洞。我们可以在脚本里完成锁检测与业务写入,避免网络往返带来的竞态。

例如下面的Lua脚本实现了在锁内安全递增缓存计数器的逻辑,只有携带正确锁标识的调用才能修改,否则返回错误。这样即便客户端与Redis之间网络延迟,也不会出现锁过了有效期却被旧请求改数据的混乱。脚本中的redis.call直接操作键空间,比在应用层反复往返更可靠。

-- KEYS[1]为锁键,KEYS[2]为业务数据键
-- ARGV[1]为客户端标识,ARGV[2]为锁超时毫秒
local lock = redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])
if not lock then
    -- 锁已被占用,返回0表示获取失败
    return 0
end
-- 在锁保护下递增业务键
local val = redis.call('incr', KEYS[2])
-- 此处可加入更多缓存修改逻辑
return val

使用Lua封装后,应用端只需一次EVAL调用即可完成加锁与业务变更,降低锁持有时间,也减轻了对锁续期机制的依赖。但要注意Lua脚本不能执行过长,否则阻塞Redis主线程,影响其他请求。因此悲观锁内的逻辑应保持轻量,复杂计算放到客户端完成,只把关键状态变更留在脚本里。

悲观策略的常见误区与规避

不少开发者在写Redis悲观锁时,会把锁键和业务数据键混用,或者释放锁时直接DEL而不校验持有者,这在客户端崩溃重试时极易删掉别人刚申请的锁,让悲观策略形同虚设。正确做法是锁值带唯一标记,释放必须走比对删除的Lua。

另一个误区是认为加了锁就不需要幂等。实际上网络抖动可能让客户端以为加锁失败但其实成功了,重试又会拿一次锁,如果业务不是幂等,重复执行仍会污染缓存。因此在Redis悲观策略之上,业务层仍需设计幂等标识或版本号,双重保障缓存一致性。

import redis
import uuid

r = redis.Redis(host='127.0.0.1', port=6379, db=0)
lock_key = 'order_lock'
data_key = 'order_count'
cid = str(uuid.uuid4())

if r.set(lock_key, cid, nx=True, px=10000):
    try:
        current = int(r.get(data_key) or 0)
        r.set(data_key, current + 1)
    finally:
        lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
        r.eval(lua, 1, lock_key, cid)

上面Python示例完整演示了单节点悲观锁的获取、业务修改与安全释放。在真实项目中,建议把这些逻辑封装为上下文管理器,配合超时与重试上限,既保留悲观策略的严谨,也避免代码分散导致疏漏。当缓存并发量极高时,还可结合本地锁做两层防护,先拦住同进程线程,再争抢Redis锁,减少无效网络竞争。

Redis悲观锁pessimistic_lock修改时间:2026-08-17 14:16:40

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。