一、MQ概述
推荐数据:RocketMQ技术内幕
1.为什么需要MQ:
使用MQ就是为了解决同步业务中的一些问题,通过异步来解决
2.MQ的好处
a.异步解耦
b.提高响应速度
c.削峰
d.排序保证FIFO
3.MQ能做的事
a.MQ无法接收请求,请求在Java中只有SpringMVC通过Servlet再通过socket去完成
b.MQ也没办法发送请求,我们只能把消息丢给MQ,MQ把消息推送给消费者
4.单点故障
如果MQ只有一台,那么就一定会有单点故障,所以需要集群来解决单点故障
5.RocketMQ
是一款阿里巴巴开源的顶级消息中间件,只要用了MQ那么在业务上一定是异步的
6.四大MQ
二、RocketMQ消息种类
1.单向消息:
不响应任何结果,发送消息发了就发了没有结果通知
Producer:
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建生产者对象,指定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("producer_hello_group");
// 2.连接mq
producer.setNamesrvAddr("127.0.0.1:9876");
// 3.启动生产者
producer.start();
for (int i = 100; i > 0; i--) {
// 4.发送消息
// 4.1.创建消息体
// topic:消息主题 tags:此主题下的二级分类 body:消息的字节
Message message = new Message("hello_topic", "hello_tags", ("Java!"+ i).getBytes(StandardCharsets.UTF_8));
// 4.2.发送消息
SendResult sendResult = producer.send(message);
// 4.3.打印消息发送结果
System.out.println(sendResult);
}
// 5.关闭生产者
producer.shutdown();
}
}
Consumer :
public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建消费者对象,指定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_hello_group");
// 2.连接mq
consumer.setNamesrvAddr("127.0.0.1:9876");
// 4.消费者订阅主题,表示此消费者消费哪个主题的消息,二级分类可以是*
consumer.subscribe("hello_topic", "*");
// 5.注册监听器
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
for (MessageExt msg : msgs) {
String result = new String(msg.getBody(), StandardCharsets.UTF_8);
System.out.println(result);
}
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 3.启动消费者
consumer.start();
}
}
2.同步消息:
发送消息的那一句代码一定要等到MQ返回发送消息的结果
Producer:
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建生产者对象,指定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("producer_hello_group");
// 2.连接mq
producer.setNamesrvAddr("127.0.0.1:9876");
// 3.启动生产者
producer.start();
for (int i = 5; i > 0; i--) {
// 4.发送消息
// 4.1.创建消息体
// topic:消息主题 tags:此主题下的二级分类 body:消息的字节
Message message = new Message("hello_topic", "hello_tags", ("Java!"+ i).getBytes(StandardCharsets.UTF_8));
// 4.2.发送消息
producer.sendOneway(message);
// 4.3.打印消息发送结果
}
// 5.关闭生产者
producer.shutdown();
}
}
Consumer :
public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建消费者对象,指定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_hello_group");
// 2.连接mq
consumer.setNamesrvAddr("127.0.0.1:9876");
// 4.消费者订阅主题,表示此消费者消费哪个主题的消息,二级分类可以是*
consumer.subscribe("hello_topic", "*");
// 5.注册监听器
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
for (MessageExt msg : msgs) {
String result = new String(msg.getBody(), StandardCharsets.UTF_8);
System.out.println(result);
}
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 3.启动消费者
consumer.start();
}
}
3.异步消息:
发送消息的那一句代码的执行结果跟后面的代码的执行是异步的,消息的发送结果也是在回调中获取,类似于Axios异步请求
Producer:
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建生产者对象,指定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("producer_hello_group");
// 2.连接mq
producer.setNamesrvAddr("127.0.0.1:9876");
// 3.启动生产者
producer.start();
for (int i = 10; i > 0; i--) {
// 4.发送消息
// 4.1.创建消息体
// topic:消息主题 tags:此主题下的二级分类 body:消息的字节
Message message = new Message("hello_topic", "hello_tags", ("Java!"+ i).getBytes(StandardCharsets.UTF_8));
// 4.2.发送消息
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("我是雷志穿");
}
@Override
public void onException(Throwable throwable) {
System.out.println("我是雷志串");
}
});
}
// 使用CountDownLatch来等待一段时间后退出
CountDownLatch countDownLatch = new CountDownLatch(1);
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.schedule(() -> {
System.out.println("定时器触发,开始关闭生产者...");
// 关闭生产者
producer.shutdown();
System.out.println("生产者已经关闭...");
countDownLatch.countDown();
}, 10, TimeUnit.SECONDS);
// 等待CountDownLatch计数归零
countDownLatch.await();
System.out.println("程序已经退出...");
scheduler.shutdown();
}
}
Consumer :
public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建消费者对象,指定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_hello_group");
// 2.连接mq
consumer.setNamesrvAddr("127.0.0.1:9876");
// 4.消费者订阅主题,表示此消费者消费哪个主题的消息,二级分类可以是*
consumer.subscribe("hello_topic", "*");
// 5.注册监听器
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
for (MessageExt msg : msgs) {
String result = new String(msg.getBody(), StandardCharsets.UTF_8);
System.out.println(result);
}
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 3.启动消费者
consumer.start();
}
}
4.延迟消息:
可以用作定时器,你发送的消息不会立马执行,在你指定的时间后才会执行
a.生产者发送消息
b.消息进入到MQ的延迟队列,MQ还会准备一个监听器去监听消息到时间没有,如果到了就 把消息重新投递到对应的topic中进行消费
c.RocketMQ默认给我们准备了18个延迟等级,我们在发送延迟消息的时候要指定使用哪个延迟等级,延迟等级如果不够用可以自己改
RocketMQ的消息延迟功能提供了18个预定义的延迟等级,分别是:
- 1s、5s、10s、30s、1m、2m、3m、4m、5m、6m、7m、8m、9m、10m、20m、30m、1h、2h
Producer:
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建生产者对象,指定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("producer_hello_group");
// 2.连接mq
producer.setNamesrvAddr("127.0.0.1:9876");
// 3.启动生产者
producer.start();
for (int i = 5; i > 0; i--) {
// 4.发送消息
// 4.1.创建消息体
// topic:消息主题 tags:此主题下的二级分类 body:消息的字节
Message message = new Message("hello_topic", "hello_tags", ("Java!"+ i).getBytes(StandardCharsets.UTF_8));
// 设置延迟时间
message.setDelayTimeLevel(3);
// 4.2.发送消息
SendResult sendResult = producer.send(message);
// 4.3.打印消息发送结果
System.out.println(sendResult);
}
// 5.关闭生产者
producer.shutdown();
}
}
Consumer :
public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建消费者对象,指定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_hello_group");
// 2.连接mq
consumer.setNamesrvAddr("127.0.0.1:9876");
// 4.消费者订阅主题,表示此消费者消费哪个主题的消息,二级分类可以是*
consumer.subscribe("hello_topic", "*");
// 5.注册监听器
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
for (MessageExt msg : msgs) {
String result = new String(msg.getBody(), StandardCharsets.UTF_8);
System.out.println(result);
}
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 3.启动消费者
consumer.start();
}
}
5.事务消息:
是为了保证分布式事务所提供的消息
Producer:
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建监听器
TransactionListener transactionListener = new TransactionListenerImpl();
// 1.1创建事务生产者对象
TransactionMQProducer producer = new TransactionMQProducer("tran_producer_group");
// 2.连接mq
producer.setNamesrvAddr("127.0.0.1:9876");
// 2.1为生产者设置事务监听器
producer.setTransactionListener(transactionListener);
// 3.启动生产者
producer.start();
// 5.发送事务消息
Message message = new Message("tran_topic", "tran_tags", "发送了事务消息!".getBytes(StandardCharsets.UTF_8));
SendResult sendResult = producer.sendMessageInTransaction(message, null);
System.out.println(sendResult);
// 6.关闭生产者
producer.shutdown();
}
}
TransactionListenerImpl:
public class TransactionListenerImpl implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
System.out.println("执行了业务,保存了本地事务!" + new String(message.getBody(), StandardCharsets.UTF_8) + o.toString());
// 保存成功
return LocalTransactionState.COMMIT_MESSAGE;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("本地事务查询:提交成功!");
return LocalTransactionState.COMMIT_MESSAGE;
}
}
Consumer :
public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建消费者对象,指定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_hello_group");
// 2.连接mq
consumer.setNamesrvAddr("127.0.0.1:9876");
// 4.消费者订阅主题,表示此消费者消费哪个主题的消息,二级分类可以是*
consumer.subscribe("tran_topic", "*");
// 5.注册监听器
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
for (MessageExt msg : msgs) {
String result = new String(msg.getBody(), StandardCharsets.UTF_8);
System.out.println(result);
}
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 3.启动消费者
consumer.start();
}
}
三.RocketMQ原理
RocketMQ最低要求C盘剩余容量是总容量的百分之10,比如100个G那么最低要求10G以上,否则会报错创建不了topic。原因是因为RocketMq的日志是存储到c盘的,默认的。
1.四大角色
a.NameServer
1.职责:是一个注册中心,职责是管理Broker的所有通信地址以及与Broker之间保持心跳,并且与生产者和消费者保持长连接这是为了知道最新的Broker状态
2.在每次Broker发送心跳时,都会记录一次他本次发送的心跳包的时间,并且有一个定时器每隔10S去对比一次当前时间与上次发送心跳的时间有没有超过90S,如果超过那么剔除Broker。最大剔除时间120S
b.Broker
1.职责:MQ所有的消息都存储到Broker中
2.在Broker启动时会根据提前设定好的NameServer地址去找到注册中心,把自己的IP+Port交给NameServer管理,并且有一个定时器每隔30S会向NameServer发送心跳包
c.Producer
1.职责:负责消息的发送
2.在Producer启动的时候会根据地址去NameServer中获取到Broker地址,再根据地址去找到对应的Broker发送消息。在第一次连接之后会与NameServer保持一个长连接,可以获取到最新的Broker地址,同时也会与Broker保持一个30S的心跳,这样是为了保证消息不会丢失但是没办法绝对保证不丢失只能说尽可能减少
d.Consumer
1.与Producer一样,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息
2.Broker中的内容
a.在Broker中一个Broker可以有多个主题
b.主题-topic:topic是主题的意思,可以把他当成是订阅发布中的频道名称
topic:军事频道
c.标签-tag:就是为了让topic下能够更加细分
军事频道:坦克栏目
d.在一个topic下默认有四写四读队列,队列才是Broker中正儿八经放消息的地方,为什么可以保证消息FIFO,因为队列是先进先出的
3.生产者组与消费者组
a.生产者组
1.如何确定是否为一组:生产者组是有组名+topic来确定为一组
2.为什么要有生产者组的概念:因为如果没有组那么生产者出现意外死了的时候,那么消息就没办法继续发送出去了,如果有组那么P1死亡,P2检测到了会把P1没有发送完成的消息接着发送,这是为了做容错尽可能保证消息不丢失
b.消费者组
1.如何来确定是否为一组:消费者组名+topic+tag才能确定为一组
2.为什么要有消费者组的概念
2.1.为了做容错
2.2为了负载均衡,让多个消费者一起消费
c.如果不指定那么都在mq的默认分组中,没办法提供容错
4.消费者消费消息的类型
1.push:MQ主动把消息推送给消费者
2.pull:消费者主动去向MQ拉取消息,已淘汰
5.MessageQueue
a.一个Queue在一个消费者组中只能有一个消费者,可以被多个消费者组消费。一个消费者可以有多个Queue
b.consumer的数量要小于等于Queue的数量,这是为了保证consumer的资源不会空闲浪费
c.一个消费者组最好只订阅一个topic,不要订阅不同的topic
d.如果消费者组下的某个消费者死了,那么要重新分配Queue与consumer的关系
6.消息消费模式
a.广播:在不同的消费者组下只能被一个consumer消费
b.集群:只能被一个consumer消费,不管什么情况下,保证消息不会被重复消费
7.topic的创建时机
a.生产环境
1.生产环境严禁使用自动状态,生成环境要提前在可视化界面先创建好再启动项目
2.原因:因为自动创建是连到哪个Broker在那个Broker中创建topic,其他集群Broker下没有,这会导致集群失效,所以生成环境不允许使用
b.开发环境
1.我们可以在启动Broker的时候指定autoCreateTopicEnable=true,那么生产者在启动时就会去自动帮我们创建此topic
2.在第一次发送消息的时候,生产者发送消息到Broker,Broker发现没有此topic,会去检测是否开启enable=true,如果没有开启报错找不到此topic,如果已开启,那么会告诉生产者要创建topic,他会根据TBW102这个MQ默认启动就有的topic拷贝一份变为你要创建的topic,往这里面发送消息
8.消费者消费消息的负载均衡算法
a.轮询:平均策略
b.环形平均:在环上面有序一人一次
c.一致性HASH环策略:计算消费者的HASH值,落在一个环上面,再计算Queue的HASH值顺时针归属给消费者
d.同机房:就近原则
四.事务消息
a.事务的种类
1.本地事务:涉及到一个数据库的事务我们叫做本地事务,本地事务的实现靠的是数据库自己的事务,但是他也仅支持管理自己的数据库的事务
2.分布式事务:当一个方法操作的是多个数据库事,要保证的事务叫做分布式事务
b.事务消息的流程
1.生产者方向mq发送一个半消息,半消息的意思是给mq,但是mq不给消费者消费
2.mq会响应一个半消息发送成功
3.本地事务如果接收到半消息发送成功,那么会调用本地事务执行的方法,这个方法不在主代码里面在mq的事务消息监听器里面
4.当消息发送成功之后,MQ会去调用监听器里面的本地事务方法进行执行,根据返回值结果判断刚才的半消息是否需要传递给消费者方,如果是成功那么传递,如果是失败那么删除此消息
5.在监听器类里面还有一个本地事务检测方法,如果检测到本地事务没有成功那么会响应失败,如果检测到成功那么会响应成功,此时半消息投递给消费者
c.如何保证消息不重复消费,消息幂等性
1.消息为什么会重复消费:因为RocketMQ有消息的ACK机制,如果消费者没有响应成功那么MQ就会一直发,直到16此之后此消息会进入到一个特殊队列-死信队列。如果由于网络波动,明明成功了但是没有响应,他也会认为你失败了,那么就会重复发送,也就导致重复消费
2.解决方案
2.1.进入到消费者的时候就根据消息的唯一Id去查询日志数据库看是否存在
2.1.1不存在:说明第一次消费,加锁然后去保存日志为处理中,处理成功之后改为消费成功。处理失败改为消费失败
2.1.2存在:查看状态
处理中:响应失败,让MQ再发
处理失败:继续消费根据结果修改日志状态
消费成功:响应成功
3.日至表的消息要定期做清理
d.如何保证消息不丢失
1.消息进入到MQ之后已经帮我们做持久化了
2.还有ACK机制来保证消息不会被误删除
e.如果保证消息一定消费成功
1.在做不重复消费的时候就做了,但是有的消息就是处理不成功,直到16次进入到死信队列,那么此时就要人工干预
详细了解参考: