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

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}:available
  • idem:{sku-42}:order-9001

把订单号放入幂等键可以隔离不同订单,把 SKU 哈希标签放在两把键中可以确保函数访问同一槽。若一张订单同时扣多个 SKU,这个模型就不再成立;应拆分为每 SKU 预占,再由订单层编排和补偿,而不是用跨槽函数硬凑成全局事务。

Redis Functions 库存键与幂等回执键静态结构图
图1:结构示意图,展示库存服务、数量参数、同槽库存键、幂等回执键、扣减函数和返回结果之间的静态关系。

实现函数:先完成校验,再执行写入

下面的函数只处理一个 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 和同一参数重试,才符合本文的幂等契约。

Redis Functions 函数库部署与调用边界静态结构图
图2:结构示意图,展示函数库、版本化函数名、管理部署、FCALL 调用、ACL、持久化与副本之间的边界。

风险点:这段函数不能替你保证什么

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 预占,由订单层协调补偿,而不是依赖跨槽函数。

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