Redis乐观锁怎么实现?Watch与CAS操作详解

来源:Ruby教程作者:周翰文头衔:网络博主
导读:本期聚焦于周翰文创作的《Redis乐观锁怎么实现?Watch与CAS操作详解》,敬请观看详情。当多个客户端同时修改同一个Redis键时,数据被覆盖的问题该怎么解决?悲观锁会阻塞其他请求影响性能,而乐观锁则通过版本比对的方式在不加锁的情况下保证数据一致性。Redis提供了Watch命令配合事务实现乐观锁,其核心思想类似CAS,即先监视键值,提交事务前检测键是否被修改过,一旦发生变化就放弃本次执行,由客户端重试。本文将深入讲解Watch的底层实现原理、事务执行的完整流程、Lua脚本替代方案,以及乐观锁在高并发秒杀、库存扣减等场景中的实战代码,同时分析乐观锁失败重试的代价与适用边界,帮助你判断业务是否适合这种方案。

在并发场景下操作共享数据时,最直接的思路是加锁,但锁会带来性能开销和死锁风险。Redis从2.2版本开始提供了另一种选择:通过WATCH命令实现乐观锁,配合事务(MULTI/EXEC)达到类似CAS(Compare And Swap)的效果。这种方式不会阻塞其他客户端,只在提交时检测数据是否被别人改过,特别适合读多写少、冲突概率低的场景。本文将从原理、代码实战到方案对比,完整拆解这套机制。

Redis乐观锁怎么实现?Watch与CAS操作详解

一、乐观锁与悲观锁的本质区别

要理解Redis的Watch机制,先要弄清楚乐观锁和悲观锁的差异。悲观锁假设冲突一定会发生,所以在操作数据前先把资源锁住,其他客户端必须等待锁释放。Redis的SETNX配合过期时间实现分布式锁就是典型的悲观锁思路,它的问题是高并发下大量请求被阻塞,吞吐量下降明显。

乐观锁则相反,它假设冲突很少发生,操作前不加任何锁,只在数据提交那一刻检查数据是否被修改过。如果数据版本与自己读取时一致,说明没有冲突,执行成功;如果不一致,说明期间有别的客户端改过数据,本次操作作废,由客户端决定是否重试。这种机制在冲突率低的环境下性能非常好,因为省去了获取锁和释放锁的开销。

CAS是乐观锁在CPU指令层面的经典实现,包含三个要素:内存位置、预期旧值、新值。只有当内存位置的值等于预期旧值时,才把它更新为新值,否则什么都不做。Redis的Watch事务本质上就是应用层的CAS模拟:Watch记录读取时的值作为预期旧值,EXEC提交时比对当前值,一致则执行,不一致则返回nil。

二、Watch配合事务的执行流程

Watch的使用有一个固定套路:先WATCH一个或多个键,接着读取数据并在本地计算新值,然后MULTI开启事务,把写命令入队,最后EXEC提交。整个流程的关键在于:如果在WATCH之后、EXEC之前,任何一个被监视的键被其他客户端修改(包括过期删除),事务就会被打断,EXEC返回nil,队列中的所有命令都不会执行。

用Redis-cli演示一个完整的会话:

127.0.0.1:6379> SET stock 100
OK
127.0.0.1:6379> WATCH stock
OK
127.0.0.1:6379> GET stock
"100"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECR stock
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 99

如果在EXEC之前另一个客户端执行了SET stock 50,那么上面的EXEC会返回(nil),DECR不会执行。这就避免了"先读后写"过程中的脏写问题。需要注意几个细节:第一,WATCH必须在MULTI之前调用,事务内调用会报错;第二,EXEC执行后无论成败,客户端的所有Watch都会自动取消;第三,可以通过UNWATCH手动取消监视。

还有一个容易被忽视的坑:事务被打断时Redis不会报错,只是EXEC返回空回复。客户端代码必须检查这个返回值,否则你以为扣减了库存,实际上什么都没发生,造成业务数据不一致。

三、Java实战:库存扣减的乐观锁代码

下面用Jedis演示一个带重试的库存扣减逻辑。由于乐观锁失败后需要重试,代码通常包在一个循环里,设定最大重试次数防止无限循环:

public boolean deductStock(Jedis jedis, String key, int retryMax) {
    for (int i = 0; i < retryMax; i++) {
        // 监视库存键
        jedis.watch(key);
        String val = jedis.get(key);
        if (val == null) {
            jedis.unwatch();
            return false;
        }
        int stock = Integer.parseInt(val);
        if (stock <= 0) {
            // 库存不足,取消监视后退出
            jedis.unwatch();
            return false;
        }
        Transaction tx = jedis.multi();
        tx.set(key, String.valueOf(stock - 1));
        List<Object> result = tx.exec();
        if (result != null && !result.isEmpty()) {
            return true; // 执行成功
        }
        // exec返回null说明事务被打断,进入下一轮重试
    }
    return false;
}

代码里有两处必须强调:一是库存不足的分支里调用了unwatch(),否则Watch会残留到下一次命令,可能带来意外行为;二是tx.exec()返回null表示键被其他客户端动过,此时事务已自动失效,直接进入下一轮循环重新WATCH、重新读值即可。

Spring Data Redis的写法类似,用SessionCallback保证多个操作在同一个连接上执行,因为WATCH和后续的MULTI必须来自同一个客户端连接:

public Boolean deductWithTemplate(RedisTemplate<String, String> template, String key) {
    return template.execute(new SessionCallback<Boolean>() {
        @Override
        public Boolean execute(RedisOperations ops) throws DataAccessException {
            ops.watch(key);
            String val = (String) ops.opsForValue().get(key);
            if (val == null || Integer.parseInt(val) <= 0) {
                ops.unwatch();
                return false;
            }
            ops.multi();
            ops.opsForValue().set(key, String.valueOf(Integer.parseInt(val) - 1));
            List<Object> rs = ops.exec();
            return rs != null && !rs.isEmpty();
        }
    });
}

四、乐观锁的适用边界与替代方案

乐观锁并非万能。它的性能与冲突率直接相关:并发越高、同一键的争抢越激烈,事务被打断的概率越大,客户端不得不反复重试,CPU空转,吞吐量反而不如悲观锁。以秒杀场景为例,上万个请求同时抢一个库存键,几乎每个事务都会被打断,重试风暴会让系统雪崩。

针对高冲突场景,更稳妥的方案有两个。第一个是改用原子命令,比如直接用DECRDECRBY,它本身是单命令原子操作,不需要事务也不需要Watch,判断返回值是否小于零再补回即可:

127.0.0.1:6379> DECR stock
(integer) -1
127.0.0.1:6379> INCR stock
(integer) 0

第二个方案是Lua脚本。Redis执行脚本是串行的,脚本内读取、判断、写入一气呵成,天然没有并发冲突,也就不需要重试逻辑:

local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock <= 0 then
    return 0
end
redis.call('DECR', KEYS[1])
return 1

简单总结一下选型思路:读多写少、冲突概率低、业务允许客户端重试的场景,用Watch乐观锁最合适,实现简单且不阻塞;热点键高并发写,优先考虑原子命令或Lua脚本;需要复杂互斥逻辑(比如锁内调用外部服务)时,再考虑SETNX分布式锁。理解了Watch的CAS本质,你就能在各种并发方案之间做出准确的取舍。

Redis乐观锁Watch命令CAS操作修改时间:2026-09-08 14:49:11

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