当前位置: 首页 > article >正文

订单自动关闭方案设计

订单自动关闭本质上是一类延时任务如何处理的问题,具体的场景可能有:

  • 订单超时未支付自动关闭
  • 自动确认收货
  • 社交平台定时发布
  • 超时未取件自动退回
  • 用户注销15天后自动删除

方案1:定时任务扫表

建立一个 cronjob 每隔一段时间扫一次表,查询所有到期的订单,执行关单操作。

问题:

  • 时间不精准:定时任务是基于固定的频率执行的,如果要保证精准度就要增加频率,不易控制
  • 无法处理大数据量:数据量大会导致任务执行时间长,订单被扫描到的时间会延后
  • 数据库压力: 数据库 IO 在短时间内被大量占用和消耗
  • 分库分表问题:如果有分库分表场景,进行全表扫描效率很低
  • 分布式问题:多个节点到同一时间同时查询数据库,需要额外的分布式锁设计

这种方案适用于业务简单、时间精度要求不高、不需考虑分布式的场景

方案2:时间轮

关于时间轮的介绍可以看:【时间轮】TimeWheel原理:实现高效任务管理-CSDN博客

使用时间轮就是将所有 订单到期检查任务 分配到一个时间轮中,时间轮按照固定的时间间隔进行周期性旋转。当时间轮旋转到某个槽位时,触发该槽中对应的任务。

这种方案比定时任务性能高一些。

问题:

  • 内存占用:时间轮需要为每个槽位维护一个任务队列,当任务量很大时,可能会占用较多内存
  • 分布式问题:容易出现单点故障,需要设计分布式时间轮,增加主备模式、负载均衡算法,设计复杂
  • 高并发场景:大量订单同时到期,导致时间轮需要在短时间内处理大量任务,引发性能瓶颈

方案3:延迟队列

利用消息队列组件的延迟队列特性实现,如以下几种消息队列组件:

  • rabbitmq:使用死信队列 或 使用插件
  • rocketmq:原生支持延迟队列,但只能设置固定级别的延迟时间
  • kafka:生产者将到期时间放入消息,消费者消费时检查是否到期,未到期则放回队列
  • redis stream:生产者将到期时间放入消息,消费者消费时检查是否到期,未到期则放回队列

问题:

  • 需要依赖消息队列组件,并且可能需要安装对应的插件
  • 高并发场景下,可能出现消息积压
  • 放回队列时,会出现消息顺序性问题,可能需要手动管理
  • 延迟精度问题:精度依赖于 mq 的调度机制,可能存在延迟偏差

方案4:Redis 过期监听

将待关闭的订单信息存入 redis 并设置过期时间,开启 redis 的 notify-keyspace-events: Ex 配置,监听 redis key 的过期事件,接收到事件通知时关闭订单。

问题:

  • 不能监听指定 key,只能监听所有 key,收到事件后再自己筛选
  • 需要修改 redis 配置
  • redis 不保证 key 在过期时被立即删除,更不保证这个消息被立即发出,会有消息延迟

测试代码:

func Test_RedisSubscribe(t *testing.T) {
     _, err := redisCli.ConfigSet(context.Background(), "notify-keyspace-events", "Ex").Result()
     if err != nil {
         t.Fatal(err)
     }

     keyPrefix := "test:pending:"
     go func() {
         for i := 0; i < 3; i++ {
             key := keyPrefix + uuid.New()
             expire := time.Second * time.Duration(i+1)
             redisCli.Set(context.Background(), key, "test", expire)
             time.Sleep(time.Millisecond * 500)
             t.Logf("set key %+v", key)
         }
     }()

    # 0表示使用redis db 0
     pattern := "__keyevent@0__:expired"
     pubsub := redisCli.PSubscribe(context.Background(), pattern)
     defer pubsub.Close()

     var result int
     go func() {
         t.Logf("start receive %+v", pattern)
         pubsub.Channel()
         for msg := range pubsub.Channel() {
             if !strings.HasPrefix(msg.Payload, keyPrefix) {
                 continue
             }
             t.Logf("got msg. channel: %+v, payload-->%+v", msg.Channel, msg.Payload)
             result++
         }
     }()

     time.Sleep(time.Second * 6)
     assert.Equal(t, result, 3)
}

方案5:Redis ZSET 有序集合

将订单 id 作为 member,过期时间设置为 score, redis 会对 zset 自动按照 score 排序。再开启一个 redis 定时任务扫描,查询 score <= 当前时间 的条目,取出订单号进行关单操作。

问题:

  • 高并发下,可能多个节点获取到同一个订单号,可以加分布式锁解决

测试代码:

func Test_RedisZSet(t *testing.T) {
     key := "test:pending"
     for i := 0; i < 5; i++ {
         id := uuid.New()
         expireT := time.Now().Add(time.Second * time.Duration(i+1))
         redisCli.ZAdd(context.Background(), key, redis2.Z{
             Member: id,
             Score:  float64(expireT.Unix()),
         })
         t.Logf("set key %+v %+v", key, id)
     }

     for {
         maxScore := fmt.Sprintf("%+v", time.Now().Unix())
         result := redisCli.ZRangeByScore(context.Background(), key, &redis2.ZRangeBy{Min: "0", Max: maxScore}).Val()
         t.Logf("got pending: %+v", result)
         if len(result) == 0 {
             break
         }

         // 处理业务逻辑
         //

         // 删除ZSet中的元素
         redisCli.ZRemRangeByScore(context.Background(), key, "0", maxScore)

         // 获取下一个时间 (即ZSet的第一个元素)
         members := redisCli.ZRangeWithScores(context.Background(), key, 0, 0).Val()
         if len(members) == 0 {
             t.Logf("no more pending, waiting...")
             <-time.After(time.Second * 3)
             continue
         }

         // 等待到达下一个删除时间
         nextT := int64(members[0].Score)
         nextV := members[0].Member.(string)
         wait := nextT - time.Now().Unix()
         if wait < 0 {
             wait = 0
         }
         t.Logf("almost reach next time, waiting...  nextT: %+v, nextV: %+v", time.Unix(nextT, 0), nextV)
         <-time.After(time.Second * time.Duration(wait))
     }
}

当然,上述代码自己写起来有些复杂,可以直接使用第三方库。

这个写得不错:github.com/HDT3213/delayqueue  一个基于 redis ZSET 实现的延时队列

  


http://www.kler.cn/a/544238.html

相关文章:

  • <tauri><rust><GUI>使用tauri创建一个图片浏览器(文件夹遍历、图片切换)
  • Django创建超管用户
  • 网络安全-攻击路径
  • 16.React学习笔记.React更新机制
  • mongoTemplate获取某列最大值
  • π 的奥秘:如何用有理数逼近无理数?
  • 如何在Linux中设置定时任务(cron)
  • 使用 React 16+Webpack 和 pdfjs-dist 或 react-pdf 实现 PDF 文件显示、定位和高亮
  • 【C#零基础从入门到精通】(十一)——C#Reandom随机类详解
  • 1.攻防世界 unserialize3(wakeup()魔术方法、反序列化工作原理)
  • 2、k8s 二进制安装(详细)
  • 《qt open3d中添加随机点采样》
  • ubuntu部署snmp
  • 深入解析:如何利用期货Level2高频Tick数据洞察市场动态
  • 变形的宽搜 育才官网 HN036 涂色游戏
  • Windows中使用Docker安装Anythingllm,基于deepseek构建自己的本地知识库问答大模型,可局域网内多用户访问、离线运行
  • JVM 类加载子系统在干什么?
  • 后端程序如何应对流量激增
  • 5、《Spring Boot自动配置黑魔法:原理深度剖析》
  • react中如何获取真实的dom
  • 【Java八股文】01-Java基础面试篇
  • C++ 设计模式-抽象工厂
  • Android的Activity生命周期知识点总结,详情
  • 【uniapp-小程序】实现方法调用的全局tips弹窗
  • 在fedora41中安装钉钉dingtalk_7.6.25.4122001_amd64
  • 2025有哪些关键词优化工具好用