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

Redis里头怎么搞递增递减还带点拦截,防止数据乱跑的那些事儿

关于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提供了 WATCHMULTIEXEC 这一组命令(来源:Redis官方事务文档),你可以把它理解为一个简单的“乐观锁”:

  • WATCH user:1000:points:告诉Redis,帮我盯住这个键。
  • 你执行 GET 命令读取当前积分值,在程序里判断是否大于10。
  • 如果满足条件,用 MULTI 开启一个命令队列,然后把 DECRBY user:1000:points 10 命令放入队列。
  • 最后用 EXEC 命令尝试执行队列中的所有命令。 关键点来了:在 EXEC 执行的那一刻,Redis会检查你之前 WATCH 的键是否被其他客户端修改过,如果被改动了,整个 EXEC 就会失败,返回空值,你的扣除操作就不会执行,这样,你就实现了“检查后再安全修改”的拦截逻辑,你需要在自己的程序里处理这个失败,比如重试整个流程或者告诉用户操作失败。

更强大的脚本:LuaWATCH 处理复杂逻辑有时比较麻烦,需要重试,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里既灵活又安全地处理各种增减数字的需求,把数据“乱跑”的可能性降到最低。

Redis里头怎么搞递增递减还带点拦截,防止数据乱跑的那些事儿