RocketMQ:一篇学会RocketMQ
MQ概述
MQ,Message Queue,是一种提供消息队列服务的中间件,也称为消息中间件,是一套提供了消息生产、存储、消费全过程API的软件系统。消息即数据。一般消息的体量不会很大。
知道是消息队列,他的规律是先进先出就行,光听概念没啥体验的。
MQ的作用
MQ的最重要的3个作用如下:
限流削峰
MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统被压垮。
说个简单的比喻:全班50个同学同时找老师问问题,老师肯定一时顶不住啊。这时候就需要有个中间人,比如说班长,大家的问题都可以发到班长那里,后面老师可根据自身的情况从班长那处理同学们处理的问题
异步解耦
上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度太高。而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。
异步解耦和限流削峰不一样的是请求的人群;除了用户发送的请求可以先储存在MQ上,系统间的调用也涉及请求发送,也理应可用mq进行中间储存。
举个简单的例子:假设A是用户,B是服务员,C是大厨。A发起请求要一份菜,B请求C去做菜,但由于C还做其他用户的菜,不能马上做A的菜,这时候A和B在干等着。这样肯定是不行,所以得有个中间人D,B告诉D做啥菜,D记起来,C到时候根据D的记录做就完了呗
数据收集
分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术。通过MQ完成此类数据收集是最好的选择。
这时候的MQ的角色就相当于 数据库的功能了。
RocketMQ安装和测试
Rocket安装
下载地址:https://archive.apache.org/dist/rocketmq/4.2.0/rocketmq-all-4.2.0-bin-release.zip
环境搭建
window环境的搭建参考:https://www.ydlclass.com/doc21xnv/distribute/rocketmq/#_2-环境搭建
这里说下遇到的坑:
- 下载完的rocketmq-all-4.2.0-bin-release.zip所在的父级及上级文件夹不要有空格
- 如果遇到【service not available now, maybe disk full, CL: 0.95 CQ: 0.95 INDEX: 0.95, maybe your broker mach】,请删除掉C盘/用户/store文件夹/commitlog文件夹下的文件,然后重新启动name Server和broker即可。
实战
单生产者单消费者消息发送(OneToOne)
生产者
public class Producer {
public static void main(String[] args) throws Exception {
/**
1. 谁来发?
2. 发给谁?
3. 怎么发?
4. 发什么?
5. 发的结果是什么?
6. 打扫战场
**/
//1.创建一个发送消息的对象Producer
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.设定发送的命名服务器地址
producer.setNamesrvAddr("localhost:9876");
//3.1启动发送的服务
producer.start();
//4.创建要发送的消息对象,指定topic,指定内容body
Message msg = new Message("topic1", "hello rocketmq".getBytes("UTF-8"));
//3.2发送消息
SendResult result = producer.send(msg);
System.out.println("返回结果:" + result);
//5.关闭连接
producer.shutdown();
}
}
消费者
public class Consumer {
public static void main(String[] args) throws Exception {
/**
1. 谁来发?
2. 发给谁?
3. 怎么发?
4. 发什么?
5. 发的结果是什么?
6. 打扫战场
**/
//1.创建一个接收消息的对象Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("localhost:9876");
//3.设置接收消息对应的topic,对应的sub标签为任意
consumer.subscribe("topic1","*");
//3.开启监听,用于接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//遍历消息
for (MessageExt msg : list) {
System.out.println("收到消息:"+msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//4.启动接收消息的服务
consumer.start();
System.out.println("接受消息服务已经开启!");
//5 不要关闭消费者!
}
}
- 生产者和消费者topic需要同一个,不然消费者监听不到这个消息。
单生产者多消费者消息发送(OneToMany)
生产者
//1.创建一个发送消息的对象Producer
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.设定发送的命名服务器地址
producer.setNamesrvAddr("localhost:9876");
//3.1启动发送的服务
producer.start();
for (int i = 0; i < 10; i++) {
//4.创建要发送的消息对象,指定topic,指定内容body
Message msg = new Message("topic1", ("hello rocketmq"+i).getBytes("UTF-8"));
//3.2发送消息
SendResult result = producer.send(msg);
System.out.println("返回结果:" + result);
}
//5.关闭连接
producer.shutdown();
消费者之负载均衡模式
消费者1和消费者2(使用同一代码)
//1.创建一个接收消息的对象Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("localhost:9876");
//3.设置接收消息对应的topic,对应的sub标签为任意
consumer.subscribe("topic1","*");
//3.开启监听,用于接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//遍历消息
for (MessageExt msg : list) {
System.out.println("收到消息:"+msg);
System.out.println("消息是:"+new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//4.启动接收消息的服务
consumer.start();
System.out.println("接受消息服务已经开启!");
//5 不要关闭消费者!
PS:广播模式:多个消费者能共享同一个topic中的消息;负载均衡模式:多个消费者竞争同一个topic中的消息。
同一个组内的 消费者,并且监听同一个topic,默认情况下是负载均衡模式。
消费者之广播模式
不同组广播
消费者1
//1.创建一个接收消息的对象Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("localhost:9876");
//3.设置接收消息对应的topic,对应的sub标签为任意
consumer.subscribe("topic1","*");
//3.开启监听,用于接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//遍历消息
for (MessageExt msg : list) {
System.out.println("收到消息:"+msg);
System.out.println("消息是:"+new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//4.启动接收消息的服务
consumer.start();
System.out.println("接受消息服务已经开启!");
//5 不要关闭消费者!
消费者2
//1.创建一个接收消息的对象Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group2");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("localhost:9876");
//3.设置接收消息对应的topic,对应的sub标签为任意
consumer.subscribe("topic1","*");
//3.开启监听,用于接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//遍历消息
for (MessageExt msg : list) {
System.out.println("收到消息:"+msg);
System.out.println("消息是:"+new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//4.启动接收消息的服务
consumer.start();
System.out.println("接受消息服务已经开启!");
//5 不要关闭消费者!
默认情况下,监听同一topic,不同消费组的消费者会共享topic,属于广播模式。
消费者1和消费者2(组内广播)
上面有提到过,组内默认消费模式是 负载均衡,如果想要组内广播,需要设置消费模式,如下:
//1.创建一个接收消息的对象Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("localhost:9876");
//3.设置接收消息对应的topic,对应的sub标签为任意
consumer.subscribe("topic1","*");
//设置当前消费者的消费模式为广播模式(默认模式:负载均衡)
consumer.setMessageModel(MessageModel.BROADCASTING);
//3.开启监听,用于接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//遍历消息
for (MessageExt msg : list) {
System.out.println("收到消息:"+msg);
System.out.println("消息是:"+new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//4.启动接收消息的服务
consumer.start();
System.out.println("接受消息服务已经开启!");
//5 不要关闭消费者!
消息发送方式
同步消息
使用场景:即时性很强,发送消息后,需要马上拿到结果,进行后续的逻辑判断的。
上面演示的例子全是同步消息的发送,只要返回值是SendResult都是,如下:
SendResult result = producer.send(msg);
异步消息
使用场景:即时性较弱,发送消息后,不需要马上拿到结果,可进行后续的逻辑代码的。等结果拿到后再进行方法回调。
异步消息拿到结果的方式在于回调方法,如下:
//1.创建一个发送消息的对象Producer
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.设定发送的命名服务器地址
producer.setNamesrvAddr("localhost:9876");
//3.1启动发送的服务
producer.start();
for (int i = 0; i < 10; i++) {
//4.创建要发送的消息对象,指定topic,指定内容body
Message msg = new Message("topic1", ("hello rocketmq"+i).getBytes("UTF-8"));
//3.2 同步消息
//SendResult result = producer.send(msg);
//System.out.println("返回结果:" + result);
//异步消息
producer.send(msg, new SendCallback() {
//表示成功返回结果
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
//表示发送消息失败
@Override
public void onException(Throwable throwable) {
System.out.println(throwable);
}
});
System.out.println("消息"+i+"发完了,做业务逻辑去了!");
}
//休眠10秒
TimeUnit.SECONDS.sleep(10);
//5.关闭连接
producer.shutdown();
注意:在异步发送消息后进行方法回调,保证生产者得活着,如果关闭了生产者则会默认调用失败回调的方法。
单向消息
使用场景:对返回结果不关心。
producer.sendOneway(msg);
延时消息
使用场景:希望消费者在某段时间后才能进行消费。
Message msg = new Message("topic3",("延时消息:hello rocketmq "+i).getBytes("UTF-8"));
//设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
msg.setDelayTimeLevel(3);
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
//1s为第1登记,5s为第2登记,以此类推
注意:这里的延迟发送是延迟发到broker上面。
批量消息
使用场景:发送一次,需要携带批量消息的情况
List<Message> msgList = new ArrayList<Message>();
Message msg1 = new Message("topic1", ("hello rocketmq1").getBytes("UTF-8"));
Message msg2 = new Message("topic1", ("hello rocketmq2").getBytes("UTF-8"));
Message msg3 = new Message("topic1", ("hello rocketmq3").getBytes("UTF-8"));
msgList.add(msg1);
msgList.add(msg2);
msgList.add(msg3);
SendResult result = producer.send(msgList);
消息过滤
tag过滤
tag:可以理解二级的topic;topic为一级目录,tag为二级目录。
而tag过滤其实很简单,比如说生产者发到 topic 下的 tag1,那么消费者在进行订阅的时候,可以有如下的写法:
consumer.subscribe("topic","*");//topic下的所有topic都进行消费
consumer.subscribe("topic6","tag1");//只消费topic下的tag1
consumer.subscribe("topic6","tag1 || tag2");//消费topic下的tag1或者tog2
属性过滤(SQL过滤)
属性过滤针对的是消息的属性。是的,你没有听错,消息也可以拥有自己的属性,而属性过滤是对消息属性进行筛选出消息的。
前提:需要在broker.conf配置enablePropertyFilter=true
//使用消息选择器来过滤对应的属性,语法格式为类SQL语法
consumer.subscribe("topic7", MessageSelector.bySql("age >= 18"));
consumer.subscribe("topic6", MessageSelector.bySql("name = 'litiedan and age = 16'"));
SpringBoot-RocketMQ
生产者-消费者
配置文件
rocketmq.name-server=localhost:9876
rocketmq.producer.group=demo_producer
生产者
@RestController
public class DemoProducers {
@Autowired
private RocketMQTemplate template;
@RequestMapping("/producer")
public String producersMessage() {
User user = new User("sharfine", "123456789");
template.convertAndSend("demo-topic", user);
//template.convertAndSend("demo-topic:tag1", user);
return JSON.toJSONString(user);
}
}
总结:生产者最重要就是发消息,实际开发我们的目的并不是去创建生产者,设置一堆的参数...而是直接拿到一个发送的工具类进行使用,这个工具就是RocketMQTemplate
消费者
@Service
@RocketMQMessageListener(topic = "demo-topic", consumerGroup = "demo_consumer")
public class DemoConsumers1 implements RocketMQListener<user> {
@Override
public void onMessage(user user) {
System.out.println("Consumers1接收消息:" + demoEntity.toString());
}
}
总结:如果你看过原生的消费者写法,会发现消费者中最重要,变化的那部分就是监听器...所以我们只需要创建监听器,编写监听后的业务逻辑就可以。
其余在springboot中配置
消息发送方式
同步发送
rocketMQTemplate.syncSend("topic1", basicMessage, 2000);
异步发送
rocketMQTemplate.asyncSend("topic9", user, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable throwable) {
System.out.println(throwable);
}
});
单向发送
rocketMQTemplate.sendOneWay("topic9",user);
批量发送
List<Message> msgList = new ArrayList<>();
msgList.add(new Message("topic6", "tag1", "msg1".getBytes()));
msgList.add(new Message("topic6", "tag1", "msg2".getBytes()));
msgList.add(new Message("topic6", "tag1", "msg3".getBytes()));
rocketMQTemplate.syncSend("topic8",msgList,1000);
延时发送
rocketMQTemplate.syncSend("topic9", MessageBuilder.withPayload("test delay").build(),2000,2);
消息过滤
Tag过滤
@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1",selectorExpression = "tag1 || tag2")
SQL过滤
@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1"
,selectorType= SelectorType.SQL92),selectorExpression = "age>18"
消息模式
@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1",messageModel = MessageModel.BROADCASTING)
顺序消息发送
大家有没有注意到一个问题:就是我们在编写简单生产者和消费者的时候,会生产一个问题:
生产者按顺序发送消息,消费者消费时并不按顺序
产生这个问题的原因:topic里默认有4个队列,在发过去的时候是轮询方式进入到4个队列里的;然后消费者在消费的时候,也还是随机的从队列来消费的。随机进,随机出,这就导致了消息的顺序错乱。
在某些业务场景下,消息的消费顺序是不可乱的。因此我们要保证消息的消费顺序是正确的;这个得从两方面入手:
- 保证同一个体(比如同一订单)的业务步骤消息在同一队列中;比如1号订单,有创建,付款,完成3个步骤的消息,保证这3条消息都发送到topic的同一队列上
- 消费者这边要有顺序性的进行消费。消息谁先到先消费
生产者
保证第1点:
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("localhost:9876");
producer.start();
List<OrderStep> orderList = new Producer().buildOrders();
//设置消息进入到指定的消息队列中
for (final OrderStep order : orderList) {
Message msg = new Message("topic1", order.toString().getBytes());
//发送时要指定对应的消息队列选择器
SendResult result = producer.send(msg, new MessageQueueSelector() {
//设置当前消息发送时使用哪一个消息队列
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
//根据发送的信息不同,选择不同的消息队列
//根据id来选择一个消息队列的对象,并返回->id得到int值
long orderId = order.getOrderId();
long mqIndex = orderId % list.size();
return list.get((int) mqIndex);
}
}, null);
System.out.println(result);
}
消费者
保证消费顺序是有序的
//使用单线程的模式从消息队列中取数据,一个线程绑定一个消息队列
consumer.registerMessageListener(new MessageListenerOrderly() {
//使用MessageListenerOrderly接口后,对消息队列的处理由一个消息队列多个线程服务,转化为一个消息队列一个线程服务
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
for (MessageExt msg : list) {
System.out.println(Thread.currentThread().getName()+"。消息:" + new String(msg.getBody())+"。queueId:"+msg.getQueueId());
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
事务
事务消息流程
事务消息只发生在生产者,与消费者无关!目的是为了保证消息不丢失,下面的流程图:
- 发送half消息(把消息发过去了,但暂时不允许被消费)
- 返回ok已接收到消息
- 执行本地事务,将消息保存一份
- 本地提交或回滚,同时告诉broker提交或回滚;如果提交,则消费者可消费,如果回滚,则broker丢弃此消息
事务补偿
如果第2步执行,但第4步迟迟没有响应。则会进行事务补偿,补偿过程:
- broker询问生产者事务结果
- 生产者检测本地事务状态
- 根据事务状态提交或回滚
生产者发送事务消息
//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("group1");
producer.setNamesrvAddr("localhost:9876");
//添加本地事务对应的监听
producer.setTransactionListener(new TransactionListener() {
//正常事务过程
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
//这里编写将数据保存到数据库,然后返回提交或回滚,进而告诉broker结果
return LocalTransactionState.COMMIT_MESSAGE;
}
//事务补偿过程
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
//检查数据的保存情况,然后返回提交或回滚,进而告诉broker结果
return null;
}
});
producer.start();
Message msg = new Message("topic8",("事务消息:hello rocketmq ").getBytes("UTF-8"));
SendResult result = producer.sendMessageInTransaction(msg,null);
System.out.println("返回结果:"+result);
producer.shutdown();
与之前编写的生产者代码有3点不同:
- 使用的生产者不同
- 有本地事务的监听器
- 发送消息方法不同
RocketMQ高级特性(了解即可,代码中可能体现不出来)
消息存储特性
上图为生产者-broker-消费者的一次消息的互动过程。在这个过程中,假设broker宕机了,会发生啥呢?
(1) 发送成功,此时broker宕机,(2)未发送;此时生产者会继续发送消息,导致消息重复发送
(3) 执行这个,此时broker宕机,(4)未执行到;后面broker恢复了,发现迟迟没有(4)的反馈,就继续发消息给消费者,也造成消息重发
rocketmq是这样解决上面的问题的:
(图中数据库DB可看成文件系统)
- 接收到生产者的消息后,先在文件系统保存一份
- 即使宕机然后恢复过来,生产者继续发送,查询文件系统中是否已存在,避免收到多条重复消息
- 由于broker宕机,(5)未执行成功;当broker恢复过来后,会重新执行(4)(5)(6)
死信队列
消息重复消费
消息重复消费有时候是不可避免的,但我们要做到业务幂等。即多次消息消费后的结果,与一次消费结果的相同。这样才能保证业务不错乱