-
用到的工具以及开发语言
- redis
- golang
-
背景
- redis实现的简单延迟队列,这个场景在工具丰富的公司的话一般使用mq代替了,但是说公司开发选型上没有搭建这些工具,需要实现的延迟队列并不复杂和数量量不大的情况下可以考虑这个实现方式
-
参考文章
-
实现思路
-
-
job pool是一个string类型的k/v形式,我们只需要把对应的key,value保存到redis中,value中包含了你想要存放的数据,这样我们在消费队列的时候可以获取到需要的信息,并且执行我们想要的操作
-
delay bucket可以理解成一个不同主题(topic)的存储桶,我们用time.tick去定时查询这个库有没有过期的数据,有的话我们就拿出来放到ready queue中。
-
delay bucket的形式这里定义成:
score: 需要过期的时间戳,
member: job id(可以通过这个找到job pool中的对应数据)
-
timer每次扫描就用当前的时间戳去对比:
- zrange key min max withscores
-
扫描出来的数据我们进行ready queue队列中,这里我们使用的是list数据类型,按照我们插入的顺序排序嘛,因为都是过期的数据,所以就没有按照过期时间排序,每次消费的时候我们从头部获取一个消费
-
这里我们就要用到线程池就消费,channel通道就非常适合干这个
-
-
具体的代码
-
redis操作部分:
-
package test import ( "context" "encoding/json" "fmt" redis2 "github.com/go-redis/redis/v8" "github.com/google/uuid" "strconv" "sync" "time" "vue-chat/redis" ) var ( JobPoolKey = "job_pool_key_" BaseDelayBucketKey = "base_delay_bucket" BaseReadyQueueKey = "base_ready_queue" delayAddTime = 1 ) type RedisJobData struct { Topic string ID string Delay int TTR int Body *BodyContent } type BodyContent struct { OrderID int OrderName string } //组装数据 func (d *RedisJobData)SetJobPool(number int,ctx context.Context) bool { redisCoon := redis.GetRedisDb() //r := rand.New(rand.NewSource(time.Now().Unix())) for i := 0; i < number; i++ { d.Topic = "order_queue" d.ID = uuid.NewString() d.Delay = 1 d.TTR = 3 d.Body = &BodyContent{ OrderID: i, OrderName: "order_name_"+strconv.Itoa(i), } key := JobPoolKey + strconv.Itoa(i) delayKey := strconv.Itoa(i) data, _ := json.Marshal(d) //写入job pool _ ,err := redisCoon.Set(ctx, key, data,0 * time.Second).Result() if err != nil { fmt.Println("添加失败: ", err) return false } fmt.Println("添加成功: ", err) nowTime := time.Now().Unix() //delayTime := int(nowTime) + r.Intn(delayAddTime) delayTime := int(nowTime) + delayAddTime //写入delay queue redisCoon.ZAdd(ctx, BaseDelayBucketKey,&redis2.Z{ Score: float64(delayTime), Member: delayKey, }) //为了可以更好的演示,这每个过期时间增加几秒,防止一次性消费了 if i % 10 == 0 && i > 0 { delayAddTime += 10 } } return true } //定时timer.tick查询bucket中是否有过期的数据,如果有放入消费队列中 func TimerDelayBucket(redisCoon *redis2.Client,ctx context.Context,pool *Pool, wg *sync.WaitGroup) error { nowTime := time.Now().Unix() result, err := redisCoon.ZRangeByScoreWithScores(ctx,BaseDelayBucketKey,&redis2.ZRangeBy{ Min: "-inf", Max: strconv.FormatInt(nowTime,10), }).Result() if err == nil { for _, z := range result { //进入ready queue redisCoon.LPush(ctx, BaseReadyQueueKey, z.Member) //写入通道说明有数据了,可以进行消费 err := pool.Put(&Task{ Member: z.Member.(string), Wg: wg, }) if err != nil { fmt.Println(err) } wg.Add(1) } } return err } //消费队列 func ConsumeQueue(redisCoon *redis2.Client, ctx context.Context, wg *sync.WaitGroup) error { defer wg.Done() //先判断list中是否有数据,有数据才有必要执行,没有数据直接返回就好了 lenQueue, err:= redisCoon.LLen(ctx,BaseReadyQueueKey).Result() if err != nil { return err } if lenQueue == 0 { return nil } result, err := redisCoon.LPop(ctx, BaseReadyQueueKey).Result() if err != nil { return err } fmt.Println("我消费了一个数据:", result) //这里可以实现需要的操作,这里简单实现了删除操作 redisCoon.Del(ctx, JobPoolKey+result) redisCoon.ZRem(ctx, BaseDelayBucketKey, result) return nil }
-
-
线程池部分:
-
package test import ( "context" "errors" "fmt" "log" "sync" "sync/atomic" "time" "vue-chat/redis" ) var ( // return if pool size ErrInvalidPoolCap = errors.New("invalid pool cap") // put task but pool already closed ErrPoolAlreadyClosed = errors.New("pool already closed") ) // running status const ( RUNNING = 1 STOPED = 0 ) //Task task to-do type Task struct { //Handler func(v ...interface{}) //Params []interface{} Member string Wg *sync.WaitGroup } //Pool task pool type Pool struct { capacity uint64 runningWorkers uint64 status int64 chTask chan *Task PanicHandler func(interface{}) sync.Mutex } // NewPool init pool func NewPool(capacity uint64) (*Pool, error) { if capacity <= 0 { return nil, ErrInvalidPoolCap } p := &Pool{ capacity: capacity, status: RUNNING, chTask: make(chan *Task, capacity), } return p, nil } func (p *Pool) checkWorker() { p.Lock() defer p.Unlock() if p.runningWorkers == 0 && len(p.chTask) > 0 { p.run() } } //GetCap get capacity func (p *Pool) GetCap() uint64 { return p.capacity } //GetRunningWorkers get running workers func (p *Pool) GetRunningWorkers() uint64 { return atomic.LoadUint64(&p.runningWorkers) } func (p *Pool) incRunning() { atomic.AddUint64(&p.runningWorkers,1) } func (p *Pool) decRunning() { atomic.AddUint64(&p.runningWorkers, ^uint64(0)) } //Put put a task to pool func (p *Pool) Put(task *Task) error { p.Lock() defer p.Unlock() if p.status == STOPED { return ErrPoolAlreadyClosed } //run workers if p.GetRunningWorkers() < p.GetCap() { p.run() } //send task if p.status == RUNNING { p.chTask <- task } return nil } func (p *Pool) run() { p.incRunning() redisConn := redis.GetRedisDb() conn := context.Background() go func() { defer func() { p.decRunning() if r := recover(); r != nil { if p.PanicHandler != nil { p.PanicHandler(r) }else { log.Printf("Worker panic: %s\n", r) } } p.checkWorker() //check worker avoid no worker running }() for{ select { case task, ok := <-p.chTask: if !ok { return } //task.Handler(task.Params...) fmt.Println(task.Member) err := ConsumeQueue(redisConn,conn,task.Wg) if err != nil { fmt.Println("消费队列发生错误:", err) return } } } }() } func (p *Pool) setStatus(status int64) bool { p.Lock() defer p.Unlock() if p.status == status { return false } p.status = status return true } //Close close pool graceful func (p *Pool) Close() { if !p.setStatus(STOPED) { //stop put task return } for len(p.chTask) > 0 { //wait all task be consumed time.Sleep(1e6) //reduce cpu load } close(p.chTask) }
-
-
测试:
-
package test func TestConsuming(t *testing.T) { var data RedisJobData redisConn := redis.GetRedisDb() conn := context.Background() data.SetJobPool(1000, conn) pool, err := NewPool(20) if err != nil { panic(err) } wg := new(sync.WaitGroup) c := time.Tick(1 * time.Second) for next := range c { fmt.Println("我在执行了") err := TimerDelayBucket(redisConn, conn, pool, wg) if err != nil { fmt.Println("定时timer发生错误:", next, err) } } wg.Wait() pool.Close() }
-
-
redis连接部分的话自己实现就好了哈
-
package redis import ( "context" "github.com/go-redis/redis/v8" "github.com/sirupsen/logrus" "time" "vue-chat/config" ) var rdb *redis.Client func init() { addr := "you redis addr" port := "you redis port" password := "you redis auth password" rdb = redis.NewClient(&redis.Options{ Addr: addr + ":" + port, Password: password, DB: 0, PoolSize: 100, }) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := rdb.Ping(ctx).Result() if err != nil { logrus.Error("redis ping err:", err) panic("redis连接失败:"+err.Error()) } } func GetRedisDb() *redis.Client { return rdb }
-
-
go + redis 实现简单的延迟队列
于 2022-02-18 23:57:09 首次发布