目录
前言
mq的使用在项目中是很常见的,当我们遇到不需要立即处理的某块业务,或者说某个业务的处理时间过长,那我们就可以使用mq来削减一下业务的立即处理时间。
在方法中,我们可以将需要处理的消息发送到Mq中,然后直接给前端做出响应,消费者再异步并发处理相关的业务,这样可以加快响应时间,给用户相对较好的使用体验,当然这只使用于不是立即需要出结果的场景。具体业务还是得具体分析。。。
常用的mq有kafka、rocketmq、rabbitmq…kafka可以每秒处理百万条的消息,rocketmq性能其次,rabbitmq性能略差,三者各有各的优缺点,本文我们我们就说说rocketmq的相关操作(下文都是rocketmq5.0基础上的操作)。
一、rocketmq常用概念介绍
模型图
1.1 主题(Topic)
简单来说就是对消息进行了分类,主题就是消息的种类,发送消息的时候要指定主题,接收消息的时候也要指定主题
Apache RocketMQ 的主题拆分设计应遵循大类统一原则,即将相同业务域内同一功能属性的消息划分为同一主题。拆分主题时,您可以从以下角度考虑拆分粒度:
-
消息类型是否一致:不同类型的消息,如顺序消息和普通消息需要使用不同的主题。
-
消息业务是否关联:如果业务没有直接关联,比如,淘宝交易消息和盒马物流消息没有业务交集,需要使用不同的消息主题;同样是淘宝交易消息,女装类订单和男装类订单可以使用同一个订单。当然,如果业务量较大或其他子模块应用处理业务时需要进一步拆分订单类型,您也可以将男装订单和女装订单的消息拆分到两个主题中。
-
消息量级是否一样:数量级不同或时效性不同的业务消息建议使用不同的主题,例如某些业务消息量很小但是时效性要求很强,如果跟某些万亿级消息量的业务使用同一个主题,会增加消息的等待时长。
1.2 队列(MessageQueue)
每个主题默认都有四个队列,消息在不指定发送规则的情况下,消息会平均分配到各个队列中,当然队列的数量可以进行调整。队列数量可以扩容或者缩减,如果要进行缩减,那么得等到队列中的消息全部消费了才能缩减,不然被缩减的队列的消息会丢失。
队列的读写权限
定义:当前队列是否可以读写数据。
取值:由服务端定义,枚举值如下
6:读写状态,当前队列允许读取消息和写入消息。
4:只读状态,当前队列只允许读取消息,不允许写入消息。
2:只写状态,当前队列只允许写入消息,不允许读取消息。
0:不可读写状态,当前队列不允许读取消息和写入消息。
约束:队列的读写权限属于运维侧操作,不建议频繁修改。
1.3 消息(Message)
消息的属性有很多,比如消息id,消息的key,消息的负载,发送主题,过滤标签等等很多,我们需要传输的消息实体指的就是消息的负载,由生产者负责序列化编码,按照二进制字节传输。springboot对此进行了封装。
发送消息的模式有两种
-
集群模式(CLUSTERING)
对于同一个消费者组里面的消费者来说,消费者是平均分配监听主题里面队列的数量的,所以说如果消费者组里面的消费者数量>=队列数量,那么多出来的消费者是没有作用的,属于空闲状态(例:如果四个队列,两个消费者,那么每个消费者对应两个队列。如果四个队列,五个消费者,那么有一个消费者就没有消息过来)。由此可见每条消息在一个消费者组只能被一个消费者所消费。 -
广播模式(BROADCASTING )
顾名思义,一条消息可以被同一个消费组里面的所有消费者消费。
1.4 生产者(Producer)
即消息的发起方,谁发的消息,谁就是生产者
1.5 消费者(Consumer)
即消息的接收方,接收消息的即为消费者
1.6 消费者组(ConsumerGroup)
如果多个消费者他们订阅的主题和标签都是同一个的话,那么这些消费者可以组成一个消费者组。
对于消费者分组的拆分设计,建议遵循以下原则:
- 消费者的投递顺序一致:同一消费者分组下所有消费者的消费投递顺序是相同的,统一都是顺序投递或并发投递,不同业务场景不能混用消费者分组。
- 消费者业务类型一致:一般消费者分组和主题对应,不同业务域对消息消费的要求不同,例如消息过滤属性、消费重试策略不同。因此,不同业务域主题的消费建议使用不同的消费者分组,避免一个消费者分组消费超过10个主题。
1.7 消费过滤
消息过滤分为两种,TAG过滤和SQL92过滤。这两种都是在主题的基础上面进行更详细的划分。
- TAG过滤
- 单Tag匹配:过滤表达式为目标Tag。表示只有消息标签为指定目标Tag的消息符合匹配条件,会被发送给消费者。
- 多Tag匹配:多个Tag之间为或的关系,不同Tag间使用两个竖线(||)隔开。例如,Tag1||Tag2||Tag3,表示标签为Tag1或Tag2或Tag3的消息都满足匹配条件,都会被发送给消费者进行消费。
- 全部匹配:使用星号(*)作为全匹配表达式。表示主题下的所有消息都将被发送给消费者进行消费。
- SQL92过滤
由于SQL属性过滤是生产者定义消息属性,消费者设置SQL过滤条件,因此过滤条件的计算结果具有不确定性,服务端的处理方式如下:
-
异常情况处理:如果过滤条件的表达式计算抛异常,消息默认被过滤,不会被投递给消费者。例如比较数字和非数字类型的值。
-
空值情况处理:如果过滤条件的表达式计算值为null或不是布尔类型(true和false),则消息默认被过滤,不会被投递给消费者。例如发送消息时未定义某个属性,在订阅时过滤条件中直接使用该属性,则过滤条件的表达式计算结果为null。
-
数值类型不符处理:如果消息自定义属性为浮点型,但过滤条件中使用整数进行判断,则消息默认被过滤,不会被投递给消费者。
二、整合springboot进行操作
2.1 依赖
springboot版本为2.3.12.RELEASE
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
<exclusions>
<exclusion>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-acl</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-acl</artifactId>
<version>5.0.0</version>
</dependency>
2.2 同步消息
同步信息是按照顺序发送,上一条消息发送完才能发送下一条消息,所以比较消耗资源。若发送失败mq会默认重试5次(加上第一次一共6次发送),若都失败,则需要在生产者方进行消息的持久化,因为都失败则会抛出异常报错。重试次数可以自己设定,一般发送失败的情况很少。
在这个样例我就做个简单的同步发送即可,正常如果防丢失的话一般采用异步发送,后续会说明。
生产者代码
@PostMapping("/sendSyncMessage")
public void sendSyncMessage() {
UserVo userVo = new UserVo();
userVo.setId(SnowFlakeUtil.getId());
userVo.setName("同步消息");
userVo.setCreateTime(new Date());
rocketMQTemplate.syncSend("sync-topic", JSON.toJSONString(userVo));
}
消费者代码
@Log4j2
@Component
@RocketMQMessageListener(
topic = "sync-topic", // 主题
consumeMode = ConsumeMode.CONCURRENTLY, // 多线程还是顺序消费,默认多线程
messageModel = MessageModel.CLUSTERING, // 集群模式还是广播模式,默认集群模式
consumerGroup = "bs-sync-consumer") //记得指定消费者组
public class SyncConsumerListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
log.info("同步接收到消息=====>{}", JSON.parseObject(new String(messageExt.getBody()), UserVo.class).toString());
}
}
2.3 异步消息
发送异步消息在mq中是很常见的一种用法,他采用回调的方式,使用户不需要等上一条消息发送完毕才能发送下一条,大大加快了发送的效率,发送结果的成功与失败都可以在回调中进行自己的业务补偿操作。默认重试3次。
生产者代码
@PostMapping("/sendAsyncMessage")
public void sendAsyncMessage() {
UserVo userVo = new UserVo();
Long id = SnowFlakeUtil.getId();
userVo.setId(id);
userVo.setName("异步消息");
userVo.setCreateTime(new Date());
// 将消息存入数据库或者redis然后再发送
redisTemplate.opsForValue().set(id.toString(), JSON.toJSONString(userVo));
rocketMQTemplate.asyncSend("async-topic", JSON.toJSONString(userVo), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("messageQueue===>" + sendResult.getMessageQueue()
+ ",sendStatus===>" + sendResult.getSendStatus()
+ ",msgId===>" + sendResult.getMsgId()
+ ",transactionId===>" + sendResult.getTransactionId()
+ ",offsetMsgId===>" + sendResult.getOffsetMsgId()
+ ",queueOffset===>" + sendResult.getQueueOffset()
+ ",regionId===>" + sendResult.getRegionId());
redisTemplate.delete(id.toString());
}
@Override
public void onException(Throwable throwable) {
log.info("exception===>{}", throwable.getMessage());
}
});
}blic void onException(Throwable throwable) {
log.info("exception===>{}", throwable.getMessage());
}
});
}
生产者异步发送,为了保证消息不丢失,我们在发送之前可以将消息持久化到数据库中(mysql,redis等等都可以),然后在成功回调的时候把前面持久化的记录删除即可。如果一直发送失败,那我们可以搞个定时任务,比如每十分钟定时扫描我们持久化的那张表,如果没发送成功就继续发送,并且定时人工检查一下消息。
当然也可以使用rocketmq自带的事务进行可靠性的实现,发送失败就直接回滚,这里不做演示。
消费者代码
@Log4j2
@Component
@RocketMQMessageListener(
topic = "async-topic",
consumeMode = ConsumeMode.CONCURRENTLY, // 默认就是该值。ConsumeMode.ORDERLY和MessageModel.BROADCASTING不能一起设置
messageModel = MessageModel.CLUSTERING, // 默认就是该值,即集群模式
consumerGroup = "bs-async-consumer")
public class AsyncConsumerListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
log.info("异步接收到消息=====>{}", JSON.parseObject(new String(messageExt.getBody()), UserVo.class).toString());
}
}
2.4 单向消息
单向消息是不需要关心发送结果,我只管发送,成功还是不成功我不关心。一般用于不是很重要的消息场景,比如日志…
生产者
@PostMapping("/sendOneWayMessage")
public void sendOneWayMessage() {
UserVo userVo = new UserVo();
userVo.setId(SnowFlakeUtil.getId());
userVo.setName("单向消息");
userVo.setCreateTime(new Date());
rocketMQTemplate.sendOneWay("oneWay-topic", JSON.toJSONString(userVo));
}
消费者
@Log4j2
@Component
@RocketMQMessageListener(
topic = "oneWay-topic",
consumeMode = ConsumeMode.CONCURRENTLY,
messageModel = MessageModel.CLUSTERING,
consumerGroup = "bs-oneWay-consumer")
public class OneWayConsumerListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
log.info("单向接收到消息=====>{}", JSON.parseObject(new String(messageExt.getBody()), UserVo.class).toString());
}
}
2.5 延迟消息
rocketmq默认的延迟等级有18个,分别是1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,这个时间和数量可以自己设置。延迟消息的场景还是比较多的,比如订单如果三十分钟之内没支付,那么自动取消等等。
生产者
@PostMapping("/sendDelayMessage")
public void sendDelayMessage() {
UserVo userVo = new UserVo();
userVo.setId(SnowFlakeUtil.getId());
userVo.setName("延迟消息");
userVo.setCreateTime(new Date());
Message<UserVo> message = MessageBuilder.withPayload(userVo).build();
//messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
// delay = 2,第二个等级,即延迟5秒
System.out.println("发送时间" + new Date());
rocketMQTemplate.syncSend("delay-topic", message, 3000, 2);
}
消费者
@Log4j2
@Component
@RocketMQMessageListener(
topic = "delay-topic",
consumeMode = ConsumeMode.CONCURRENTLY,
messageModel = MessageModel.CLUSTERING,
consumerGroup = "bs-delay-consumer")
public class DelayConsumerListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
System.out.println("接受时间" + new Date());
log.info("延迟接收到消息=====>{}", JSON.parseObject(new String(messageExt.getBody()), UserVo.class).toString());
}
}
2.6 顺序消息
某些特殊的场景,对于一系列的操作,我们需要按照顺序执行。比如某个业务分为1,2,3三个步骤,那么需要我们对于同一个业务,必须按照1,2,3的顺序消费,中间如果有其他业务步骤我们不管,反正对于同一个业务的顺序必须是正确的即可。
生产者
@PostMapping("/sendOrderlyMessage")
public void sendOrderlyMessage() {
Long id1 = SnowFlakeUtil.getId();
Long id2 = SnowFlakeUtil.getId();
List<UserVo> userVoList = Arrays.asList(
new UserVo(id1, "顺序消息1-1", new Date()),
new UserVo(id1, "顺序消息1-2", new Date()),
new UserVo(id1, "顺序消息1-3", new Date()),
new UserVo(id2, "顺序消息2-1", new Date()),
new UserVo(id2, "顺序消息2-2", new Date()),
new UserVo(id2, "顺序消息2-3", new Date())
);
userVoList.forEach(userVo -> rocketMQTemplate.syncSendOrderly("orderly-topic", JSON.toJSONString(userVo), userVo.getId().toString()));
}
消费者
@Log4j2
@Component
@RocketMQMessageListener(
topic = "orderly-topic",
consumeMode = ConsumeMode.ORDERLY,
messageModel = MessageModel.CLUSTERING,
consumerGroup = "bs-orderly-consumer")
public class OrderlyConsumerListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
log.info("顺序接收到消息Name=====>{}", JSON.parseObject(new String(messageExt.getBody()), UserVo.class).getName());
}
}
顺序消息需要注意点:
- 发送的时候,同一个业务的消息需要发送到主题的同一个队列当中。(所以我们在发送的时候指定了hashkey,此例中的hashkey我们用的是userId)
- 消费者对于一个队列的消费必须顺序,或者说必须一个一个的消费,不要多线程消费(此例中我们采取的是前者方案,将消费方式改成了顺序消费,consumeMode = ConsumeMode.ORDERLY。。。如果想要继续用consumeMode = ConsumeMode.CONCURRENTLY模式,只需配合设置最大线程数为1即可,如consumeThreadNumber = 1)
2.7 消费者的可靠性分析
前面我们分析了生产者的消息的可靠性的操作,其实生产环境中生产者出现错误的情况还是比较少的,除了网络波动几乎不会出现问题,真正要解决的重点是消费者端的问题。
消费端的两大问题
- 重复消费
- 消费失败和消息丢失
2.7.1 重复消费
由于生产者的不确定性,可能导致某条消息发送多次,那么我们消费端就需要保证消息的幂等性。
方法:可以在发送消息的时候,给消息一个唯一标识,可以是key,也可以是消息实体类里面的主键id,然后消费端在接收到消息的时候将前面提到的唯一标识存入数据库(数据库中可以创建一张去重表)如果插入失败,说明已经处理过这个消息,直接丢弃,反之进行业务操作。
2.7.2 消费失败和消息丢失
方法:消息失败我们可以设置重试机制,自定义重试次数,如果多次失败超过我们设置阈值,那么我们就将此消息持久化到数据库即可,联系上面的重复消费的问题,所以如果消费失败则需要在去重表删除主键。
生产者代码
@PostMapping("/sendHasKeyMessage")
public void sendHasKeyMessage() {
UserVo userVo = new UserVo();
Long id = SnowFlakeUtil.getId();
userVo.setId(id);
userVo.setName("带有key的消息");
userVo.setCreateTime(new Date());
Message<UserVo> message = MessageBuilder.withPayload(userVo).setHeader(RocketMQHeaders.KEYS, id.toString()).build();
rocketMQTemplate.send("hasKey-topic", message);
}
消费者代码
@Log4j2
@Component
@RocketMQMessageListener(
topic = "hasKey-topic",
consumeMode = ConsumeMode.CONCURRENTLY,
messageModel = MessageModel.CLUSTERING,
consumerGroup = "bs-hasKey-consumer")
public class ReliabilityConsumerListener implements RocketMQListener<MessageExt> {
@Resource
private MessageDeduplicationService messageDeduplicationService;
@Resource
private MyService myService;
@Override
public void onMessage(MessageExt messageExt) {
log.info("同步接收到消息=====>{}", JSON.parseObject(new String(messageExt.getBody()), UserVo.class).toString());
Long keys = Long.valueOf(messageExt.getKeys());
MessageDeduplication messageDeduplication = new MessageDeduplication();
messageDeduplication.setId(keys);
try {
messageDeduplicationService.save(messageDeduplication);
} catch (Exception e) {
log.info("消息已经消费过了===={}", JSON.parseObject(new String(messageExt.getBody()), UserVo.class).toString());
}
// 能插入数据说明未重复,直接处理业务即可
try {
myService.doSomeThing();
} catch (Exception e) {
// 消费失败删除去重表的记录
messageDeduplicationService.removeById(keys);
// 获取重试次数
int reconsumeTimes = messageExt.getReconsumeTimes();
// 设置重试两次
if (reconsumeTimes > 2) {
// TODO 将消息持久化到数据库,多次消费失败基本是业务代码有问题,需要人工干预
}
}
}
}
总结
`上述讲述了rocketmq的常见用法,还有一些事务机制,死信队列等等不常用内容在这先不做叙述,后续有时间再进行补充,有不足的地方希望大家在评论区指正,谢谢~