[RocketMQ] Golang中操作RocketMQ

个人学习笔记分享
由于网上RocketMQ关于go语言实现的文章较少,分享个人学习笔记


添加依赖

go get github.com/apache/rocketmq-client-go/v2

创建消费者

笔记

  • 消费模式:
    1. Clustering 集群模式(负载均衡模式):队列会被消费者分摊,队列数量>=消费者数量,mq服务器会记录处理消消费位点
    2. Broadcasting 广播模式:消息会被每一个消费者处理一次,mq服务器不会记录消费位点,也不会重试
  • 重新消费次数:
    1. 重新消费次数,默认重试16次,超过次数的消息会被放到死信队列 %DLQ%TopicName
    2. 重试时间间隔:10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

代码

func StartConcurrentConsumer(groupName string, instanceName, topicName string) {
	// 创建消费者
	con, err := rocketmq.NewPushConsumer(
		// 消费模式
		consumer.WithConsumerModel(consumer.Clustering),
		consumer.WithGroupName(groupName),
		// RocketMQ的端口,如127.0.0.1:9876
		consumer.WithNameServer([]string{consts.RocketMQAddr}),
		// 配置访问密钥
		consumer.WithCredentials(primitive.Credentials{
			AccessKey: "rocketmq2",
			SecretKey: "12345678",
		}),
		// 从Broker到Consumer的重复发送次数
		consumer.WithRetry(2),
		// Consumer对消息的重新消费次数
		consumer.WithMaxReconsumeTimes(2),
		// 如要在一个消费者组中创建多个消费者,需要指定示例名
		consumer.WithInstance(instanceName),
	)
	if err != nil {
		fmt.Println("创建消费者失败:", err)
		return
	}
	defer func() {
		err = con.Shutdown()
		if err != nil {
			fmt.Println("关闭消费者失败:", err)
			return
		}
	}()
	// 订阅主题
	err = con.Subscribe(topicName, consumer.MessageSelector{}, onMessage)
	if err != nil {
		fmt.Println("订阅主题失败:", err)
		return
	}
	// 开启消费
	err = con.Start()
	if err != nil {
		fmt.Println("启动消费者失败:", err)
		return
	}
	time.Sleep(time.Hour)
}

模拟消费逻辑

这里演示业务如果报错,如何让消费者重试的一种优雅方案

  1. 定义一个最大重新消费次数
  2. 获取消息目前的重新消费次数,是否已经超出最大次数
  3. 超出最大次数,则说明消息可能无法处理,将消息保存并通知人工处理,然后签收消息
  4. 如果没有超出,则返回consumer.ConsumeRetryLater,让消费者稍后重新消费
  • 这样的好处是不用额外创建消费者管理死信队列,节省资源

代码


func onMessage(ctx context.Context, msg ...*primitive.MessageExt) (res consumer.ConsumeResult, err error) {
	defer func(reconsumeTimes int32) {
		rec := recover()
		if rec != nil {
			fmt.Println("检测到业务报错", rec)
			// 重试次数大于等于设定的值
			if reconsumeTimes >= maxReconsumeTimes {
				fmt.Println("业务出现问题,已保存到指定位置,请通知人工处理")
				res = consumer.ConsumeSuccess
			} else {
				res = consumer.ConsumeRetryLater
			}
		}
	}(msg[0].ReconsumeTimes)

	// 伪代码:
	// 添加消息的Key到去重表
	// err = insert(key)
	// if err != nil: return success
	// 开始处理消息
	// handleMessage()

	// msg虽然是数组,但长度都是1
	fmt.Printf("订阅消息:%v \n", msg[0])
	// 模拟业务报错
	panic("业务报错")
	return consumer.ConsumeSuccess, nil
}


开启消费者

StartOrderlyConsumer()自行编写,与StartConcurrentConsumer()相同,只是多加了一行单线程设置

单线程消费者需要在创建时添加consumer.WithConsumeGoroutineNums(1)

代码

go StartConcurrentConsumer("group_concurrent", "consumer1", "topic_concurrent")
go StartOrderlyConsumer("group_orderly", "consumer1", "topic_orderly")

创建生产者并发送消息

笔记

  • 消息类型:
    1.同步消息:生产者发送消息之后等待broker返回发送结果
    2.异步消息:生产者发送消息后不等待结果,让参数内的函数处理返回结果
    3.单向消息:broker不返回结果,性能高
    4.顺序消息:生产者将需要顺序处理的消息放在同一个队列中,注意需要由单线程的消费者来消费消息

代码

func SendMessage() (err error){
	// 创建生产者
	pro, err := rocketmq.NewProducer(
		producer.WithGroupName("pg"),
		// RocketMQ的端口,如127.0.0.1:9876
		producer.WithNameServer([]string{consts.RocketMQAddr}),
		// 配置访问密钥
		producer.WithCredentials(primitive.Credentials{
			AccessKey: "rocketmq2",
			SecretKey: "12345678",
		}),
		producer.WithRetry(2),
		//指定队列选择器
		//hashQueueSelector如果消息具有分片密钥ShardingKey,则按hash选择队列,否则按随机选择队列
		producer.WithQueueSelector(producer.NewHashQueueSelector()),
	)
	if err != nil {
		fmt.Println("创建生产者失败:", err)
		return
	}
	defer func() {
		err = pro.Shutdown()
		if err != nil {
			fmt.Println("关闭生产者失败:", err)
			return
		}
	}()
	err = pro.Start()
	if err != nil {
		fmt.Println("启动生产者失败:", err)
		return
	}
	// 发送消息
	msg := primitive.NewMessage("topic_concurrent", []byte("测试发送"))
	// 1.同步消息
	res, err := pro.SendSync(context.Background(), msg)
	fmt.Println(res)
	if err != nil {
		fmt.Println("发送消息失败:", err)
		return
	}
	// 2.异步消息
	//err = pro.SendAsync(context.Background(), func(ctx context.Context, result *primitive.SendResult, err error) {
	//	fmt.Println(result.String())
	//}, msg)
	//if err != nil {
	//	fmt.Println("发送消息失败:", err)
	//	return
	//}
	// 3.单向消息
	//err = pro.SendOneWay(context.Background(), msg)
	//if err != nil {
	//	fmt.Println("发送消息失败:", err)
	//	return
	//}
	// 4.顺序消息(消费者要设置为单线程模式)
	wg := &sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			m := primitive.NewMessage("topic_orderly", []byte(fmt.Sprintf("第一组第%d个消息", i)))
			// 为消息设置分片秘钥,保证同一个ShardingKey的消息进入同一个queue
			m.WithShardingKey("123")
			pro.SendSync(context.Background(), m)
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			m := primitive.NewMessage("topic_orderly", []byte(fmt.Sprintf("第二组第%d个消息", i)))
			// 为消息设置分片秘钥,保证同一个ShardingKey的消息进入同一个queue
			m.WithShardingKey("456")
			pro.SendSync(context.Background(), m)
		}
	}()

	wg.Wait()
	return
}

常见问题和解决方案

同一个消费者组中不要同时存在并发和单线程的消费者,否则会出现问题

Topic和Tag的选择问题:是则选Tag,否则选Topic

1.消息类型(如普通消息、事务消息、延迟消息、顺序消息)是否一致
2.业务是否相关联
3.消息优先级是否一致
4.消息量级是否相当
*同一个消费者组中订阅的Topic和Tag要一致

重复消费解决方案

1.设计一个去重表,对消息的key添加唯一索引
2.每次消费前先插入数据库,如果成功插入,则执行业务逻辑;插入失败说明消息来过了,直接签收
3.业务报错则从去重表删除记录

死信消息解决方案

1.在业务逻辑中捕获异常
2.判断此时是否已经超出最大重复消费次数,是则保存信息(mysql等)并直接签收,通知人工处理;否则重新消费

消息堆积解决方案

1.生产过快问题:生产方业务限流;增加消费者数量(<=队列数量);动态扩容队列数量
2.消费者出现问题:排查消费者程序的问题

消息丢失解决方案

1.设计一个发送表,发送者发送消息后,在表中增加状态索引,表示是否已处理
2.消费者成功消费后,将表中状态改为已处理
3.定时对表中数据状态进行检查、补发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值