订单自动关闭本质上是一类延时任务如何处理的问题,具体的场景可能有:
建立一个 cronjob 每隔一段时间扫一次表,查询所有到期的订单,执行关单操作。
问题:
这种方案适用于业务简单、时间精度要求不高、不需考虑分布式的场景
关于时间轮的介绍可以看:【时间轮】TimeWheel原理:实现高效任务管理-CSDN博客
使用时间轮就是将所有 订单到期检查任务 分配到一个时间轮中,时间轮按照固定的时间间隔进行周期性旋转。当时间轮旋转到某个槽位时,触发该槽中对应的任务。
这种方案比定时任务性能高一些。
问题:
利用消息队列组件的延迟队列特性实现,如以下几种消息队列组件:
redis stream:生产者将到期时间放入消息,消费者消费时检查是否到期,未到期则放回队列
问题:
将待关闭的订单信息存入 redis 并设置过期时间,开启 redis 的 notify-keyspace-events: Ex 配置,监听 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)
}
将订单 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 实现的延时队列