订单自动关闭方案设计

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

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

方案1:定时任务扫表

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

问题:

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

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

方案2:时间轮

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

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

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

问题:

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

方案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 实现的延时队列

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值