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

Redis Lua 脚本怎样把检查与扣减保持为一个原子操作

来源:17golang原创

时间:2026-11-14 03:36:00 137浏览 收藏

如果客户端先读取库存,再根据结果发送扣减命令,检查和扣减之间就隔着一次网络往返;两个请求可能同时读到同一个库存值,随后都判断“够用”并继续扣减。把这段逻辑放进 Redis Lua 脚本,由一次 EVAL 交给 Redis 在服务端执行,就能让读取、判断和写回处在同一个原子执行单元中。

稳定的做法是:所有脚本实际访问的键名放进 KEYS,扣减量和阈值放进 ARGV,脚本只返回约定好的状态码。这样业务客户端只需要处理“成功、库存不足、参数或命令错误”三类结果。

官方参考地址:https://redis.io/docs/latest/develop/programmability/eval-intro/

先划清 Lua 脚本的原子范围

Redis 官方文档说明,Lua 脚本在 Redis 服务端执行,脚本运行期间服务器活动会被阻塞,脚本产生的效果要么尚未发生,要么已经完成。这个语义适合把“读取当前值—判断—修改值”合并起来,但它不等于脚本可以无限制地承载业务流程。

以库存键 stock:book:42 为例,客户端单独发送 GET 和 DECRBY 时,两个请求会在中间插入新的请求。Lua 版本把库存读取和扣减放在脚本内部,客户端只发送一次调用;脚本的边界就是本次库存判断和写入。

客户端、EVAL、Lua 执行上下文、库存键、余额判断和扣减写入之间的静态关系说明图
图1:客户端请求、EVAL 与库存读写边界的结构说明图,不是 Redis 运行截图。

把键名和业务参数分别放入 KEYS 与 ARGV

EVAL 的前两个参数分别是脚本内容和键数量,后续参数先列出键名,再列出普通参数。脚本通过 KEYS 读取键名,通过 ARGV 读取扣减量、最低阈值等业务输入。不要把业务参数混进 KEYS,也不要在脚本里拼出另一个需要访问的键名。

这种区分不只是命名习惯。Redis 文档要求脚本访问的所有键都显式作为键参数提供,这对独立部署和 Redis Cluster 都重要。本文只有一个库存键,因此调用时键数量是 1;如果以后增加冻结库存键,就应该显式增加第二个 KEYS 参数,并确认相关键满足部署拓扑的约束。

EVAL 参数如何进入 KEYS 和 ARGV 并连接 GET、DECRBY 与状态码返回的结构说明图
图2:KEYS、ARGV 与脚本内部命令及状态返回的关系说明图,不是终端或控制台截图。

编写检查后扣减的 Lua 脚本

下面的脚本约定三个返回值:1 表示扣减成功,0 表示库存不足,-1 表示扣减量或阈值不合法。阈值默认允许为零,意味着库存扣减后不能低于零;如果业务需要保留安全库存,只需在调用时传入更大的阈值。

-- KEYS[1] 是库存键;ARGV 只承载扣减量和最低阈值
local stock_key = KEYS[1]
local amount = tonumber(ARGV[1])
local floor = tonumber(ARGV[2])

-- 参数错误直接返回业务约定的失败码,避免把无效输入当成库存不足
if amount == nil or floor == nil or amount 

调用时把固定脚本作为第一个参数,把键数量写成 1,再依次传入库存键、扣减量和最低阈值:

# 第三个参数 1 表示后面只有一个 Redis 键名
redis-cli EVAL "local amount=tonumber(ARGV[1]); local floor=tonumber(ARGV[2]); if not amount or not floor or amount

实际项目中不建议让客户端每次拼接长脚本文本;可以把脚本作为固定资源,通过客户端的脚本加载与缓存能力复用它。这里的命令只用于展示参数位置,返回值由客户端按整数处理。

用 redis.call 与返回值处理边界

redis.call() 会执行指定的 Redis 命令;如果命令参数错误或命令执行触发运行时错误,错误会直接返回给发起调用的客户端。对于库存脚本,GET 得到非数字内容时,tonumber 会得到空值,示例把它按零处理;如果业务不能接受这种数据,应改成显式错误分支,而不是默默当作空库存。

业务失败和命令错误要分开处理。返回 0 代表脚本正确执行但库存条件不满足,返回 -1 代表输入不符合约定;网络错误、脚本语法错误或 Redis 命令错误则属于调用失败。若需要在脚本内接住 Redis 命令错误,可以使用 redis.pcall(),它会把错误作为返回结果交给脚本继续判断,但不要用它掩盖数据格式问题。

客户端的处理契约可以保持很小:

结果含义客户端动作
1检查通过并已扣减继续创建订单或发放额度
0库存低于最低阈值返回库存不足,不重试同一业务请求
-1扣减量或阈值非法记录参数错误,修复调用方
Redis error脚本或命令执行失败按基础设施错误处理并评估重试

迁移清单:从 EVAL 调用到可维护脚本

  1. 固定脚本文本:把脚本当作版本化资源,业务变化时更新脚本版本,而不是为每个请求拼出不同源码。
  2. 核对参数契约:先写清 KEYS 的数量和顺序,再写 ARGV 的类型、范围和默认规则。
  3. 限制脚本范围:脚本只做必要的数据读取和条件写入,避免把慢查询、远程调用或长循环塞进原子区。
  4. 设计可区分返回值:把业务不满足和执行异常分开,让客户端不会因为“库存不足”而重复提交同一请求。
  5. 检查多键部署约束:如果脚本访问多个键,所有键都要显式放进 KEYS,并按目标部署方式检查键的组织规则。

总结一下,Redis Lua 脚本的价值不在于把多条命令简单拼在一起,而在于把一次业务判断收缩成服务端的原子边界。用 KEYS 声明资源,用 ARGV 传入业务参数,用固定状态码表达业务结果,再把命令错误单独交给基础设施处理,检查和扣减就会更容易维护。

相关问答

为什么不能在脚本里动态拼库存键?

脚本访问的键应显式作为 KEYS 输入,这样 Redis 才能知道脚本涉及哪些键,集群场景也能据此处理键的分布。需要访问另一个键时,应修改调用契约显式传入。

库存不足时应该返回错误还是返回 0?

库存不足是预期的业务分支,用整数状态码返回通常更容易让客户端区分;参数非法或 Redis 命令失败则应作为错误处理。关键是先固定契约,不要让同一个状态在不同调用方有不同含义。

使用 Lua 后还需要幂等键吗?

需要。Lua 保证的是脚本内部读写的原子性,不能自动判断客户端是否因为超时而重复提交同一个订单。订单创建、支付或消息投递仍应按业务需要设计幂等键。

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