背景
公司实际业务有两个动作,一个插入数据库的操作,一个修改数据库状态的操作,直接操作数据库,会导致数据库的压力太大,引发系统故障;所以需要在保存中间增加mq进行削峰,但是要求修改动作必须在插入之后才能执行,所以需要mq顺序消费
流程很简单:业务方发送消息到生产者—》发送到MQ----》消费者消费,保存到数据库
rocket mq 基础常识
在开始做这个事情前,需要先了解一些mq的基本常识
建议读一下这个文档来熟悉Topic、Tag、GroupName和queue的概念、设计初衷以及使用方法。
rocket mq 的消息类型
1、普通消息
2、事务消息
3、定时和延时消息
4、顺序消息:分区顺序消息和全局顺序消息
本次只介绍顺序消息:
全局顺序消息:
当发送和消费参与的Queue只有一个时所保证的有序是整个Topic中消息的顺序, 称为全局有序。
全局顺序消息实际上是一种特殊的分区顺序消息,即Topic中只有一个分区,因此全局顺序和分区顺序的实现原理相同。因为分区顺序消息有多个分区,所以分区顺序消息比全局顺序消息的并发度和性能更高。
适用场景
适用于性能要求不高,所有的消息严格按照FIFO原则来发布和消费的场景。
分区顺序消息:
当一个Topic存在多个Queue参与的时候,所有消息根据Sharding Key选择Queue,同一个Queue的消息按照严格的先进先出(FIFO)原则进行发布和消费。同一Queue的消息保证顺序,不同分区之间的消息顺序不做要求。
这种情况试用场景:适用于性能要求高,以Sharding Key作为队列选择字段,在同一个区块中严格地按照先进先出(FIFO)原则进行消息发布和消费的场景。
比如:电商的订单创建,以订单ID作为Sharding Key,那么同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息都会进入同一个Queue,按照发布的先后顺序来消费。
注意事项
对于全局顺序消息,建议消息不要有阻塞。无论运行多少实例,实际工作的只会有一个实例。所以对性能要求高的业务,建议还是走分区顺序消息,这个地方作者踩过坑,起了4个实例,但是消费能力一直上不去,才发现全局消息,只有一个消费者会工作
实现Queue的选择
无论是什么开发语言,都是在定于Producer时候,指定消息队列选择器,java是实现MessageQueueSelector接口定义的
在定义选择器的选择算法时,一般需要使用Sharding key。这个Sharding key可以是消息key也可以是其它数据。但无论谁做Sharding key,都不能重复,都是唯一的。比如订单号,或者消息的唯一ID
一般性的选择算法是,让Sharding key(或其hash值)与该Topic所包含的Queue的数量取模,其结果即为选择出的Queue的QueueId
java的实现阿里云给了sdk,网上也有很多,但是GO语言的sdk阿里云没有给,需要自己实现,下面给出简单的代码实现
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
"github.com/google/uuid"
"go.chanjet.com/chanjet/common/db"
"log"
"time"
)
var dbm db.DbWrap
func main() {
p, err := rocketmq.NewProducer(
producer.WithGroupName(uuid.New().String()),
producer.WithNameServer([]string{"rocketMqAddress"}),
producer.WithCredentials(primitive.Credentials{
AccessKey: "accessKey",
SecretKey: "accessSecret",
}),
producer.WithSendMsgTimeout(time.Second*60),
//指定队列选择器,表明使用要发送的队列就是msg中定义的queue
//hashQueueSelector如果消息具有分片密钥,则按hash选择队列,否则按随机选择队列
producer.WithQueueSelector(producer.NewHashQueueSelector()),
)
if err != nil {
fmt.Println("--生成 isv message producer 失败:", err)
panic("生成isv message producer 失败")
}
if err = p.Start(); err != nil {
fmt.Println("--启动 isv message producer 失败:", err)
panic("启动isv message producer 失败")
}
body, _ := json.Marshal("{\"status\":\"SEND_SUCCESS\",\"requestI\nd\":\"8d7d7bd0-7414-4f20-bd16-17c5ac71b8ce\",\"traceId\":\"315518776ef8a801\",\"appKey\":\"*******\",\"updatedAt\":\"0001-01-01T00:00:00Z\"}")
msg := primitive.NewMessage("TOPIC", body)
msg.WithTag("tag")
//指定分片 key,保证相同key的消息进入同一个queue
msg.WithShardingKey("*******" + "8d7d7bd0-7414-4f20-bd16-17c5ac71b8ce")
//设置
_, err = p.SendSync(context.Background(), msg) // 不能用单向
if err != nil {
log.Printf("send message err: %s", err)
return
}
}
这个就是Go发送分区顺序消息的方式,go这块资料比较少,记录一下,避免后续遗忘
核心就在于创建producer时候,就需要指定queue选择器,否则WithShardingKey设置了也不生效
这个QueueSelector队列选择器,其实就是hashQueueSelector,如果消息具有分片密钥,则按hash选择队列,否则按随机选择队列。