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]。

用 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}:stock 和 order:{1001}:sold。不要只改脚本参数顺序,路由槽位不会因此改变。
应该一直发送 EVAL 还是使用 EVALSHA?
脚本稳定后可以先用 SCRIPT LOAD 缓存,再用 EVALSHA 调用以减少重复传输;缓存会因重启、故障切换或刷新而消失,客户端仍应准备回退加载。
为什么不直接用 MULTI/EXEC?
只需要把一组已经确定的命令排队时,事务就够用;如果必须先读取、判断,再决定是否更新多个 key,Lua 可以把条件逻辑和写入放在服务端一次完成。
把 key 边界、参数边界和失败边界先设计清楚,Lua 才真正适合做 Redis 的多 key 原子更新。上线前重点检查 Cluster 槽位、脚本耗时和异常后的对账方案。
-
287 收藏
-
数据库 · Redis | 2小时前 | Redis · 消息队列 · Stream · 消费组 · XREADGROUP XACK XPENDING XAUTOCLAIM Redis Streams Pending List342 收藏
-
349 收藏
-
456 收藏
-
387 收藏
-
478 收藏
-
205 收藏
-
423 收藏
-
490 收藏
-
440 收藏
-
237 收藏
-
273 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习