MQ简介
MQ全称为Message Queue(消息队列),是在消息传输中保存消息的容器。多用于分布式系统之间进行通信。
Queue:数据结构的一种,特征为”先进先出“
优势
-
应用解耦
假设我现在还要添加一个大数据系统 就不需要修改上游系统的逻辑。
-
异步提速
多个消息队列下游系统并行执行逻辑比顺序执行逻辑要快
-
削峰填谷
流量值过大的时候可以缓存消息,让下游系统平稳处理数据,避免流量过大系统崩溃
劣势
-
系统可用性降低
一旦MQ宕机,就会对所有下游系统造成影响。如何保证MQ高可用? -
复杂度提高
以前的系统是同步的远程调用,现在通过MQ调用,如何保证重复消费?怎么处理消息丢失情况?如何保证消息传递的顺序性 -
一致性问题
A系统处理完业务,通过MQ给A,B,C三个系统发送消息,如果B、C系统处理成功但是A系统异常,如何保证给消息数据处理的一致性?
RocketMQ
RocketMQ是阿里开源的一款非常优秀的中间件产品,后捐赠给apache基金会的顶级项目,能承受住双十一极致的场景压力(双十一峰值达到万亿级)
基础感念
Windows安装
- 下载rocketmq4.8.0版本
- 配置环境变量
变量名:ROCKETMQ_HOME
变量值:环境路径
注:部分电脑重启环境变量才会生效
3. 启动nameserve:
D:\rocketmq-all-4.8.0-bin-release\bin>start mqnamesrv.cmd
- 启动broker:
#autoCreateTopicEnable=true 自动创建topic
D:\rocketmq-all-4.8.0-bin-release\bin>start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true -c ../conf/broker.conf
Linux安装
自行百度
Java整合RocketMQ
入门案例
生产者
- 引入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
- 编写代码
public static void main(String[] args) throws Exception {
//1.谁来发
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.发给谁
producer.setNamesrvAddr("127.0.0.1:9876");
//3.怎么发
//4.发什么
producer.start();
String content = "hello rocketmq";
Message message = new Message("topic1", "tag1", content.getBytes());
SendResult send = producer.send(message);
//5.发送的结果是什么
System.out.println(send);
//6.打扫战场
//关闭连接
producer.shutdown();
}
消费者
- 引入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
- 编写代码
public static void main(String[] args) throws Exception {
//1.谁来收
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.从哪里收消息
consumer.setNamesrvAddr("127.0.0.1:9876");
//3.监听那个消息队列
consumer.subscribe("topic1", "*");
//处理业务,注册实时监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt data : list) {
byte[] body = data.getBody();
System.out.println(new String(body));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
多消费模式
这里存在一个问题 为了保证消费者系统高可用部署了集群,那么消费者会处理(集群*消息数)翻倍的消息数量吗
生产者
生产者发送十条消息
//1.谁来发
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.发给谁
producer.setNamesrvAddr("127.0.0.1:9876");
//3.怎么发
//4.发什么
producer.start();
for (int i = 0; i < 10; i++) {
String content = "hello rocketmq";
Message message = new Message("topic2", "tag1", content.getBytes());
SendResult send = producer.send(message);
//5.发送的结果是什么
System.out.println(send);
}
//6.打扫战场
//关闭连接
producer.shutdown();
}
消费者
启动两个消费者服务
public static void main(String[] args) throws Exception {
//1.谁来收
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.从哪里收消息
consumer.setNamesrvAddr("127.0.0.1:9876");
//3.监听那个消息队列
consumer.subscribe("topic2", "*");
//处理业务,注册实时监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt data : list) {
byte[] body = data.getBody();
System.out.println(new String(body));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
消费者1
消费者2
为什么会这样呢,这样就涉及到group的概念?
group的概念
group的作用就是用来做负载均衡,假设生产者生产了10条消息,处在相同group的消费者就会平分消息,处在不同group的消费者就会接收全量的消息
上图:Consumer1接受5条,Consumer2接受5条,Consumer3接受10条
当然也可以不修改group来使处在相同组的消费者接受全量的消息
//默认是负载均衡模式,MessageModel.BROADCASTING就变成全量接受
consumer.setMessageModel(MessageModel.BROADCASTING);
同步消息
特征:即使性较强,重要的消息,且必须有回执的消息,例如短信,通知
SendResult send = producer.send(message);
异步消息
特征:即时性较弱,但需要有回执的消息,例如订单中的某些信息,吞吐量较高
//注:异步消息不能关闭mq的连接
producer.send(message, new SendCallback() {
//异步回调成功
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
//异步回调失败
public void onException(Throwable throwable) {
System.out.println(throwable.toString());
}
});
单向消息
特征:不需要回执的消息,例如日志消息
producer.sendOneway(message);
延时消息
//1(1s) 2(5s) 3(10s) 4(30s) .. 自行百度等级对应的具体时间
message.setDelayTimeLevel(1);
批量消息
每次生产者和MQ建立连接都是非常消耗性能的(要经历三握四挥)。所以就需要建立一次连接传输全量的数据
List<Message> data = new ArrayList<Message>();
for (int i = 0; i < 10; i++) {
String content = "hello rocketmq";
Message message1 = new Message("topic5", "tag1", content.getBytes());
data.add(message1);
}
producer.send(data);
注:
- 批量消息应该有相同的topic
- 相同的对象字段类型
- 不能是延时消息
- 消息内容总长度不超过4M
Tag过滤
//接收全部
consumer.subscribe("topic2", "*");
//接受单个
consumer.subscribe("topic2", "tag1");
//接受多个tag
consumer.subscribe("topic2", "tag1 || tag2");
SQL过滤
像写SQL一样在消费方过滤不想要的数据
- 开启SQL过虑支持(默认不开启)
#找到conf/broker.conf 追加以下配置
enablePropertyFilter=true
- 重启broker
start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true -c ../conf/broker.conf
- 生产者设置Message
public static void main(String[] args) throws Exception {
//1.谁来发
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.发给谁
producer.setNamesrvAddr("127.0.0.1:9876");
//3.怎么发
//4.发什么
producer.start();
for (int i = 0; i < 10; i++) {
String content = "hello rocketmq"+i;
Message message = new Message("topic10", "tag1", content.getBytes());
//设置sql过滤的追加属性
message.putUserProperty("age", Integer.toString(i));
SendResult send = producer.send(message);
//5.发送的结果是什么
System.out.println(send);
}
//6.打扫战场
//关闭连接
producer.shutdown();
}
- 消费者设置
public static void main(String[] args) throws Exception {
//1.谁来收
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.从哪里收消息
consumer.setNamesrvAddr("127.0.0.1:9876");
//3.监听那个消息队列 SQL过滤
consumer.subscribe("topic10", MessageSelector.bySql("age >= 5 "));
//处理业务,注册实时监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt data : list) {
byte[] body = data.getBody();
System.out.println(new String(body));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
这里可以发现MQ已经过滤掉了age>=5 的数据
SpringBoot整合
- Idea创建项目
- 勾选SpringMVC改为Web项目
- 添加RocketMQ依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.3</version>
</dependency>
- 编写配置文件
rocketmq:
# 配置nameserver地址
name-server: localhost:9876
# 配置生产者组名
producer:
group: group1
- 编写生产者代码
@RestController
public class SendController {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@GetMapping(value = "test")
public String send() {
Map map = new HashMap();
map.put("name", "范源鑫");
map.put("age", "18");
rocketMQTemplate.convertAndSend("topic1:tag1", map);
return "success";
}
}
- 编写消费者代码
@Service
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.TAG, selectorExpression = "tag1", consumerGroup = "group1")
public class Consumer implements RocketMQListener<Map> {
@Override
public void onMessage(Map map) {
System.out.println("我是消费者A" + map.toString());
}
}
消息类型案例
生产者的消息类型案例
//同步消息
SendResult topic1 = rocketMQTemplate.syncSend("topic1:2", map);
//异步消息
rocketMQTemplate.asyncSend("topic1:tag2", map, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("处理成功" + sendResult.toString());
}
@Override
public void onException(Throwable throwable) {
System.out.println("处理失败" + throwable.getMessage());
}
});
//发送单向消息
rocketMQTemplate.sendOneWay("topic1:2", map);
//发送延时消息
rocketMQTemplate.syncSend("topic1:2", MessageBuilder.withPayload(map).build(), 1, 2);
//发送批量消息
ArrayList<Map> list = Lists.newArrayList();
list.add(map);
SendResult data = rocketMQTemplate.syncSend("topic1:2", list);
消费者根据SQL过滤
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.SQL92, selectorExpression = "age >= 10 ", consumerGroup = "group1")
消费者默认的负载改为广播模式
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.SQL92, selectorExpression = "age >= 10 ", consumerGroup = "group1",messageModel = MessageModel.BROADCASTING)
消费顺序
假设一个订单的流程 是 创建->付款->完成 之后用消息队列发送会产生什么影响呢
- 创建生产者模拟数据
private void getData() {
ArrayList<OrderStep> objects = Lists.newArrayList();
objects.add(new OrderStep(1L, "创建"));
objects.add(new OrderStep(2L, "创建"));
objects.add(new OrderStep(1L, "付款"));
objects.add(new OrderStep(3L, "创建"));
objects.add(new OrderStep(2L, "付款"));
objects.add(new OrderStep(3L, "付款"));
objects.add(new OrderStep(2L, "完成"));
objects.add(new OrderStep(3L, "完成"));
objects.add(new OrderStep(1L, "完成"));
return objects;
//同步发送消息
for (OrderStep datum : data) {
SendResult topic1 = rocketMQTemplate.syncSend("topic1:tag2", datum);
}
}
- 创建两条消费者A,B
@Service
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.TAG, selectorExpression = "tag2", consumerGroup = "group1")
public class ConsumerA implements RocketMQListener<OrderStep> {
@Override
public void onMessage(OrderStep map) {
System.out.println("消费者A" + map.toString());
}
}
@Service
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.TAG, selectorExpression = "tag2", consumerGroup = "group1")
public class ConsumerB implements RocketMQListener<OrderStep> {
@Override
public void onMessage(OrderStep map) {
System.out.println("消费者B" + map.toString());
}
}
运行会发现订单2先完成了。这是什么情况呢?
是因为一个broker默认有4个队列(可修改)消息发送时每个消息可能会被分配到不同的队列中 导致多个消者消费时会可能先读取到队列2的订单完成订单。那怎么保证顺序消费呢,就是在生产者吧同个订单分配到相同的队列内部就可以了
解决:将订单相同的放进同一个队列中
- 发送同步消息
//通过Orderly接口实现顺序发送 关键点是hashcode参数 hashcode参数相同分配的队列就相同
//这里是根据订单的id当作hashcode
SendResult sendResult = rocketMQTemplate.syncSendOrderly("topic1:tag2", orderStep, hashcode);
- 消费方设置同步消息模式(consumeMode = ConsumeMode.ORDERLY)
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.TAG, selectorExpression = "tag2", consumerGroup = "group1",consumeMode = ConsumeMode.ORDERLY)
事务消息
- 红色是正常事务
- 蓝色为事务补偿过程
注:事务和消费者没有关系
状态:
提交状态:允许进入队列,此消息与非事务消息无区别(属于第四步的提交操作)
回滚状态:不允许进入队列,此消息等同于未发送过消息(属于第四步的回滚操作)
中间状态:完成了half消息的发送,没有对mq进行二次确认(broker主动询问生产者消息事务的状态)
- 创建本地事务消息监听类
package com.fyx.rocketmq.transaction;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Service;
@Service
@RocketMQTransactionListener(txProducerGroup = "tx_order")
public class LocalTrsaction implements RocketMQLocalTransactionListener {
//正常事务处理
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
if (true) {
//业务成功把消息提交到broker
System.out.println("事务正常提交");
return RocketMQLocalTransactionState.COMMIT;
}else{
//业务失败就回滚不发送消息到broker
System.out.println("事务异常提交");
return RocketMQLocalTransactionState.ROLLBACK;
}
}
//事务补偿处理
//正常事务处理返回UNKNOWN时候进入这个方法进行重新进行业务操作
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
if (true) {
//业务成功把消息提交到broker
System.out.println("事务正常提交");
return RocketMQLocalTransactionState.COMMIT;
}else{
//业务失败就回滚不发送消息到broker
System.out.println("事务异常提交");
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
- 生产者发送事务消息
Message<OrderStep> message = MessageBuilder.withPayload(new OrderStep(1L, "创建")).build();
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction("tx_order", "tx_topic:" + "tx_tag", message, message.getPayload().getOrderId());
String localTXState = sendResult.getLocalTransactionState().name();
System.out.println("事务名:"+localTXState);
- 消费者接受消息
@Service(value = "consumer3")
@RocketMQMessageListener(topic = "tx_topic", selectorType = SelectorType.TAG, selectorExpression = "tx_tag", consumerGroup = "tx_group")
public class Consumer implements RocketMQListener<OrderStep> {
@Override
public void onMessage(OrderStep map) {
System.out.println(map.toString());
}
}
消息重试
当消费者处理完业务没有向broker正常返回处理成功的状态后,boker会对这一条消息进行重复消费消息
大致分为两种消息重试机制,顺序消息和无序消息
- 顺序消息(同步消息)
- 当消费者消费失败后,rocketmq会自动进行消息重试(每次间隔时间1秒)
注:应用会出现消息消费被阻塞的情况,因此,要对顺序消息的消费情况给进行监控,避免阻塞现象的发生
- 无序消息(普通消息、定时消息、延时消息、事务消息)
- 无序消息重试仅适用于负载均衡(集群)模型下的消息消费,不适用于广播模式下的消息消费
- 为保障无序消息的消费,MQ设定了合理的消息重试间隔时常
死信队列
当消息重试达到了指定次数(默认16次)后,MQ将无法被正常消费的消息称为死信消息,死信消息不会被直接抛弃,而是保存到了一个全新的队列中,该队列称为死信队列(Dead-Letter Queue)
死信队列处理:在监控平台中查找死信队列,获取死信的MessageId,然后通过id对死信进行精准消费
重复消费
可能因为网络闪断、生产者宕机、broker重启、订阅方应用重启 会对同一笔订单做多次业务操作(例:如对同一笔订单扣除多次库存)
解决方案:
- 使用业务id作为消息的key使用
- 在消费消息时,客户端对key做判断,未使用过放行,使用过抛弃