Redis里说的CAS到底是啥,怎么用,有啥讲究和坑?
- 问答
- 2026-01-05 09:19:25
- 22
你问的Redis里的CAS,其实是一个用来解决并发问题的方法,咱们用一个最经典的例子来解释它到底要解决什么问题:秒杀抢购。
想象一下,Redis里存着一个键值对,"秒杀商品库存": 100,意思是还剩100件商品,现在有1万个用户同时点击“购买”,如果没有保护措施,可能会发生这样的情况:
- 用户A和用户B几乎同时从Redis读取库存,都读到了
100。 - 用户A计算
100 - 1 = 99,然后准备把99写回Redis。 - 但就在A写入之前,用户B也计算了
100 - 1 = 99,并且抢先一步把99写回了Redis。 - 用户A的写入操作也执行了,又把
99写了一遍。 - 最终结果是,库存变成了99,但实际上卖出了两单!这就叫“超卖”,对商家来说是重大事故。
这个问题的根源在于,“读取-计算-写入”这三个操作不是一个整体(用专业术语说就是“非原子性”),中间可以被其他客户端插队。
CAS是什么?
CAS的中文意思是“比较并交换”,它的核心思想非常朴素:我在修改一个值的时候,得先看看它是不是还是我当初读出来的那个老样子,如果没变,说明这中间没人动过,那我就可以放心修改;如果变了,说明有别人改过了,那我这次修改就放弃,然后重头再来。
在Redis里,实现CAS理念的核心命令就是WATCH,它不是什么复杂的黑科技,而是一种乐观锁机制,乐观锁就是假设一般情况下不会冲突,所以先不加锁直接操作,只在最后提交更新时检查一下有没有冲突,有冲突就失败重试。
CAS在Redis里怎么用?(引用来源:Redis官方文档对WATCH命令的说明)
根据Redis官方文档的描述,WATCH命令可以监视一个或多个键,如果在你开始监视之后,直到执行事务(EXEC)之前,有任何一个被监视的键被其他客户端修改了,那么你整个事务都会失败。
具体的使用步骤就像下面这样,我们还是用秒杀的伪代码例子:
-
开启监视:
WATCH "秒杀商品库存"这就好比你去买限量球鞋,先跟店员说:“嘿,请帮我看好这双42码的鞋,别卖给别人,我决定要买了。”
-
读取值:
current_stock = GET "秒杀商品库存"你看了下标价,确认要买。
-
业务计算:
new_stock = current_stock - 1心里算一下钱包够不够。
-
开启事务并执行:
MULTI// 开启一个事务块。SET "秒杀商品库存" new_stock// 把减1后的库存值放入事务队列。EXEC// 尝试提交事务。- 这步就像你拿着鞋去收银台结账,收银员(Redis)在扫码收款前,会先确认一下:“咦,这双鞋在我看着的时候,有没有被其他店员卖掉的记录?”如果没有(键没被改),那就成功收款,事务执行,如果有(键被改了),收银员就会告诉你:“不好意思先生,这双鞋刚刚被另一位顾客线上订走了。”然后事务整个失败,返回nil。
-
判断结果与重试:
- 如果
EXEC返回成功,说明你抢购成功了。 - 如果
EXEC返回失败(nil),说明在你WATCH之后库存被别人修改了(比如被别人买走了一件,或者库存直接归零了),这时候常见的做法是,放弃当前操作,或者从头开始重试整个流程(重新WATCH,重新GET,重新计算,直到成功或确实无库存为止)。
- 如果
有啥讲究和坑?
-
性能开销与重试成本:
WATCH本身开销不大,因为它只是给键打个标记,真正的成本在于事务失败后的重试,在高并发场景下,如果100个人同时WATCH一个键,最后只有1个人能成功,其余99个都得重试,这会增加系统的负载和请求的延迟,CAS适用于冲突不那么频繁的场景,如果冲突是常态(比如秒杀初期),可能需要结合其他方案(如Lua脚本)。 -
事务内不能有依赖前面结果的查询:(引用来源:Redis官方文档关于事务中错误处理的说明)这是一个大坑,在
MULTI和EXEC之间,你放入事务队列的所有命令,在真正执行EXEC之前,是不会立即执行的,你无法根据队列中前一个命令的结果来决定下一个命令是什么,你不能先GET库存,然后在事务里判断如果库存大于0才执行SET,所有逻辑判断必须在MULTI之前做好。 -
连接不能断开:
WATCH的效果是和当前的Redis连接绑定的,如果你监视了一些键,然后这个网络连接断开了,那么监视状态会自动清除,所以你的应用程序需要能处理连接异常的情况。 -
不是严格的串行化:
WATCH保证的是在你提交时,被监视的键没有被“意外”修改,但它并不能保证在所有情况下事务的绝对串行执行,在某些极端复杂的并发时序下,仍然可能出现边界情况,不过对于绝大多数应用来说,WATCH提供的保证已经足够安全了。 -
替代方案:Lua脚本:(引用来源:Redis官方文档对EVAL命令的介绍)正是因为
WATCH+事务的某些局限和复杂度,Redis从2.6版本开始支持Lua脚本,你可以把整个“读取-判断-写入”的逻辑写成一个Lua脚本,然后一次性发送给Redis执行,因为Lua脚本在Redis中是原子执行的(执行过程中不会插入任何其他命令),所以它天然就解决了并发问题,无需WATCH和重试,在性能和要求上,Lua脚本通常是实现复杂CAS逻辑的更优选择,比如上面的秒杀,用Lua脚本就是一两行代码的事,既简单又高效。
Redis里的CAS就是通过WATCH命令实现的一种乐观锁,用来在并发环境下安全地修改数据,它的用法是“监视-读取-计算-打包事务-提交检查”,核心讲究是最后提交时检查数据是否变化,变了就失败重试,主要的坑在于重试带来的性能压力、事务内无法进行逻辑判断以及连接依赖,对于更复杂的场景,现在更推荐使用Lua脚本作为替代方案。

本文由钊智敏于2026-01-05发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/74866.html
