当前位置:首页 > 问答 > 正文

Redis查看和修改那点事儿,怎么双重操作才不慌乱

Redis查看和修改那点事儿,怎么双重操作才不慌乱

(引用来源:根据日常运维和开发中常见的Redis数据操作场景总结)

咱们平时用Redis,很多时候不就是图它快嘛,存个用户状态、放个缓存数据、搞个临时计数器,顺手得很,但不知道你有没有遇到过这种让人心里一咯噔的情况:你想看看某个键的值是啥,看完之后觉得不对劲,想马上改掉它,就这“看一眼”和“改一下”两个简单的动作,要是没弄好,轻则数据不对,重则可能引发线上问题,今天咱就唠唠,怎么把这两个操作稳稳当当地绑在一块儿,心里不慌。

为啥会慌?场景还原一下

比如说,你负责一个商城系统,有个关键商品库存就存在Redis里,键叫 item_stock:1001,大促快来了,运营同事跟你说:“小王,快帮我看看那款热门手机的库存还剩多少,好像不对,如果少于100件你就直接给我改成500件吧。”

你一听,简单啊,顺手就打开了Redis客户端,敲了命令:

GET item_stock:1001

结果返回 50,你心里想:“哦,果然是50,小于100,得改成500。” 然后你紧接着输入:

SET item_stock:1001 500

操作完成,你松了口气,告诉运营搞定了。

但你可能没意识到,就在你执行 GETSET 这两个命令中间,哪怕只是隔了零点几秒,可能已经发生了这些事情:

  1. 另一个用户下单成功,系统扣减了库存,库存可能已经从50变成了49。
  2. 或者,另一个运维同学也因为别的原因,同时把库存改成了另一个值,比如800。

而你最后那个 SET 500 的操作,会毫无顾忌地覆盖掉中间发生的所有变化,结果就是,用户的实际下单数据丢了,或者别人的修改白干了,这就是典型的“读后写”问题,数据不一致的坑就这么埋下了。

不慌的关键:让“看”和“改”变成“原子操作”

所谓“原子操作”,你可以理解成一组命令被打包成一个不可分割的单元,就像你去银行转账,银行不会让你“先查A账户余额,再扣钱,再查B账户余额,再加钱”,而是把所有步骤打包成一个事务,要么全成功,要么全失败,中间别人看不到半截子的状态。

Redis提供了几种方式来实现这种“看和改”的原子绑定,咱们挑最常用的两种说。

使用Redis事务(MULTI/EXEC)—— 但这个方法有坑!

很多人第一个想到的是Redis的事务,它是这么用的:

MULTI
GET item_stock:1001
SET item_stock:1001 500
EXEC

你以为这就能保证原子性了?其实不然,在Redis事务里,GET 命令只是把操作排进队列,它返回的不是实际的值,而是一个“排队成功”的标识,真正的 GET 结果要在 EXEC 执行后,在所有返回结果的列表里才能看到。(引用来源:Redis官方文档对MULTI命令的说明,指出命令在EXEC调用前会被排队)

这意味着,你无法在事务内部根据 GET 的结果来决定是否要执行 SET,上面的例子相当于:“我不管库存现在是啥,我都要把它设为500。” 这显然不符合我们“只有小于100才设置”的逻辑,单纯用事务解决不了我们这个“先判断后修改”的需求。

使用Lua脚本 —— 推荐的王道

Lua脚本是Redis解决复杂原子操作的“瑞士军刀”,Redis会保证整个Lua脚本在执行时是原子的,也就是说,脚本执行过程中不会有其他命令插队。

我们把刚才的需求用Lua脚本来实现:

local current_stock = redis.call('GET', KEYS[1])
current_stock = tonumber(current_stock)
if current_stock < 100 then
    redis.call('SET', KEYS[1], 500)
    return "updated to 500"
else
    return "no update needed, current is " .. current_stock
end

然后你可以用 EVAL 命令执行这个脚本:

EVAL "上面那段Lua代码" 1 item_stock:1001

这下就完美了:

  1. 原子性保证:从读取 GET 到判断再到 SET,一系列操作一气呵成,中间不会有其他命令干扰,库存值在读取和修改的整个过程中是一致的。
  2. 逻辑内嵌:复杂的判断逻辑(比如小于100才修改)可以直接写在脚本里,Redis会帮你按顺序执行。
  3. 心里踏实:你只需要发起一次网络请求(执行EVAL命令),剩下的交给Redis服务器端完成,再也不用担心手速不够快或者网络延迟导致的数据覆盖问题了。

(引用来源:Redis官方文档强烈建议使用Lua脚本来处理需要原子性执行的复杂逻辑)

除了库存,还能用在哪儿?

这种“查看并修改”的模式应用非常广:

  • 计数器限流:检查一分钟内API调用次数,如果没超阈值就加1,超了就直接拒绝。
  • 状态切换:检查一个任务是否是“待处理”状态,如果是,则原子性地将其改为“处理中”,防止多个消费者同时处理同一个任务。
  • 分布式锁:检查某个锁键是否存在,不存在则设置它,这本身就是一个典型的“检查-设置”操作,通常用 SET key value NX 命令(它本身是原子的)来实现,更复杂的锁逻辑也可以用Lua。

总结一下

下次当你需要对Redis数据进行“看一眼再改一下”的操作时,别再分两次发 GETSET 命令了,先停下来想一想:

  1. 操作是否依赖读取到的值? 如果只是单纯设置,不管原值,那直接 SET 没问题。
  2. 如果依赖原值,且不允许中间被干扰,就别图省事,老老实实写一个Lua脚本,把“看”和“改”打包成一个原子操作。

养成这个习惯,你操作Redis的时候,心里自然就有底了,不会再为可能的数据竞争而慌乱,工具用对了,才能真的享受到Redis带来的速度和便利。

Redis查看和修改那点事儿,怎么双重操作才不慌乱