Redis里头怎么搞递增递减还带点拦截,防止数据乱跑的那些事儿
- 问答
- 2026-01-25 06:30:27
- 5
关于Redis中实现递增递减并确保数据准确性的方法,主要涉及一些特定的命令和策略,其核心思想是利用Redis单线程执行和原子操作的特点,并辅以一些“检查”机制来防止意外。
最基础的武器:INCR 和 DECR
这是最直接的“递增递减”命令,你有一个键叫 user:1000:points,想给用户增加1点积分,直接用 INCR user:1000:points,想减少1点,就用 DECR,这两个命令的妙处在于,它们是原子性的(来源:Redis官方文档对INCR命令的描述),这意味着在Redis单线程模型下,无论有多少客户端同时发送这个命令,Redis都会一个一个执行,绝对不会出现两个客户端同时读到“10”,然后都写成“11”,导致实际只加了一次的混乱情况,这是防止数据“乱跑”的第一道,也是最基础的保障。
需要“拦截”的复杂场景:先检查,再操作
但光有 INCR/DECR 不够,你想实现“只有当用户积分大于10时,才能扣除10积分兑换礼物”,这个过程需要两步:先检查当前值,如果条件满足再减少,如果分开做,在“检查”和“扣除”之间,其他客户端可能已经修改了积分,导致你扣的时候积分已经不足10了,这就是数据“乱跑”了。
为了解决这个问题,Redis提供了 WATCH、MULTI、EXEC 这一组命令(来源:Redis官方事务文档),你可以把它理解为一个简单的“乐观锁”:
WATCH user:1000:points:告诉Redis,帮我盯住这个键。- 你执行
GET命令读取当前积分值,在程序里判断是否大于10。 - 如果满足条件,用
MULTI开启一个命令队列,然后把DECRBY user:1000:points 10命令放入队列。 - 最后用
EXEC命令尝试执行队列中的所有命令。 关键点来了:在EXEC执行的那一刻,Redis会检查你之前WATCH的键是否被其他客户端修改过,如果被改动了,整个EXEC就会失败,返回空值,你的扣除操作就不会执行,这样,你就实现了“检查后再安全修改”的拦截逻辑,你需要在自己的程序里处理这个失败,比如重试整个流程或者告诉用户操作失败。
更强大的脚本:Lua
用 WATCH 处理复杂逻辑有时比较麻烦,需要重试,Redis还支持 Lua脚本(来源:Redis官方EVAL命令文档),你可以把“读取、判断、修改”这一连串操作写成一个Lua脚本,然后用 EVAL 命令一次性发给Redis。Lua脚本在Redis中也是原子执行的,在执行期间不会被任何其他命令打断,就好像这个脚本是一个独立的命令一样,这就相当于你把整个需要“拦截”的业务逻辑,打包成了一个全新的、原子的Redis命令,这是最强大、最推荐用于处理复杂递增递减逻辑的方式,上面的兑换积分逻辑,可以写成一个脚本,内容大致是:
local current = redis.call('GET', KEYS[1])
if tonumber(current) >= 10 then
return redis.call('DECRBY', KEYS[1], 10)
else
return -1 -- 表示不足
end
这样,数据一致性的问题在服务器端就彻底解决了,没有竞态条件。
设置安全边界:SETNX 和 INCRBY 的配合
还有一种“拦截”是防止初始值被错误覆盖,你想设置一个初始值为0的计数器,但只在这个键不存在的时候才设置,可以用 SETNX 命令(SET if Not eXists),你可以先 SETNX mycounter 0,确保它只被初始化一次,之后就可以放心地用 INCRBY 来增加任意值了,这防止了在并发初始化时,可能出现的重复设置或覆盖已有值的问题。
总结一下思路:
- 简单的加一减一,直接用
INCR/DECR,它们本身是原子的,很安全。 - 对于“先判断后修改”的复杂业务(这是最容易“乱跑”的地方),优先使用 Lua脚本,它是终极解决方案。
- 如果不便使用Lua,可以用
WATCH+ 事务 来模拟乐观锁,但需要在客户端处理失败重试。 - 对于初始化等场景,使用
SETNX来“拦截”重复的创建操作。
这些方法组合起来,就能在Redis里既灵活又安全地处理各种增减数字的需求,把数据“乱跑”的可能性降到最低。

本文由雪和泽于2026-01-25发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/85564.html
