redis订单超时取消需用lua脚本实现被动触发+原子检查,因expire无可靠回调;须将时间判断与状态更新封装于单个脚本,配合zset索引避免全量扫描,并注意时钟同步、状态守卫及重复key处理。

Redis 用 Lua 实现订单超时自动取消,核心不是“定时”,而是“被动触发 + 原子检查”——你无法靠 Redis 自己跑 cron,得靠业务方主动调用或结合外部调度器轮询 EVAL 脚本。
为什么不能直接用 EXPIRE + 过期回调?
Redis 没有可靠的键过期回调机制:Redis Keyspace Notifications 虽然能发 expired 事件,但存在丢失、延迟、需额外消费服务(如订阅 __keyevent@0__:expired)等问题,生产环境难保证订单状态最终一致。更稳妥的做法是:把“是否超时”和“是否可取消”这两步压缩进一个 Lua 脚本里,由下游(比如支付轮询、订单查询接口、或独立的延迟队列消费者)来触发检查。
EVAL 脚本必须同时读取时间戳并原子更新状态
典型错误是先用 GET 拿订单创建时间,再在客户端判断是否超时,最后发 SET 改状态——这中间有竞态:两个请求同时读到“未超时”,都去改状态,结果只应取消一次的订单被重复处理。正确做法是把逻辑全塞进 Lua:
eval "local ctime = redis.call('hget', KEYS[1], 'created_at') if not ctime then return 0 end if tonumber(ctime)
<p>说明:</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理"><img
src="https://img.php.cn/upload/skill/000/000/081/178960683454849.jpg" alt="Redis Skill - 高性能缓存管理" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理" class="overflowclass">Redis Skill - 高性能缓存管理</a>
<p class="overflowclass">Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。</p>
</div>
<a rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
-
KEYS[1]是订单 hash key(如order:12345),避免硬编码 -
ARGV[1]是当前时间戳(秒级),由调用方传入,不依赖redis.call('time')(返回的是数组,且精度不够) - 脚本内用
hget/hset操作 hash 结构,比 string 更适合存订单多字段 - 返回
1表示已取消,0表示未超时或已取消,调用方可据此决定是否发 MQ 或更新 DB
如何让“超时检查”更轻量?别全量扫表
不能每次都在 Lua 里遍历所有订单。常见做法是配合有序集合(ZSET)做时间索引:
- 下单时执行:
ZADD order:timeout:zset 1717027200 order:12345(分值为超时时间戳) - 定时任务(如每分钟)执行:
ZRANGEBYSCORE order:timeout:zset -inf (now_timestamp,拿到待处理订单 ID 列表 - 对每个 ID 调用上面的 Lua 脚本——注意:这里要批量调用,别用 pipeline 包一堆
EVAL,而应写个批量版 Lua,用for i=1, #KEYS do ...循环处理多个 key - 处理完记得
ZREM已处理的 key,防止重复触发
这样既避免了全库扫描,又把“发现超时”和“执行取消”解耦,也方便监控积压量(查 ZCARD order:timeout:zset)。
容易忽略的边界:时钟漂移与重复执行
外部传入的 ARGV[1] 时间戳必须来自可信源(如应用服务器 NTP 同步后的时间),不能用客户端本地时间;Lua 脚本本身无事务回滚能力,一旦 hset 成功就不可逆,所以脚本里要加状态守卫(比如只允许从 pending 变成 cancelled,若已是 paid 则直接返回 0);另外,ZSET 的 ZRANGEBYSCORE 默认包含等于分值的元素,如果多个订单超时时间完全相同,要确保你的批量脚本能正确处理重复 key。










