个人学习笔记分享
由于网上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)
}
模拟消费逻辑
这里演示业务如果报错,如何让消费者重试的一种优雅方案
- 定义一个最大重新消费次数
- 获取消息目前的重新消费次数,是否已经超出最大次数
- 超出最大次数,则说明消息可能无法处理,将消息保存并通知人工处理,然后签收消息
- 如果没有超出,则返回
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.定时对表中数据状态进行检查、补发