go延迟消息队列

go-delay-queue

github项目位置

基于消息组件实现延迟队列

延迟队列其实是与时间相关的队列,普通队列只要队列有数据,客户端消费就能拿到这个数据,而延迟队列内的数据是与时间相关的,绑定了一个过期时间,只有到了过期时间,客户端才能消费到这个数据。

应用场景:

  1. 定时任务,比如任务A和任务B是同条流水线上的,当任务A完成了,一个小时后执行任务B
  2. 重试业务,比如业务A需要调用其它服务,而服务出现问题,这时候就需要做业务重试

实现方式:

  1. 基于消息的延迟:指为每条消息设置不同的延迟时间,那么每当队列中有新消息进入的时候就会重新根据延迟时间排序,或者定义时间轮,新消息落在指定位置;
  2. 基于队列的延迟: 设置不同延迟级别的队列,比如5s、1min、30mins、1h等,每个队列中消息的延迟时间都是相同的。

基于第一种不少组件都有实现方案,比如redis的sortset间接实现,kafka内部时间轮,rmq可安装插件实现。第一种实时性高,不过主观看会比较依赖组件本身,但自己实现就得考虑持久化、高可用等问题,建议直接使用组件本身;第二种方案可以基于组件去实现,通用性会高点,不过实时性不高,更适合用于重试业务场景。

本文基于第二种方案使用go去实现
先看简单的实现方式
在这里插入图片描述

  1. 生产者将消息格式化后按延迟等级发布到不同生产队列
  2. 延迟服务订阅所有队列,拉出消息,判断是否到期
  3. 到期后投递到不同消费队列
  4. 消费者订阅所有消费队列,拉出消息,进行对应到业务逻辑
  5. 业务操作失败,需重试,格式化数据投递到高等级队列

问题:

  1. 客户端要自己实现订阅不同队列组件(redis/ kafka/ rmq/ mysql);
  2. 队列数量多;
  3. 客户端要自己格式化消息以符合延迟服务数据标准;
  4. 失败重试需要自己开发往哪个等级发布

优化方案:
在这里插入图片描述

  1. 对服务进行抽象,对外提供生产和消费消息接口;
  2. 客户端不需要自己订阅队列,队列数减少一半;
  3. 服务内部对数据进行格式化流转;
  4. 服务通过回调对失败消息往高等级队列投递

代码设计思路

github项目位置

  1. 从上图看出,有四个主体:生产者、消费者、服务提供方、消息组件。先看消息组件,该方案基于组件实现,需要能够接入各个组件,那么可以就定义接口由各个组件库实现,有两个方法,也是消息队列组件最基本的功能,订阅、生产消息。
type Iqueue interface {
    // 订阅消息
    // topic 为队列名,dealFn为消息处理逻辑方法,[]byte为队列消息,当err发生则放回消息,默认为程序退出
	SubscribeMsg(topic string, dealFn func([]byte) (err error)) (err error)
	// 生产消息
	PublishMsg(topic string, msg []byte) (err error)
}

以redis实现为例 (其他实现:kafka

func (dr *Dredis) SubscribeMsg(topic string, dealFn func([]byte)(err error)) (err error){
	go func() {
		var tk = time.NewTicker(5 * time.Second)
		for ;; {
			// 获取队列消息
			if msg,err := dr.conn.RPop(topic).Bytes(); err == nil {
				err = dealFn(msg)
				if err != nil {
					// 程序退出,消息重新放回
					err = dr.conn.RPush(topic, msg).Err()
					if err != nil {
						// log
						log.Print("消息重新放回失败:", err)
					}
				}
			} else {
				if err != redis.Nil {
					// log
					log.Print("消息出队失败:", err)
				}
				<-tk.C
			}
		}
	}()
	return 
}

func  (dr *Dredis) PublishMsg(topic string, msg []byte) (err error){
	err = dr.conn.LPush(topic, msg).Err()
	if err != nil {
		// log
		log.Print("消息入队失败:", err)
	}
	return 
}
  1. 生产者和消费者实质都为服务调用方,即客户端,那客户端和服务端之间的纽带就是消息,所以我们要构建消息队列,不同的消息队列代表了不同的延迟等级(1min,5min,1h等等)、处理逻辑等, 下面demo定义了三个等级(低中高)示例前往:
// 创建延迟消息
low := &go_delay_queue.DelayLevel{
	TopicName: "low", // 队列名
	Level:     0, // 延迟等级
	RetryNums: 1, // 重试次数,处理失败可重新放回该队列,超过次数就入到下一等级
	Ttl:       1 * time.Minute, // 消息延迟至少1min后可被使用
	DealFn: dealMsg, // 客户端消息处理逻辑方法
}
medium := &go_delay_queue.DelayLevel{
	TopicName: "medium",
	Level:     1,
	RetryNums: 1,
	Ttl:       5 * time.Minute,
	DealFn: dealMsg,
}
high := &go_delay_queue.DelayLevel{
	TopicName: "high",
	Level:     2,
	RetryNums: 1,
	Ttl:       1 * time.Hour,
	DealFn: dealMsg,
}

func dealMsg(dtm go_delay_queue.DelayTopicMsg) (err error) {
	fmt.Printf("开始处理消息:%#v\n", dtm)
	return
}
  1. 有了消息就可以把服务方和消息组件关联起来,延迟服务本身做的几个关键事
  • 把延迟消息队列名交给对应消息接口实现,等待延迟消息
go dr.queuer.SubscribeMsg(item.TopicName, dr.dealMsg)
  • 计算延迟消息过期时长,生效则将延迟消息传递给客户端处理
// 判断时间是否达到指定时间
var ttl = stru.ExpiredAt - dr.now
if dr.Debug {
	fmt.Println(fmt.Sprintf("消息未到达指定时间,等待 %ds\n", ttl))
}
select {
	case <-time.After(time.Duration(ttl) * time.Second):
	case <-dr.clientCtx.Done():
		// 消息重回
		err = errors.New("reload")
		return err
	}
}
  • 根据客户端处理结果,成功则丢弃消息,失败则重入消息
// 可以开始消费
go func(delayMsg *DelayTopicMsg) {
    err := dr.levelTopicMap[delayMsg.Level].DealFn(*delayMsg)
    if dr.Debug {
        fmt.Println("交付客户端处理:", err)
    }
    if err != nil {
        // 入下一等级消息
        dr.inQueue(true, delayMsg)
    }
}(stru)
go-zero提供了一个官方开发的消息队列组件,称为go-queue。它包含两种类型的队列,一种是基于Kafka的消息队列(kq),另一种是基于Beanstalkd的延迟队列(dq)。但是需要注意的是,go-queue不支持定时任务。如果你想了解更多关于go-queue的信息,可以查看官方文档或者参考之前写过的教程。\[3\]另外,除了go-queue,当前使用较多的消息队列还包括RabbitMQ、RocketMQ、Kafka等。此外,一些数据库如Redis、MySQL和phxsql也可以实现消息队列的功能。\[2\]所以,根据你的需求和项目的具体情况,你可以选择合适的消息队列来使用。 #### 引用[.reference_title] - *1* *3* [go-zero微服务到k8s部署应有尽有系列(八)消息、延迟、定时队列](https://blog.csdn.net/jj546630576/article/details/122979672)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [消息队列及常见消息队列介绍](https://blog.csdn.net/weixin_53370274/article/details/118997765)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值