登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

Redis Lua 脚本中如何保证多个 key 原子更新

来源:17golang原创

时间:2026-09-10 17:57:25 311浏览 收藏

要同时修改 Redis 中的多个 key,最稳妥的做法是把“读取、判断、写入”放进一次 Lua 脚本调用。Redis 会在脚本运行期间串行执行其中的命令,其他客户端不会插入到读取和两次更新之间,因此库存扣减与已售计数可以保持并发可见性上的一致。

要点速览
  • 脚本访问的 key 全部放在 KEYS,业务值放在 ARGV
  • Redis Cluster 中,多个 key 必须落在同一个 hash slot,常用做法是使用相同的 hash tag。
  • Lua 脚本是不可交错执行的,但脚本内部运行时错误不会替业务自动补偿回滚。

先把多个 key 设计成一个原子更新单元

下面用库存场景说明:stock:{sku-1001}:available 保存可用库存,stock:{sku-1001}:sold 保存已售数量。花括号里的 sku-1001 是 Redis Cluster 的 hash tag,两个 key 会使用同一段标签计算槽位,脚本才能在集群中作为一个多 key 操作执行。

脚本中的 key 名不要通过字符串拼接生成,也不要从 Redis 里的值推导。调用方提前声明全部 key,脚本用 KEYS[1]KEYS[2] 访问;扣减数量则放到 ARGV[1]

Redis Lua 原子更新中 KEYS、ARGV、库存与已售计数的边界关系技术框图
图1:把两个数据 key 放入同一更新边界,业务参数通过 ARGV 进入脚本。

用 Lua 完成检查、扣减和累计

帮助读者理解脚本内的读取、条件判断、两个写命令与返回值之间的静态关系。
图2:查看 Lua 脚本中的校验边界和两个写入组件,理解为什么客户端不会看到半更新状态。

这个脚本先读取可用库存,扣减量不合法或库存不足时直接返回;只有校验通过,才同时执行两个写命令。关键点是不要先在客户端执行 GET,再单独发送 DECRBY,否则两个请求之间可能被另一个购买请求抢走库存。

-- 先检查输入,再在同一脚本中更新两个相关 key
local available = tonumber(redis.call('GET', KEYS[1]) or '0')
local delta = tonumber(ARGV[1])

-- 拒绝非正数,避免把扣减脚本变成意外的加库存脚本
if not delta or delta 

返回数组的第一个元素是状态:1 表示扣减成功,0 表示库存不足;第二个元素是当前可用量。生产代码可以把它映射成明确的业务结果,而不是只判断命令是否返回错误。

通过 EVAL 显式传入两个 key

准备初始数据后,用 EVAL 的第二个参数声明 key 数量。这里的 2 很重要:Redis 会把后面的两个参数放进 KEYS,再把逗号之后的参数放进 ARGV

# 初始化两个同槽位 key;注释说明每个值的业务含义
redis-cli SET 'stock:{sku-1001}:available' 8
redis-cli SET 'stock:{sku-1001}:sold' 0

# 2 表示后面有两个 KEYS,最后的 3 会进入 ARGV[1]
redis-cli EVAL "local n=tonumber(redis.call('GET',KEYS[1]) or '0'); local d=tonumber(ARGV[1]); if n 

成功时返回类似 1, 5,随后可分别读取两个 key 检查业务结果。若把扣减量误放到两个 key 之后的数量位置,或者把 numkeys 写成 1,Lua 看到的数组就会错位,这是最常见的调用错误。

原子执行的边界:不交错,不等于自动回滚

Redis 的脚本执行期间会阻塞其他服务端活动,所以其他客户端不会读到“库存已经减了、已售还没加”的中间状态。但原子执行主要解决并发交错问题,不应被理解成带数据库事务回滚的异常恢复机制。

因此要把可能失败的输入校验放在第一条写命令之前,尽量使用参数明确、类型稳定的命令。脚本执行中如果在第一条写命令之后又遇到命令错误,业务层仍需要幂等键、补偿任务或对账机制兜底;不要用一段长脚本包住大量不可控逻辑。

检查项正确做法常见误区
key 传递所有访问的 key 放入 KEYS在 Lua 中拼接 key 名
集群路由多 key 使用相同 hash tag两个 key 分属不同槽位
参数位置业务值放 ARGV把值当成 key 传入
脚本长度只保留短小、可预测的更新逻辑在脚本中塞入慢查询或复杂循环

常见问题

多个 key 不在同一个 Cluster 槽位怎么办?

调整 key 命名,让共同的业务标识放进相同的花括号 hash tag,例如 order:{1001}:stockorder:{1001}:sold。不要只改脚本参数顺序,路由槽位不会因此改变。

应该一直发送 EVAL 还是使用 EVALSHA?

脚本稳定后可以先用 SCRIPT LOAD 缓存,再用 EVALSHA 调用以减少重复传输;缓存会因重启、故障切换或刷新而消失,客户端仍应准备回退加载。

为什么不直接用 MULTI/EXEC?

只需要把一组已经确定的命令排队时,事务就够用;如果必须先读取、判断,再决定是否更新多个 key,Lua 可以把条件逻辑和写入放在服务端一次完成。

把 key 边界、参数边界和失败边界先设计清楚,Lua 才真正适合做 Redis 的多 key 原子更新。上线前重点检查 Cluster 槽位、脚本耗时和异常后的对账方案。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>