基于redis实现的延时队列

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/idwtwt/article/details/88309737

1 需求背景

  • 用户抢单成功之后,如果一定时间后没有完成任务,任务自动取消
  • 用户提交任务审核后,如果商家一定时间后没有审核,任务自动通过

类似的场景比较多 简单的处理方式就是使用定时任务 假如数据比较多的时候 有的数据可能延迟比较严重,而且越来越多的定时业务导致任务调度很繁琐不好管理。

2 技术支撑

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。

示例

redis 127.0.0.1:6379> ZADD runoobkey 1 redis
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 2 mongodb
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql
(integer) 0
redis 127.0.0.1:6379> ZADD runoobkey 4 mysql
(integer) 0
redis 127.0.0.1:6379> ZRANGE runoobkey 0 10 WITHSCORES

1) "redis"
2) "1"
3) "mongodb"
4) "2"
5) "mysql"
6) "4"

3 队列设计

3.1 任务结构

// Job 使用msgpack序列化后保存到Redis,减少内存占用
type Job struct {
	Topic string `json:"topic" msgpack:"1"`	//主题(任务类型)
	No    string `json:"no" msgpack:"2"`    // job唯一标识ID
	Delay int64  `json:"delay" msgpack:"3"` // 执行时间, unix时间戳
	TTR   int64  `json:"ttr" msgpack:"4"`	//超时时间
	XXX  string `json:"xxx" msgpack:"5"`	//其他附加字段
}

Topic:主题,每个任务所属类型,不通类型任务可以分开调度

No :主题标识,标识唯一

Delay:执行时间戳

TTR:超时时间,执行时间已过,再进过TTR时间,任务将被丢弃

XXX:根据需要添加其他字段

扫描二维码关注公众号,回复: 5535854 查看本文章

3.2 任务桶Bucket(redis有序集合)

根据定时任务量的大小,设置Bucket的数量,Bucket的数量指定同时轮询的队列数。添加的任务将平均放置到不同的任务桶中。

3.3 就绪队列(list)

达到执行时间的任务,将从任务桶中取出,根据主题分类,放置到就绪队列中。

3.4 持久化

需要借助数据库等额外实现持久化

3.5 运行

1 初始化:根据配置连接redis服务,初始化相应数量的任务桶和轮询协程

2 添加任务:初始化任务结构,经过msgpack序列化后,添加到redis集合和其中一个任务桶中

3 轮询队列:各个任务桶的轮询协程轮询时间戳最小的任务是否达到执行时间,如果达到,则取出放到就绪队列。

4 执行任务:从就绪队列中取出任务,检查任务是否被删除,如果被删除,则直接返回;否则返回任务给执行体执行。

5 删除任务:执行完任务,删除redis集合中的任务;不想任务执行,直接提前删除任务。

4 代码实现(包含商品和订单任务例子)

延时队列初始化

package delayqueue

import (
	"errors"
	"fmt"
	"github.com/astaxie/beego"
	"ranbbService/models"
	"time"
	"github.com/astaxie/beego/orm"
	"ranbbService/util/cache"
)

var (
	// 每个定时器对应一个bucket
	timers []*time.Ticker
	// bucket名称chan
	bucketNameChan <-chan string

	session_redis *cache.RedisCache
)


// Init 初始化延时队列
func InitDelayQueue() error{
    var err error

	session_redis, err = cache.NewRedisCacheFromCfg("session_redis")
	if err!=nil{
		beego.Error("InitDelayQueue,err=",err)
		return err
	}
	//初始化配置,如果配置文件中没有,则使用默认配置
	Setting = &Config{}
	Setting.configParse()

	RedisPool = initRedisPool()

    //初始化各个任务桶的轮询定时器
	initTimers()
	bucketNameChan = generateBucketName()


	//初始化时添加Jobs
	err = InitJobsTable()
	if err!=nil{
		beego.Error("InitDelayQueue,err=",err)
		return err
	}


	//处理任务定时
	go handleGoods()
	//处理订单定时
	go handleOrders()

	return nil
}
func InitJobsTable() error {

	//处理任务
	goods,count,err := models.DelayQueueGoods(0)
	if err !=nil{
		beego.Error("AddJobsTable,err=",err)
		return err
	}
	beego.Debug("Goods Jobs counts:",count)
	for _,goods:=range goods{
		job := Job{
			Topic:"GOODS",
			No:goods.No,
			Delay:goods.ExecTime,
			TTR:10,//十秒超时
			UUID:goods.UUID,
		}
		beego.Debug("Push goods:",job.No)
		err = Push(job)
		if err !=nil{
			beego.Error("AddJobsTable,err=",err)
			return err
		}
	}

	//处理订单
	orders,count,err := models.DelayQueueOrders(0)
	if err !=nil{
		beego.Error("AddJobsTable,err=",err)
		return err
	}
	beego.Debug("Orders Jobs counts:",count)
	for _,order:=range orders{
		job := Job{
			Topic:"ORDERS",
			No:order.No,
			Delay:order.ExecTime,
			TTR:10,//十秒超时
			UUID:order.UUID,
		}
		beego.Debug("Push orders:",job.No)
		err = Push(job)
		if err !=nil{
			beego.Error("AddJobsTable,err=",err)
			return err
		}
	}

	return nil
}

添加任务:

// Push 添加一个Job到队列中
func Push(job Job) error {
	if job.No == "" || job.Topic == "" || job.Delay < 0 || job.TTR <= 0 {
		return errors.New("invalid job")
	}
	//将任务添加到任务池(redis集合)
	err := putJob(job.No, job)
	if err != nil {
		beego.Error("添加job到job pool失败#job-%+v#%s", job, err.Error())
		return err
	}
	
	//将任务添加到任务桶(redis有序集合)
	err = pushToBucket(<-bucketNameChan, job.Delay, job.No)
	if err != nil {
		beego.Error("添加job到bucket失败#job-%+v#%s", job, err.Error())
		return err
	}

	return nil
}

轮询任务桶,将达到执行时间的任务放到就绪队列中


// 初始化定时器
func initTimers() {
	timers = make([]*time.Ticker, Setting.BucketSize)
	var bucketName string
	for i := 0; i < Setting.BucketSize; i++ {
		timers[i] = time.NewTicker(1 * time.Second)
		bucketName = fmt.Sprintf(Setting.BucketName, i+1)
		beego.Debug("Init delay queue bucket:",bucketName)
		go waitTicker(timers[i], bucketName)
	}
}

func waitTicker(timer *time.Ticker, bucketName string) {
	for {
		select {
		case t := <-timer.C:
			tickHandler(t, bucketName)
		}
	}
}

// 扫描bucket, 取出延迟时间小于当前时间的Job
func tickHandler(t time.Time, bucketName string) {
	for {
		bucketItem, err := getFromBucket(bucketName)
		if err != nil {
			beego.Error("扫描bucket错误#bucket-%s#%s", bucketName, err.Error())
			return
		}

		// 集合为空
		if bucketItem == nil {
			//beego.Debug("Got bucketItem is nil")
			return
		}

		// 延迟时间未到
		if bucketItem.timestamp > t.Unix() {
			if (bucketItem.timestamp - t.Unix()) < 10{
				beego.Debug("Not the time for executing job:",bucketItem.jobNo,"need time:",bucketItem.timestamp - t.Unix(),"s")
			}
			return
		}

		// 延迟时间小于等于当前时间, 取出Job元信息并放入ready queue
		job, err := getJob(bucketItem.jobNo)
		if err != nil {
			beego.Error("获取Job元信息失败#bucket-%s#%s", bucketName, err.Error())
			continue
		}

		// job元信息不存在, 从bucket中删除
		if job == nil {
			removeFromBucket(bucketName, bucketItem.jobNo)
			continue
		}

		// 再次确认元信息中delay是否小于等于当前时间
		if job.Delay > t.Unix() {
			// 从bucket中删除旧的jobNo
			removeFromBucket(bucketName, bucketItem.jobNo)


			// 重新计算delay时间并放入bucket中
			pushToBucket(<-bucketNameChan, job.Delay, bucketItem.jobNo)
			continue
		}


		//beego.Debug("pushToReadyQueue:",job.Topic,",No:",bucketItem.jobNo)
		err = pushToReadyQueue(job.Topic, bucketItem.jobNo)
		if err != nil {
			beego.Error("JobNo放入ready queue失败#bucket-%s#job-%+v#%s",
				bucketName, job, err.Error())
			continue
		}

		// 从bucket中删除
		removeFromBucket(bucketName, bucketItem.jobNo)
	}
}

完整代码:https://github.com/zzpu/go-delayqueue.git

猜你喜欢

转载自blog.csdn.net/idwtwt/article/details/88309737