登录
首页 >  数据库 >  Redis

Redis ZSET 延迟队列实战:订单超时取消这样做更稳

来源:17golang原创

时间:2026-06-13 04:02:59 116浏览 收藏

订单创建后 15 分钟未支付要自动取消,这是很多业务都会遇到的场景。最直接的做法是定时扫数据库,但数据量一大,扫描成本和延迟都会上来。Redis ZSET 很适合做轻量延迟队列:把“到期时间”放到 score,把“任务标识”放到 member,到点后由后台任务取出处理。

适合人群:正在处理订单超时、优惠券过期、消息延后提醒、任务延迟触发的后端同学。本文重点讲方案和工程细节,不依赖复杂消息队列,也不把 Redis 当成永久存储。

目录

  • ZSET 延迟队列适合什么场景
  • 写入任务:score 存到期时间
  • 扫描到期任务:查找、抢占、处理
  • 失败重试和防重复处理
  • 常见坑和上线检查

一、ZSET 延迟队列适合什么场景

ZSET 延迟队列适合“允许秒级延迟、任务量中等、逻辑简单”的场景。比如订单超时取消、内容发布提醒、短信补发、活动状态切换等。它的优势是结构简单、部署成本低、排查方便;限制是可靠性和吞吐不如专门的消息队列。

它的核心模型只有两个字段:

  • score:任务应该被处理的时间戳,通常使用毫秒或秒。
  • member:任务唯一标识,例如 `order:10001`、`coupon:888`。

二、写入任务:score 存到期时间

当订单创建成功后,把订单号写入延迟队列,score 设置为“当前时间 + 15 分钟”。后台扫描任务只需要取 score 小于等于当前时间的 member,就能找到已经到期的任务。

Redis ZSET 延迟队列从订单创建、写入到期时间、等待扫描到自动取消的流程图

# 订单 10001 创建后,15 分钟后检查是否未支付
redis-cli ZADD order:delay 1781295000 order:10001

# 查看队列里最早到期的任务
redis-cli ZRANGE order:delay 0 0 WITHSCORES

member 建议带上业务类型和业务 id,不要只写一个数字。这样排查时看到 `order:10001` 就知道它是订单任务,也方便后面扩展不同业务队列。

三、扫描到期任务:查找、抢占、处理

后台任务每隔一小段时间扫描一次到期数据。扫描到候选任务后,先尝试从 ZSET 中删除;删除成功的任务才归当前工作进程处理。这个“先删再处理”的动作可以减少多个工作进程重复处理同一个任务。

package main

import (
    "context"
    "log"
    "strconv"
    "strings"
    "time"
)

type RedisClient interface {
    ZRangeByScore(ctx context.Context, key string, min string, max string, limit int64) ([]string, error)
    ZRem(ctx context.Context, key string, members ...string) (int64, error)
    ZAdd(ctx context.Context, key string, score int64, member string) error
}

func scanDelayQueue(ctx context.Context, redis RedisClient) {
    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case 

上面的示例保留了核心结构:按时间取出候选任务,用 `ZREM` 抢占,抢占成功后再处理业务。真实项目中可以把 `limit`、扫描间隔、重试间隔做成配置项。

四、失败重试和防重复处理

延迟队列一定要考虑失败路径。比如取消订单时数据库短暂不可用,直接丢弃任务会让订单一直停留在未支付状态。比较稳的做法是:处理失败后重新写入队列,score 设置为下一次重试时间。

Redis ZSET 延迟队列任务到期后抢占处理、失败重试和成功落库的分支图

同时,业务处理函数本身也要防重复。以订单取消为例,应该在数据库更新时限制状态:

UPDATE orders
SET status = 'closed', close_reason = 'timeout'
WHERE id = 10001 AND status = 'unpaid';

这条条件很重要:如果用户已经支付成功,状态不再是 `unpaid`,延迟任务即使到达也不会把订单错误关闭。Redis 队列负责触发,数据库状态负责最终判断。

五、常见坑和上线检查

1. 不要一次取太多到期任务

每次扫描建议加 limit,比如 20、100、500。一次取太多会让单个工作进程处理时间过长,也容易造成 Redis 和业务数据库突刺。

2. 不要只依赖前端倒计时

前端倒计时只是展示,用户关掉页面后就不会继续运行。订单超时这种业务规则必须由服务端延迟任务或后台任务保证。

3. 重试要有上限或告警

如果某个任务一直失败,可以记录失败次数,超过阈值后写入异常表或发送告警。否则它会在队列里反复出现,掩盖真正的问题。

4. 上线前自测清单

  • 创建订单后,ZSET 里能看到对应 member 和到期 score。
  • 任务到期后,只有一个工作进程能抢占成功。
  • 订单已支付时,到期任务不会把订单关闭。
  • 处理失败时,任务会按下一次重试时间重新进入队列。

总结

Redis ZSET 延迟队列的关键是把时间作为 score,把任务标识作为 member。工程上要补齐四件事:写入时确保 member 唯一,扫描时限制批量大小,处理前用 `ZREM` 抢占,业务落库时做状态条件判断。这样就能用很轻的成本完成订单超时取消这类延迟任务。

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