提示:本文属于RocketMQ的基础文章,参考自RocketMQ · 官方网站 | RocketMQ (apache.org)
目录
4.6RocketMQ集成SpringBoot消息消费两种模式
1.基础概念
RocketMQ 是一款高性能、可扩展的消息中间件,它提供了强大的异步通信能力,支持多种消息类型和消息传递模式。通过异步消息处理机制,RocketMQ 能够帮助应用程序实现非阻塞式的通信,从而显著提高系统的响应速度和吞吐量。这种异步特性使得应用能够在发出消息后立即返回,无需等待消息被处理完成,这对于需要快速响应用户请求的高并发系统尤其重要。RocketMQ 还具备优秀的削峰限流功能,可以在高流量场景下平滑地处理突发的数据洪峰。例如,在电商网站的促销活动中,短时间内可能会产生大量的订单请求,RocketMQ 可以有效地缓冲这些请求,避免后端服务因瞬时高负载而崩溃,从而保证业务系统的稳定运行。RocketMQ还 提供了一种优雅的解耦合机制,允许生产者和消费者独立地开发、部署和扩展。这种解耦可以极大地提高系统的灵活性和可维护性,因为生产者和消费者之间不再直接依赖,即使消费者暂时不可用,生产者也可以继续正常工作。这不仅提高了系统的健壮性,还简化了故障恢复的过程。
以下是RocketMQ的几个重要概念:
1.1基础结构
Producer:消息的发送者,生产者;举例:发件人
Consumer:消息接收者,消费者;举例:收件人
Broker:暂存和传输消息的通道;举例:快递
1.2引入topic
Topic:主题,消息的分类
生产者组中有p1,p2。消费者组中有c1,c2。可以发送信息到不同的主题,也可以从不同的主题中接收信息。
1.3引入队列
Queue:队列,消息存放的位置,一个Broker中可以有多个队列
1.4NameServer
管理Broker;举例:各个快递公司的管理机构 相当于broker的注册中心,保留了broker的信息
1.5主从概念
2.RocketMQ快速入门
编写第一个测试程序,主要是熟悉编写生产者和消费者的代码
引入依赖:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
定义生产者:
@Test
public void simpleProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//创建一个生产者,制定一个组名
DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
//连接namesrv
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
//启动
producer.start();
//创建信息
Message message = new Message("testTopic", "我是一个简单的信息".getBytes());
//发送信息
SendResult sendResult = producer.send(message);
System.out.println(sendResult.getSendStatus());
//关闭生产者
producer.shutdown();
}
可以看到test Topic已经加入
定义消费者:
/*
消费者操作
*/
@Test
public void simpleConsumer() throws Exception {
//创建一个消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");
//连接nameSrv
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
//订阅一个主题 * 代表订阅这个主题中所有信息
consumer.subscribe("testTopic","*");
//设置一个监听器 (一直监听,异步回调)
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//此处为消费的方法
System.out.println("我是消费者");
System.out.println(msgs.get(0).toString());
//转为能看懂的文字
System.out.println("消息内容:" + new String(msgs.get(0).getBody()));
System.out.println("消费上下文:" + consumeConcurrentlyContext);
//返回成功(SUCCESS) ,消息从队列中弹出
//RECONSUME,失败,消息重新回到队列,过一会重新投递供当前消费者或其他消费者
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//启动
consumer.start();
//挂起当前的jvm
System.in.read();
}
流程解析
注意!
如下代码创建两个消费者实例,它们都属于同一个生产者组 "test-producer-group"。他们都能选择不同的topic, 但是消费者的一个组只能选择一样的topic
DefaultMQProducer producer1 = new DefaultMQProducer("test-producer-group");
DefaultMQProducer producer2 = new DefaultMQProducer("test-producer-group");
发送是以轮询的方式随机发送到broker中的四个队列中,但是消费的时候,看组里有多少个消费者,每个消费者只能从固定的队列中取到消息
假设有五个消费者,第五个人永远没有消息, 所以队列数量要大于等于消费者数量
3.RocketMQ常见消息种类
3.1发送同步消息
上面的快速入门就是发送同步消息,发送过后会有一个返回值,也就是 mq 服务器接收到消息后返回的一个确认,这种方式虽然保证了安全性,但是性能上并没有这么高,而且在mq 集群中,也是要等到所有的从机都复制了消息以后才会返回,所以针对重要的消息可以选择这种方式
3.2发送异步消息
异步消息通常用在对响应时间要求较高的业务场景,即发送端不能容忍长时间地等待 Broker的响应。发送完以后会有一个异步消息通知。
生产者代码如下:
@Test
public void asyncProducer() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("async-producer-group");
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
producer.start();
Message message = new Message("asyncTopic", "我是一个异步消息".getBytes());
//异步消息就是在此调用了new SendCallback()来完成操作,重写发送成功和发送失败的逻辑。
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功");
}
@Override
public void onException(Throwable e) {
System.out.println("发送失败" + e.getMessage());
}
});
System.out.println("我先执行");
System.in.read();
}
3.3发送单向信息
RocketMQ 的单向消息发送是一种优化的发送模式,主要用于那些不需要确认消息是否成功到达消费者的应用场景。这种模式非常适合于日志记录、监控数据上报等场景,其中发送方并不关心消息是否被成功消费。由于不需要等待消息确认,发送速度非常快,能够达到极高的吞吐量。发送方可以迅速释放资源,减少等待时间,提高整体效率。消息一旦发送出去,就不再追踪其状态,这意味着如果消息在网络传输过程中丢失或者 Broker 出现故障,消息将无法被重发,因此对于那些对消息可靠性要求不高的场景,这是可以接受的。
@Test
public void onewayTest() throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("oneway-producer-group");
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
producer.start();
Message message = new Message("onewayTopic", "日志xx".getBytes());
//单向信息的实现依靠 senOneway()方法实现
producer.sendOneway(message);
System.out.println("成功");
producer.shutdown();
}
3.4发送延迟消息
RocketMQ 的延迟消息是一种非常有用的功能,它允许消息在发送后经过一段时间延迟再被消费。这种功能在很多场景中都非常有用,尤其是在需要定时执行某些操作的情况下。消息放入 mq 后,过一段时间,才会被监听到,然后消费比如下订单业务,提交了一个订单就可以发送一个延时消息,30min 后去检查这个订单的状态如果还是未付款就取消订单释放库存。
延迟等级: 延迟等级是从 1 到 18 的整数,每个等级对应不同的延迟时间。默认情况下,延迟等级 1 对应 1 秒的延迟,等级 2 对应 5 秒的延迟,等级 3 对应 10 秒的延迟,以此类推。更高级别的延迟等级对应的延迟时间更长,例如等级 18 可能对应 72 小时的延迟。
延迟时间配置:延迟时间的具体值可以在 RocketMQ 的配置文件中自定义,这使得开发者可以根据具体需求灵活配置延迟时间。通常,延迟时间是按照指数递增的方式来设置的,例如 1s, 5s, 10s, 30s, 1m, 2m, 3m, 4m, 5m, 6m, 7m, 8m, 10m, 20m, 30m, 1h, 2h, 3h, 12h, 1d, 2d, 3d, 7d。
以下是延迟消息生产者与消费者的示例代码:
@Test
public void testDelayProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
Message msg = new Message("TopicTest", ("延迟消息").getBytes());
// 给这个消息设定一个延迟等级
// messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
msg.setDelayTimeLevel(3);
// 发送单向消息
producer.send(msg);
// 打印时间
System.out.println(new Date());
// 关闭实例
producer.shutdown();
}
@Test
public void msConsumer() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ms-consumer-group");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("orderMSTopic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println("收到信息了" + new Date());
System.out.println(msgs.get(0).getBody());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
3.5发送批量消息
RocketMQ 支持批量发送消息,这可以提高消息发送的效率,尤其是在需要发送大量相似消息的场景中。批量发送的消息会被作为一个批次进行处理,从而减少网络开销和提高吞吐量。
示例代码:
@Test
public void testBatchProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
List<Message> msgs = Arrays.asList(
new Message("TopicTest", "我是一组消息的A消息".getBytes()),
new Message("TopicTest", "我是一组消息的B消息".getBytes()),
new Message("TopicTest", "我是一组消息的C消息".getBytes())
);
SendResult send = producer.send(msgs);
System.out.println(send);
// 关闭实例
producer.shutdown();
}
@Test
public void testBatchConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅一个主题来消费 表达式,默认是*
consumer.subscribe("TopicTest", "*");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println("收到信息了" + new Date());
System.out.println(msgs.get(0).getBody());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
看到运行结果可得,全部信息都发送到第三个队列中,实现了批量发送。
3.6发送顺序消息
由上文可知,由于生产者把消息发送到队列用的是轮询的方式,而消费者在默认情况下接收消息也是轮询的方式,消费者默认情况下是用多线程的并发模式接收消息,这时候就不能保证接收消息的顺序。比如我们想实现A->B->C->D的接收消息的顺序,如何解决这个问题呢?我们可以:把消费者变为单线程模式,但还不能解决问题,因为可能以B->A->C->D的顺序出来,不能保证ABCD的顺序。
可以想到另一个解决方式:保证队列顺序,不需要全局顺序 , 局部有序即可,消费者把消息按序发送到一个队列中就行了,然后用单线程模式
多线程模式下示例代码:
//生产者
@Test
public void orderlyProducer() throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("orderly-producer-group");
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
producer.start();
for (int i = 1 ; i <= 10; i++){
Message message = new Message("orderlyTopic", ("我是第" + i + "个消息").getBytes());
producer.send(message);
}
producer.shutdown();
System.out.println("发送完成");
}
//消费者
@Test
public void orderlyConsumer()throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly-consumer-group");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("orderlyTopic","*");
//默认模式下并发模式多线程执行(20个线程)
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println(new String(msgs.get(0).getBody()));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
可以看到多线程模式下即默认模式下,四个队列中都收到了信息。
同时消息也是无序的。
使用顺序发送:
private List<MsgModel> msgModels = Arrays.asList(
new MsgModel("aaa",1,"下单"),
new MsgModel("aaa",1,"短信"),
new MsgModel("aaa",1,"物流"),
new MsgModel("bbb",2,"下单"),
new MsgModel("bbb",2,"短信"),
new MsgModel("bbb",2,"物流")
); //先定义两组对象,需要让者两组都按照下单-短信-物流的顺序发送出去。
//生产者
@Test
public void orderlyProducer() throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("orderly-producer-group");
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
producer.start();
//发送顺序消息 发送时要确保有序,并且要发到同一个队列下面
msgModels.forEach(msgModel -> {
Message message = new Message("orderlyTopic", msgModel.toString().getBytes());
try {
// 发送的时候 相同的订单号选择同一个队列
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 当前主题有多少个队列
int queueNumber = mqs.size();
// 这个arg就是后面传入的 order.getOrderNumber()
Integer i = (Integer) arg;
// 用这个值去%队列的个数得到一个队列
int index = i % queueNumber;
// 返回选择的这个队列即可 ,那么相同的订单号 就会被放在相同的队列里 实现FIFO了
return mqs.get(index);
}
}, msgModel.getOrderSn());
} catch (Exception e) {
System.out.println("发送异常");
}
});
producer.shutdown();
System.out.println("发送完成");
}
查看发送的结果,这六条两组的消息都成功发送,接下来要去看消费者是否正确按序收到消息。
消费者需要开启单线程模式。
//消费者代码
@Test
public void orderlyConsumer()throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly-consumer-group");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("orderlyTopic","*");
//顺序模式,单线程的
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
System.out.println("线程id" + Thread.currentThread().getId());
System.out.println(new String(msgs.get(0).getBody()));
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.in.read();
}
按序发送成功了。
3.7发送带标签的消息,消息过滤
Rocketmq提供消息过滤功能,通过tag或者key进行区分。当业务场景需要细分的时候,同一个消费组里的消费者可能要订阅不同的主题进行不同的业务流程,但按照规定同一个消费组里的消费者必须订阅同一个Topic,这时候就可以用tag标签进行发送达到想要的效果,比如带有tagA标签的被A消费,带有tagB标签的被B消费
订阅关系一致性:主题和tag必须一致才能叫做订阅关系一致,两个消费者在一个组内
当tag不一致的时候,c1和c2就不是一个消费组,c2和c3是一个消费组
示例代码:
/*
生产者
这里有vip1 和 vip2 两个标签
*/
@Test
public void tagProducer()throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("tag-producer-group");
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
producer.start();
Message message1 = new Message("tagTopic", "vip1", "我是vip1的文章".getBytes());
Message message2 = new Message("tagTopic", "vip2", "我是vip2的文章".getBytes());
producer.send(message1);
producer.send(message2);
System.out.println("发送成功");
producer.shutdown();
}
//消费者1 使用了vip1标签
@Test
public void tagConsumer1() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tag-consumer-group-a");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("tagTopic","vip1");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println("我是vip1的消费组,我正在消费消息" + new String(msgs.get(0).getBody()));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
//消费者2,同时有vip1和vip2两个标签
@Test
public void tagConsumer2() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tag-consumer-group-b");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("tagTopic","vip1 || vip2");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println("我是vip2的消费组,我正在消费消息" + new String(msgs.get(0).getBody()));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
消费结果:
消费者1只能收到vip1的消息
消费者2可以收到vip1和vip2的消息
3.8Key的使用
在业务中 要自身确保key值的唯一,为了后续方便查阅和去重
示例代码:
//生产者
@Test
public void keyProducer()throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("key-producer-group");
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
producer.start();
String key = UUID.randomUUID().toString();
System.out.println(key);
Message message = new Message("keyTopic", "vip1", key,"我是vip1的文章".getBytes());
producer.send(message);
System.out.println("发送成功");
producer.shutdown();
}
结果:
//消费者代码
@Test
public void keyConsumer() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("key-consumer-group");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("keyTopic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt messageExt = msgs.get(0);
System.out.println("我是vip1的消费者,我正在消费消息" + new String(messageExt.getBody()));
System.out.println("我们业务的标识:" + messageExt.getKeys());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
结果:
3.9RocketMQ消息重复消费问题(去重)重点
为什么会出现重复消费? 原因可能是因为生产者多次投递消息,或者消费者因为扩容时会重试接收消息。解决方法可以在C端加一个唯一标记,可以用msgId(mq给消息生产的唯一标记)也可以自定义key 。
RocketMQ 确保所有消息至少传递一次。大多数情沈下,消息不会重复
RocketMQ无法避免消息重复 (Exactly-Once),所以如果业务对消费重复非常敏感,务必要在业务层面进行去重处理。可以借助关系数据库进行去重。首先需要确定消息的唯一键,可以是msgld,也可以是消息内容中的唯一标识字段,例如订单Id等。在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入,并消费,否则跳过。 (实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳
msgld一定是全局唯一标识符,但是实际使用中,可能会存在相同的消息有两个不同msgld的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费。
*幂等性 : 多次操作产生的影响均和第一次操作产生的影响相同
新增:普通的新增操作是非幂等的,唯一索引的新增,是幂等的
修改:看情况 如:update goods set stock = 10 where id = 1这一句是幂等的。update goods set stock = stock -1 where id = 1这一句是非幂等的。
查询:是幂等操作
删除:是幂等操作
所以我们可以利用这一点来避免消息的重复消费。我们设计一个去重表 对消息的唯一key添加唯一索引每次消费消息的时候 先插入数据库 如果成功则执行业务逻辑如果插入失败 则说明消息来过了,直接签收
//生产者
@Test
void contextLoads() throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("repeat-producer-group");
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
producer.start();
String key = UUID.randomUUID().toString();
System.out.println(key);
// 测试, 发两个一样的key
Message m1 = new Message("repeatKey", null, key, "扣减库存-1".getBytes());
Message m2 = new Message("repeatKey", null, key, "扣减库存-1".getBytes());
System.out.println("发送成功");
producer.shutdown();
}
//消费者
@Test
void repeatConsumer() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("repeat-consumer-group");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("repeatTopic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//获取key
MessageExt messageExt = msgs.get(0);
String keys = messageExt.getKeys();
//插入数据库 因为key是唯一索引
jdbcTemplate.update("插入数据库的语句");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
}
3.10重试机制
RocketMQ 的重试机制是为了确保消息能够被正确处理而设计的。当消费者未能成功处理消息时,RocketMQ 会尝试重新发送这些消息,直到它们被成功处理为止。mq默认重试16次(并发模式),如果是顺序模式下重试次数是(int的最大值次)。
那么我们是否可以自定义重试次数?答案是可以的。如果不在消费者设置重试次数,就会按照十六次一次一次重拾下去,但是设置了规定的次数就只会重试规定的次数
//生产者
@Test
public void retryProducer()throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("retry-producer-group");
producer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
producer.start();
//生产者发送消息 重试次数 默认都是两次
producer.setRetryTimesWhenSendFailed(2);
producer.setRetryTimesWhenSendAsyncFailed(2);
String key = UUID.randomUUID().toString();
System.out.println(key);
Message message = new Message("retryTopic", "vip1", key,"我是vip1的文章".getBytes());
producer.send(message);
System.out.println("发送成功");
producer.shutdown();
}
//消费者
@Test
public void retryConsumer() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("retryTopic","*");
//设定重试次数
consumer.setMaxReconsumeTimes(2);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt messageExt = msgs.get(0);
System.out.println(new Date());
System.out.println(messageExt.getBody());
//业务报错了 返回null 返回RECONSUME_LATER 都会重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
System.in.read();
}
上述代码故意设置重试两次,并且一直返回失败,重试两次后就不再重试。
重试16次之后呢?(如果是顺序模式下重试次数是int的最大值次)如果均为失败的话怎么办?
那就会变成一个死信消息 会被放到一个死信主题中 主题的名称:%DLQ%retry-consumer-group"
第一种方案:直接监听死信主题的消息,记录下拉,通知人工运维处理
@Test
public void retryDeadConsumer() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-dead-consumer-group");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("%DLQ%retry-consumer-group","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt messageExt = msgs.get(0);
System.out.println(new Date());
System.out.println(messageExt.getBody());
System.out.println("记录到特别的位置 文件或者mysql中通知人工处理");
//业务报错了 返回null 返回RECONSUME_LATER 都会重试
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
第二种方案,在业务逻辑中捕获异常
@Test
public void retryConsumer2() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
consumer.setNamesrvAddr(MQConstant.NAME_SRV_ADDR);
consumer.subscribe("retryTopic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try {
// 这里执行消费的代码
System.out.println(Thread.currentThread().getName() + "----" + msgs);
// 这里制造一个错误
int i = 10 / 0;
} catch (Exception e) {
// 出现问题 判断重试的次数
MessageExt messageExt = msgs.get(0);
// 获取重试的次数 失败一次消息中的失败次数会累加一次
int reconsumeTimes = messageExt.getReconsumeTimes();
if (reconsumeTimes >= 3) {
// 则把消息确认了,可以将这条消息记录到日志或者数据库 通知人工处理
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} else {
//继续重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
4.Springboot集成RocketMQ
4.1前期准备
添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
配置yml文件
4.2同步,异步 ,单向,延迟消息
生产者示例代码:
@SpringBootTest
class BRocketmqPApplicationTests {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
void contextLoads() {
//同步
rocketMQTemplate.syncSend("bootTestTopic","我是boot的一个消息");
//异步
rocketMQTemplate.asyncSend("bootAsyncTestTopic", "我是boot的异步消息", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功");
}
@Override
public void onException(Throwable throwable) {
System.out.println("失败" + throwable.getMessage());
}
});
//单向
rocketMQTemplate.sendOneWay("bootOnewayTopic","单向消息");
//延迟消息
Message<String> message = MessageBuilder.withPayload("我是一个延迟消息").build();
rocketMQTemplate.syncSend("bootMsTopic",message,3000,3);
}
}
消费者示例代码:
@Component
@RocketMQMessageListener(topic = "bootTestTopic",consumerGroup = "boot-test-group")
public class ABootSimpleMsgListener implements RocketMQListener<MessageExt> {
/**
*
* 这个方法就是消费者的方法
* 如果泛型制定了固定的类型 那消息体就是我们的参数
* MessageExt类型是消息的所有内容
* 没有报错就签收了
* 如果报销了 就是拒收 就会重试
* 水
* @param messageExt
*/
@Override
public void onMessage(MessageExt messageExt) {
System.out.println(new String(messageExt.getBody()));
}
}
4.3顺序消息
生产者示例代码:
@SpringBootTest
class BRocketmqPApplicationTests {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
void contextLoads() {
//顺序消息 发送者 需要将一组消息 都发送到一个队列中去 消费者 需要单线程消费
List<MsgModel> msgModels = Arrays.asList(
new MsgModel("aaa",1,"下单"),
new MsgModel("aaa",1,"短信"),
new MsgModel("aaa",1,"物流"),
new MsgModel("bbb",2,"下单"),
new MsgModel("bbb",2,"短信"),
new MsgModel("bbb",2,"物流")
);
msgModels.forEach(msgModel -> {
//发送,以json的方式进行处理
rocketMQTemplate.syncSendOrderly("bootOrderlyTopic",
JSON.toJSONString(msgModel),msgModel.getOrderSn());
});
}
}
消费者示例代码:
@Component
@RocketMQMessageListener(topic = "bootOrderlyTopic",
consumerGroup = "boot-orderly-consumer-group",
consumeMode = ConsumeMode.ORDERLY, //顺序消费模式 单线程
maxReconsumeTimes = 5) //消费重试次数
public class BOrderlyMsgListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
MsgModel msgModel = JSON.parseObject(new String(messageExt.getBody()), MsgModel.class);
System.out.println(msgModel);
}
}
运行结果:
4.4带标签的消息
生产者示例代码:
@Test
void tagKeyTest() throws Exception{
//topic:tag
rocketMQTemplate.syncSend("bootTagTopic:tagA","我是一个带tag的消息");
}
消费者示例代码:
@Component
@RocketMQMessageListener(topic = "bootTagTopic",
consumerGroup = "boot-tag-consumer-group",
selectorType = SelectorType.TAG, //tag过滤模式
selectorExpression = "tagA || tagB"
)
public class CTagMsgListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
System.out.println(new String(messageExt.getBody()));
}
}
另一种发送方式
rocketMQTemplate.syncSend("bootTagTopic:3","我是一个带tag的消息"); //broker.conf中开启enbalePropertyFilter=true
消费者代码:
@Component
@RocketMQMessageListener(topic = "bootTagTopic",
consumerGroup = "boot-tag-consumer-group",
selectorType = SelectorType.SQL92, //broker.conf中开启enbalePropertyFilter=true
selectorExpression = "a in (3,5,7)"
)
public class CTagMsgListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
System.out.println(new String(messageExt.getBody()));
}
}
第二种方式比较少见,作了解即可。
4.5带key的消息
生产者示例代码:
@Test
void tagKeyTest() throws Exception{
//key是携带在消息头的
Message<String> message = MessageBuilder.withPayload("我是一个带key的消息").
setHeader(RocketMQHeaders.KEYS, "123").build();
rocketMQTemplate.syncSend("bootKeyTopic",message);
}
消费者示例代码:
@Override
public void onMessage(MessageExt messageExt) {
messageExt.getKeys();
System.out.println(new String(messageExt.getBody()));
}
4.6RocketMQ集成SpringBoot消息消费两种模式
负载均衡模式
生产者示例代码:
@Test
void modeTest()throws Exception{
for (int i = 1 ; i<=10 ;i++){
rocketMQTemplate.syncSend("modeTopic","我是第"+ i + "个消息");
}
}
消费者示例代码:
//创建两个消费者DC1和DC2
@Component
@RocketMQMessageListener(topic = "modelTopic",
consumerGroup = "mode-consumer-group-a" ,
messageModel = MessageModel.CLUSTERING //集群模式 负载均衡 是默认的模式
)
public class DC1 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-a组的第一个消费者" + message);
}
}
@Component
@RocketMQMessageListener(topic = "modelTopic",
consumerGroup = "mode-consumer-group-a" ,
messageModel = MessageModel.CLUSTERING //集群模式 负载均衡 是默认的模式
)
public class DC2 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-a组的第二个消费者" + message);
}
}
从消费结果来看,两个消费者各自都消费了五条信息,队列中也都有信息
广播模式
广播模式就是每个消费者都接收相同数量的消息
生产者示例代码:
//发送五条信息
@Test
void modeTest()throws Exception{
for (int i = 1 ; i<=5 ;i++){
rocketMQTemplate.syncSend("modeTopic","我是第"+ i + "个消息");
}
}
消费者:建立3个同一个组内的消费者
//即DC3,DC4,和DC5,此处省略了冗余代码创建
@Component
@RocketMQMessageListener(topic = "modelTopic",
consumerGroup = "mode-consumer-group-b" ,
messageModel = MessageModel.BROADCASTING //广播模式
)
public class DC4 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-b组的第一个消费者" + message);
}
}
和负载均衡模式进行对比可得,当有三个消费者时,三个消费者会消费掉15条信息,而负载均衡模式是三个消费者一起消费5条信息。
5.如何解决消息堆积问题?
一般认为单条队列消息差值>=10w时算堆积问题
什么情况下会出现堆积?
1.生产太快了,生产方可以做业务限流或者增加消费者数量,但是消费者数量<=队列数量,适当的设置最大的消费线程数量(根据IO(2n)/CPU(n+1))。动态扩容队列数量,从而增加消费者数量。
2.消费者消费出现问题,排查消费者程序的问题。
6.如何确保消息不丢失?
消息生产阶段如何保证消息不丢失
同步发送:特点是需要等待Broker返回确认信息。如果不抛出异常,意味着消息发送成功。同步发送方式能够确保消息正确地投递到RocketMQ。但是,这并不代表消息在存储阶段不丢失。如果出现超时或失败状态,会触发默认的2次重试机制。重试机制有助于提高消息的可靠性。
异步发送:特点是异步发送方式同样需要等待Broker返回确认信息。消息发送成功后,回调函数会被触发,从而得知消息是否成功发送。异步发送方式也可以通过回调机制来处理消息发送失败的情况。
单向发送:特点是单向发送方式不需要Broker返回确认信息。发送方不会收到Broker的确认信息,因此不能保证消息是否成功发送。通常用于不关心消息是否成功到达的场景,如日志收集等。
将三种发送方式进行比较:同步发送是保证消息不丢失的最佳选择。异步发送虽然也能够保证消息不丢失,但需要通过回调函数来处理失败情况。相较于同步发送,异步发送能够提供更高的吞吐量。单向发送不保证消息不丢失,适用于对消息可靠性要求较低的场景。
Broker如何保证接收到的消息不会丢失
broker想要保证收到的消息不会丢失,也需要将消息写入磁盘。先介绍下两种刷盘机制
同步刷盘机制
在同步刷盘机制下,只有当消息被成功写入磁盘后,Broker 才会向 Producer 发送确认消息(ACK)。
这种机制确保了消息的高可靠性,即使 Broker 发生故障,消息也不会丢失。
但由于需要等待磁盘 I/O 完成,同步刷盘机制会影响消息发送的延迟和系统的整体吞吐量。
适用场景:
对于那些对消息可靠性要求极高,且可以接受一定性能损失的场景,同步刷盘是一个很好的选择。
例如,在金融交易、账务结算等场景中,同步刷盘机制可以确保每条消息都被持久化,避免因系统故障导致的数据丢失。
异步刷盘机制
异步刷盘机制允许消息先写入操作系统缓存(PageCache)中,然后再由后台线程异步地将数据刷写到磁盘。
这样做可以充分利用操作系统缓存的优势,提高消息发送的效率。
由于消息不必等到实际写入磁盘就能返回确认,因此可以显著提高系统的吞吐量。
但是,如果在数据从 PageCache 刷写到磁盘之前 Broker 发生故障,可能会导致消息丢失。
适用场景:
对于那些对性能要求较高,但可以容忍一定程度消息丢失风险的场景,异步刷盘机制是一个不错的选择。
例如,在日志收集、监控数据上报等场景中,异步刷盘可以提供更高的吞吐量,即使偶尔丢失一些消息也不会对业务造成严重影响。
所以我们可以将将mq的刷盘机制设置为同步刷盘,就能避免消息丢失。