Redis Functions 部署库存扣减逻辑的幂等边界
来源:17golang原创
时间:2026-10-01 20:27:48 468浏览 收藏
库存扣减真正难处理的不是“减一”,而是客户端超时后不知道第一次调用是否已经成功。如果调用方直接重试,同一订单可能再次扣减;如果一律不重试,又可能把一次网络抖动误判成失败。Redis Functions 适合把“查幂等回执、校验库存、扣减、写回执”封装成一个服务端原子操作,但它只解决 Redis 内部这一段,不会自动把订单数据库、支付和消息系统变成同一个事务。
官方文档:https://redis.io/docs/latest/develop/programmability/functions-intro/
业务负载:一次扣减为什么会被调用两次
假设下单接口按单个 SKU 扣减库存。请求带有 sku_id、order_id、扣减数量和幂等回执有效期。服务端处理完成后,响应可能在网络中丢失;网关、消息消费者或客户端会按同一个 order_id 重试。此时系统需要返回第一次的结果,而不是再次执行扣减。
本文把结果契约限定为四种状态:
| 返回码 | 含义 | 是否写成功回执 |
|---|---|---|
1 | 首次扣减成功 | 是 |
2 | 重复请求,返回首次成功后的剩余库存 | 读取已有回执 |
0 | 当前库存不足 | 本文示例不写 |
-2 | 库存键不存在 | 否 |
“库存不足是否也要幂等”必须由业务决定。示例只固化成功结果,因此补货后同一订单重试可能成功;如果业务要求第一次库存不足后永远返回不足,就要把失败原因也写入回执,并明确它的有效期。
约束条件:原子执行不等于永久幂等
Redis 函数的执行具有原子性,执行期间会阻塞其他客户端操作,因此检查库存与扣减之间不会被另一条命令插入。但原子性只覆盖当前 Redis 实例上的函数调用,还存在五个必须显式承认的边界:
- 回执键过期或被淘汰后,同一订单再次到达会被当作新请求。
- Redis Cluster 中函数访问的键必须显式作为 key 参数传入,并满足槽位约束。
- 函数运行越久,阻塞其他客户端的时间越长,不能在函数里扫描大集合或做慢循环。
- Lua 运行时错误不会提供关系型数据库式的自动回滚,因此所有可预检条件应在第一次写入前完成。
- Redis 成功不代表订单数据库已提交;跨系统一致性仍需事务消息、补偿或对账。
Redis Functions 从 Redis 7.0 起可用。函数库会作为数据库的一等对象被持久化和复制,但最终耐久性仍取决于实际的 AOF/RDB、复制、故障切换和备份策略。
方案对比:为什么这里选择 Redis Functions
| 方案 | 优点 | 主要代价 |
|---|---|---|
| 客户端先 GET 再 DECRBY | 实现简单 | 检查与扣减之间存在并发窗口,重复请求也无回执 |
| WATCH / MULTI | 使用原生命令 | 冲突时客户端重试,热点 SKU 下重试成本明显 |
| EVALSHA | 可原子组合命令 | 脚本缓存可能丢失,应用需要处理加载与 NOSCRIPT |
| Redis Functions | 命名函数、函数库统一部署,并随数据持久化与复制 | 要求 Redis 7+,部署和升级属于数据库运维动作 |
对稳定复用的库存 API,Redis Functions 的价值不只是减少一次网络往返,而是把函数名、参数、返回码和版本变成可部署契约。调用方只需要使用 FCALL,不再随请求发送整段脚本。
推荐架构:库存键与回执键同槽
每次调用传入两把 key:库存键保存可用数量,幂等键保存首次成功后的剩余数量。Redis Cluster 使用大括号中的哈希标签计算槽位,因此下面两把键都会按 {sku-42} 路由:
stock:{sku-42}:availableidem:{sku-42}:order-9001
把订单号放入幂等键可以隔离不同订单,把 SKU 哈希标签放在两把键中可以确保函数访问同一槽。若一张订单同时扣多个 SKU,这个模型就不再成立;应拆分为每 SKU 预占,再由订单层编排和补偿,而不是用跨槽函数硬凑成全局事务。

实现函数:先完成校验,再执行写入
下面的函数只处理一个 SKU。它先读取幂等回执,再检查参数和库存;只有全部条件满足后,才执行 DECRBY 与 SET EX。写函数不要声明 no-writes 标志,Redis 默认会把未声明标志的函数按可能读写处理。
#!lua name=inventory_lib_v1
-- 返回:{1, remaining} 首次成功;{2, remaining} 重复成功;
-- {0, current} 库存不足;{-2, 0} 库存键不存在。
local function inventory_deduct(keys, args)
if #keys ~= 2 then
return redis.error_reply('ERR expected stock key and receipt key')
end
local quantity = tonumber(args[1])
local ttl_seconds = tonumber(args[2])
if not quantity or quantity
代码里没有使用 SET NX,因为同一个函数调用期间不存在另一位客户端插入执行的机会;真正的幂等判断是函数开头对回执键的读取。这里仍然先读取两个键并校验参数,避免常见的类型错误发生在扣减之后。
部署与调用:把函数库当成数据库制品
函数库内容以 shebang 开头,当前 Lua 引擎名称为 lua。加载是管理动作,调用是业务动作,二者应使用不同权限。发布流水线可以通过标准输入加载文件:
# 用同名函数库整体替换旧定义,加载前应在预发环境完成兼容测试。 redis-cli -x FUNCTION LOAD REPLACE
REPLACE 替换的是同名函数库,库内函数不能单独更新。为了避免旧调用方在参数或返回码变化时被突然破坏,建议保持 inventory_deduct_v1 契约不变;不兼容变更注册为 inventory_deduct_v2,调用方逐步切换后再清理旧版本。
调用时,FCALL 后面的数字 2 表示紧随其后的两个参数是 key,其余是普通参数:
# 扣减 sku-42 的 3 件库存,成功回执保留 7 天。
redis-cli FCALL inventory_deduct_v1 2 \
'stock:{sku-42}:available' \
'idem:{sku-42}:order-9001' \
3 604800
# 使用同一订单号重试时读取首次回执,不应再次扣减。
redis-cli FCALL inventory_deduct_v1 2 \
'stock:{sku-42}:available' \
'idem:{sku-42}:order-9001' \
3 604800
生产调用方应把数组返回值映射成稳定的领域结果,不要把 Redis 错误文本直接暴露给终端用户。连接超时、MOVED/ASK 路由和服务端错误仍由 Redis 客户端层处理;只有使用同一订单号、同一 SKU 和同一参数重试,才符合本文的幂等契约。

风险点:这段函数不能替你保证什么
1. 回执 TTL 就是重复扣减防线的时间边界
示例设置 7 天只是展示参数,不是通用推荐值。TTL 至少应覆盖 API 重试、消息重投、人工补偿和订单回放的最长窗口。若业务要求订单永久不可重复扣减,应把最终结果写入持久订单账本,并让 Redis 回执只承担快速判重。
2. maxmemory 淘汰可能提前删除回执
即使 TTL 尚未到期,允许淘汰的内存策略仍可能删除幂等键。高价值库存不能把 Redis 中一个可淘汰键当作唯一事实来源。需要结合独立实例、合适的淘汰策略、内存水位监控和业务账本来设计。
3. 原子执行不是回滚事务
函数执行期间其他客户端看不到中间状态,但 Lua 运行时发生错误时,Redis 不会像关系型数据库那样自动撤销已经执行的写命令。因此函数要保持短小,所有参数、键类型和可预见条件尽量在首个写命令之前检查,不在写入后调用可能因输入而失败的复杂命令。
4. Redis 成功与订单提交仍可能分叉
函数返回成功后,订单数据库写入可能失败;反过来,Redis 响应也可能丢失。常见做法是把库存扣减视为预占,订单系统记录同一个业务幂等键,再通过超时释放、事务消息或周期对账修复分叉。本文函数提供的是可靠的 Redis 侧原语,不是跨系统的最终一致性方案。
5. 慢函数会阻塞整个 Redis 执行线程
函数应只做固定次数的读写,不执行 SCAN、大范围集合遍历或外部 I/O。Redis 脚本运行在沙箱中,不能访问文件系统和网络;超过忙碌阈值也不会被安全地自动中断已写入的函数。
落地清单
- 给函数名、参数顺序、返回码和错误语义建立版本化契约。
- 让库存键与幂等键使用相同哈希标签,并全部通过
FCALL的 key 参数传入。 - 保证同一订单重试时 SKU、数量和业务语义不变;参数变化应使用新业务请求。
- 按消息重投和人工补偿周期确定回执 TTL,不照搬示例值。
- 在首个写命令前校验参数、键存在性、数据类型和库存值域。
- 把函数库加载权限与业务调用权限分离,记录每次库版本发布。
- 确认 AOF/RDB、复制、故障切换、备份和 maxmemory 策略与库存等级匹配。
- 为库存预占建立释放、对账和人工修复通道,覆盖 Redis 与订单库分叉。
相关问题
Redis Functions 比 Lua EVAL 一定更快吗?
选择 Functions 的主要理由是可管理、命名、持久化和统一部署,不应把它简单宣传成无条件更快。真实性能仍取决于命令数量、数据大小和函数执行时间。
库存不足结果要不要写幂等键?
如果补货后允许原订单重试成功,就不要固化库存不足;如果首次结果必须稳定,就应保存失败回执,并把有效期写入业务契约。
可以在一个函数里扣减多个 SKU 吗?
单机模式技术上可以访问多键,但 Cluster 要受槽位约束。多个 SKU 通常分布在不同槽,更适合拆成单 SKU 预占,由订单层协调补偿,而不是依赖跨槽函数。
-
117 收藏
-
426 收藏
-
171 收藏
-
113 收藏
-
195 收藏
-
394 收藏
-
386 收藏
-
357 收藏
-
101 收藏
-
145 收藏
-
351 收藏
-
460 收藏
-
471 收藏
-
413 收藏
-
327 收藏
-
169 收藏
-
244 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习