目录
RocketMQ优点
解耦、削峰、数据分发
RocketMQ缺点
- 系统可用性降低:系统引入外部依赖越多,系统稳定性越差,一旦mq宕机,就会对业务造成影响。如何保证MQ高可用?
- 系统复杂度提高:mq的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过mq进行异步调用。如何保证 消息没有被重复消息?怎么处理消息丢失情况?如何保证消息传递的顺序性?
- 一致性问题:A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败。如何保证消息数据处理的一致性?
RocketMQ 集群搭建
RocketMQ角色
- producer:消息的发送者,会与nameserver(随机选择)建立长链接,发送topic消息定期从nameserver获取broker路由信息,并向提供topic master建立长链接,且定时向master broker发送心跳。producer完全无状态,可集群部署
- consumer:消息接受者,会与nameserver(随机选择)建立长链接,定期从namesever获取topic路由信息,并向提供topic服务的master、salve建立长链接,且定时向master、salve发送心跳。consumer既可以从master订阅消息,也可以从slave订阅消息,订阅规则由broker配置决定。
- broker:暂存和传输消息,broker分为 master和slave,一个master可以对应多个slave,但是一个slave只能对应一个master,master也可以比部署多个。每个broker与namesever集群中的所有节点建立长链接,定时注册topic信息到所有nameserver。
- namesever:管理broker,namesever集群是无状态的节点,其中的每个节点broker都会向每个namesever上报信息
- topic:区分消息的种类;一个发送者可以发送消息给一个或者多个topic;一个消息的接收者可以订阅一个或者多个topic消息
- message queue:相当于topic的分区,用于并行发送和接收消息
集群模式
单 master 模式
- 也就是只有一个 master 节点,称不上是集群,一旦这个 master 节点宕机,那么整个服务就不可用,适合个人学习使用。
多 master 模式
- 多个 master 节点组成集群,单个 master 节点宕机或者重启对应用没有影响。
- 优点:所有模式中性能最高
- 缺点:单个 master 节点宕机期间,未被消费的消息在节点恢复之前不可用,消息的实时性就受到影响。
- 注意:使用同步刷盘可以保证消息不丢失,同时 Topic 相对应的 queue 应该分布在集群中各个节点,而不是只在某各节点上,否则,该节点宕机会对订阅该 topic 的应用造成影响。
多 master 多 slave 异步复制模式
- 在多 master 模式的基础上,每个 master 节点都有至少一个对应的 slave。
- master 节点可读可写,但是 slave 只能读不能写,类似于 mysql 的主从模式。
- 优点: 在 master 宕机时,消费者可以从 slave 读取消息,消息的实时性不会受影响,性能几乎和多 master 一样。
- 缺点:使用异步复制的同步方式有可能会有消息丢失的问题。
多 master 多 slave 同步双写模式
- 同多 master 多 slave 异步复制模式类似,区别在于 master 和 slave 之间的数据同步方式。
- 优点:同步双写的同步模式能保证数据不丢失。
- 缺点:发送单个消息 RT 会略长,性能相比异步复制低10%左右。
- 刷盘策略:同步刷盘和异步刷盘(指的是节点自身数据是同步还是异步存储)
- 同步方式:同步双写和异步复制(指的一组 master 和 slave 之间数据的同步)
- 注意:要保证数据可靠,需采用同步刷盘和同步双写的方式,但性能会较其他方式低。
双主双从集群搭建
- 总体构架
- 集群工作流程
- 启动namsever,namesever起来后监听端口,等待broker、producer、consumer连上来,相当于一个路由管理控制中心。
- broker启动,跟所有的namesever保持长链接,定时发送心跳包,心跳包中包含当前broker信息(ip+端口号等)以及存储所有topic信息。注册成功后,namesever集群中就有topic跟broker的映射关系。
- 发送消息前,先创建topic,创建topic时需要指定该topic要存储在哪些broker上,也可以在发送消息时自动创建topic
- producer发送消息,启动时先跟nameserver集群中的其中一台建立链接,并从namesever中获取当前发送topic存在哪些broker上,轮询从队列列表中选择一个队列,然后与队列所在broker建立长链接从而向broker发消息。
- consumer跟producer类似,跟其中一台nameserver建立长链接,获取当前发送topic存在哪些broker上,然后跟broker建立通道,开始消费消息。
3. 双主双从集群搭建
搭建过程可以参考如下链接: https://www.jianshu.com/p/66afa5ae41ba
RocketMQ消息发送样例
- 导入mq客户端依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.0</version>
</dependency>
- 消息发送者步骤分析
1.创建消息生产者producer,并制定生产者组名
2.制定NameServer地址
3.启动producer
4.创建消息对象,制定主题Topic、tag和消息体
5.发送消息
6.关闭生产者produce
- 消息消费者步骤分析
1.创建消费者consumer,制定消费者组名
2.指定Nameserver地址
3.订阅主题Topic和tag
4.设置回调函数、处理消息
5.启动消费者consumer
基本样例
-
发送同步消息:可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知、短信通知
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.util.concurrent.TimeUnit;
public class SyncProducer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.启动producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.创建消息对象,指定主题Topic、Tag和消息体
/*
参数1:消息主题Topic
参数2:消息Tag
参数3:消息内容
*/
Message msg = new Message("base","tag1",("hello world"+i).getBytes());
// 5.发送消息结果包含 发送状态 消息id 消息接收队列id等
SendResult result = producer.send(msg);
System.out.println("发送结果"+result);
// 线程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6关闭生产者producer
producer.shutdown();
}
}
- 发送异步消息:
通常对响应时间敏感的业务场景,即发送不能容忍长时间地等待broker的响应
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.util.concurrent.TimeUnit;
public class AsyncProducer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.启动producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.创建消息对象,指定主题Topic、Tag和消息体
/*
参数1:消息主题Topic
参数2:消息Tag
参数3:消息内容
*/
Message msg = new Message("base","tag2",("hello world"+i).getBytes());
// 5.发送消息结果包含 发送状态 消息id 消息接收队列id等
producer.send(msg, new SendCallback() {
// 发送成功回调函数
public void onSuccess(SendResult sendResult) {
System.out.println("发送结果:"+sendResult);
}
// 发送失败回调函数
public void onException(Throwable e) {
System.out.println("发送异常"+e);
}
});
// 线程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6关闭生产者producer
producer.shutdown();
System.out.println("发送完成");
}
}
- 发送单向消息:
这种方式主要用于不特别关心发送结果的场景,例如日志发送
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.util.concurrent.TimeUnit;
public class OnewayProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
// 1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.启动producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.创建消息对象,指定主题Topic、Tag和消息体
/*
参数1:消息主题Topic
参数2:消息Tag
参数3:消息内容
*/
Message msg = new Message("base","tag3",("hello world,单向消息"+i).getBytes());
// 5.发送单向消息
producer.sendOneway(msg);
System.out.println("发送单向消息");
// 线程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6关闭生产者producer
producer.shutdown();
}
}
- 消费者
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import java.util.List;
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建消费者Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.订阅主题Topic和Tag
consumer.subscribe("base","tag1");
// 消费模式:默认是负载均衡模式,还有一种是广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
// 4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息内容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.启动消费者consumer
consumer.start();
}
}
- 广播消费模式
- 集群消费模式
顺序消息
消息有序指的是可以按照消息的发送顺序来消费(FIFO)RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。
顺序消费的原理解析,在默认的情况下消息发送采取round robin轮询方式把消息发送到不同的queue(区分队列);而消费消息的时候从多个queue上拉去消息,这种情况发送和消费不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息是有序的
public class OrderStep {
private long orderId;
private String desc;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public long getOrderId() {
return orderId;
}
public void setOrderId(long orderId) {
this.orderId = orderId;
}
@Override
public String toString() {
return "OrderStep{" +
"orderId=" + orderId +
", desc='" + desc + '\'' +
'}';
}
public static List<OrderStep> buildOrders() {
// 1039L 创建 付款 推送 完成
// 1065L 创建 付款
// 7235L 创建 付款
List<OrderStep> orderList = new ArrayList<OrderStep>();
OrderStep demo = new OrderStep();
demo = new OrderStep();
demo.setOrderId(6L);
demo.setDesc("创建");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(6L);
demo.setDesc("付款");
orderList.add(demo);
demo.setOrderId(6L);
demo.setDesc("推送");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(6L);
demo.setDesc("完成");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(7L);
demo.setDesc("推送");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(7L);
demo.setDesc("完成");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(9L);
demo.setDesc("创建");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(9L);
demo.setDesc("付款");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(9L);
demo.setDesc("推送");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(9L);
demo.setDesc("完成");
orderList.add(demo);
return orderList;
}
}
- 生产者
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.启动producer
producer.start();
// 构建消息集合
List<OrderStep> orderStepList = OrderStep.buildOrders();
// 发送消息
for (int i = 0; i < orderStepList.size(); i++) {
String body = orderStepList.get(i)+"";
Message message = new Message("OrderTopic","Order","i"+i,body.getBytes());
/**
* 参数1:消息对象
* 参数2:消息队列的选择器
* 参数3:选择队列的业务标识(订单id)
*/
SendResult send = producer.send(message, new MessageQueueSelector() {
/**
*
* @param list 队列集合
* @param message 消息对象
* @param o 业务标识的参数
* @return
*/
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
long orderId = (Long) o;
long index = orderId % list.size();
return list.get((int) index);
}
}, orderStepList.get(i).getOrderId());
System.out.println("发送结果:"+send);
}
producer.shutdown();
}
}
- 消费者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建消费者Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.订阅主题Topic和Tag
consumer.subscribe("OrderTopic","*");
// 4.注册消息监听器
consumer.registerMessageListener(new MessageListenerOrderly() {
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
for (MessageExt messageExt : list) {
System.out.println("线程名称:["+Thread.currentThread().getName()+"]消费消息:"+new String(messageExt.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 5.启动消费者
consumer.start();
System.out.println("消费者启动");
}
}
延时消息
- 生产者
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.启动producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.创建消息对象,指定主题Topic、Tag和消息体
/*
参数1:消息主题Topic
参数2:消息Tag
参数3:消息内容
*/
Message msg = new Message("DelayTopic","tag1",("hello world"+i).getBytes());
// 设置延迟时间 RocketMq并不支持任意时间的延时,需要试着几个固定的延时等级
// 从1s到2h分别对应着等级1到18 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
msg.setDelayTimeLevel(2);
// 5.发送消息结果包含 发送状态 消息id 消息接收队列id等
SendResult result = producer.send(msg);
System.out.println("发送结果"+result);
// 线程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6关闭生产者producer
producer.shutdown();
}
}
- 消费者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建消费者Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.订阅主题Topic和Tag
consumer.subscribe("DelayTopic","tag1");
// 消费模式:默认是负载均衡模式,还有一种是广播模式
// consumer.setMessageModel(MessageModel.BROADCASTING);
// 4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息内容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println("消息id:【"+messageExt.getMsgId()+"】,延时时间:"+(System.currentTimeMillis()-messageExt.getStoreTimestamp()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.启动消费者consumer
consumer.start();
System.out.println("消费者启动");
}
}
批量消息
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOk,
而且不能是延时消息。此外,这一批消息的总大小不应超过4MB
如果超过4MB,这时候最好把消息进行分割
- 生产者
public class Producer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
// 1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.启动producer
producer.start();
List<Message> msgs = new ArrayList<Message>();
// 4.创建消息对象,指定主题Topic、Tag和消息体
/*
参数1:消息主题Topic
参数2:消息Tag
参数3:消息内容
*/
Message msg1 = new Message("batchTopic","tag1",("hello world"+1).getBytes());
Message msg2 = new Message("batchTopic","tag1",("hello world"+2).getBytes());
Message msg3 = new Message("batchTopic","tag1",("hello world"+2).getBytes());
msgs.add(msg1);
msgs.add(msg2);
msgs.add(msg3);
// 5.发送消息结果包含 发送状态 消息id 消息接收队列id等
SendResult result = producer.send(msgs);
System.out.println("发送结果"+result);
// 线程睡眠1秒
TimeUnit.SECONDS.sleep(1);
// 6关闭生产者producer
producer.shutdown();
}
}
- 消费者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建消费者Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.订阅主题Topic和Tag
consumer.subscribe("batchTopic","*");
// 消费模式:默认是负载均衡模式,还有一种是广播模式
// consumer.setMessageModel(MessageModel.BROADCASTING);
// 4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息内容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.启动消费者consumer
consumer.start();
System.out.println("消费者启动");
}
}
过滤消息
- 生产者
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.启动producer
producer.start();
for (int i = 0; i < 3; i++) {
// 4.创建消息对象,指定主题Topic、Tag和消息体
/*
参数1:消息主题Topic
参数2:消息Tag
参数3:消息内容
*/
Message msg = new Message("filterTopic","tag1",("hello world"+i).getBytes());
// 5.发送消息结果包含 发送状态 消息id 消息接收队列id等
SendResult result = producer.send(msg);
System.out.println("发送结果"+result);
// 线程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6关闭生产者producer
producer.shutdown();
}
}
- 消费者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建消费者Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.订阅主题Topic和Tag
consumer.subscribe("filterTopic","tag1 || tag2");
// 消费模式:默认是负载均衡模式,还有一种是广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
// 4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息内容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.启动消费者consumer
consumer.start();
System.out.println("消费者启动");
}
}
事务消息
这张图说明了事务消息的大致方案,分为两个逻辑:正常事务消息的发送及提交、事务消息的补偿流程
1、事务消息发送及提交:
- 发送消息(half消息)
- 服务端响应消息写入结果
- 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)
- 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
2、补偿流程:
- 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
- Producer收到回查消息,检查回查消息对应的本地事务的状态
- 根据本地事务状态,重新Commit或者Rollback
- 补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
3、事务消息状态
事务消息共有三种状态,提交状态、回滚状态、中间状态:
- TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息
- TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费
- TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态
注意事项:要使用集群实现事务消息,集群必须是异步的2m-2s-async
4、生产者
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者producer,并制定生产者组名 事务生产者
TransactionMQProducer producer = new TransactionMQProducer("group5");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 生产者监听器
producer.setTransactionListener(new TransactionListener() {
/**
* 在该方法中执行本地事务 第二步
* @param message
* @param o
* @return
*/
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
if (StringUtils.equals("taga",message.getTags())){
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.equals("tagb",message.getTags())){
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if (StringUtils.equals("tagc",message.getTags())){
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
/**
* 该方法是mq进行消息事务状态回查 第三步
* @param messageExt
* @return
*/
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("消息的tag:"+messageExt.getTags());
return LocalTransactionState.COMMIT_MESSAGE;
}
});
// 3.启动producer
producer.start();
String[] tags = {"taga","tagb","tagc"};
for (int i = 0; i < 3; i++) {
// 4.创建消息对象,指定主题Topic、Tag和消息体
/*
参数1:消息主题Topic
参数2:消息Tagf
参数3:消息内容
*/
Message msg = new Message("transactionTopic",tags[i],("hello world"+i).getBytes());
// 5.发送消息结果包含 发送状态 消息id 消息接收队列id等 第一步
SendResult result = producer.sendMessageInTransaction(msg,null);
System.out.println("发送结果"+result);
// 线程睡眠1秒
TimeUnit.SECONDS.sleep(3);
}
// 6关闭生产者producer
// producer.shutdown();
}
}
5、消费者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建消费者Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group5");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.订阅主题Topic和Tag
consumer.subscribe("transactionTopic","*");
// 4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息内容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.启动消费者consumer
consumer.start();
System.out.println("消费者启动");
}
}
6、使用限制
- 事务消息不支持批量或者延迟消息
- 对于UNKNOW的消息,RocketMQ会尝试回调15次,超过15次会扔掉消息,并记录错误日志;可以修改broker的transactionCheckMax属性来改变这个次数,如果不想扔掉消息(或者想做一些其他的操作),也可以重写AbstractTransactionCheckListener类来修改默认的扔掉消息的行为。
- 如果返回状态为UNKNOW,那么会每 60秒 执行一次事务状态检查回调函数,这个时间可以通过修改broker的transactionTimeout参数来改变,或者在发送消息的时候修改用户参数:CHECK_IMMUNITY_TIME_IN_SECONDS,这个参数优先于transactionTimeout。
- 事务消息可能被检查或者消费多次?
- 反正RocketMQ开源版本还是会丢数据,如果你一点儿都不想丢,建议使用同步的 double wirte机制。
- Producer 发送者的ID不能和其他非事务型生产者的ID共用
消息存储
分布式队列因为有高可靠行的要求,所以数据要进行持久化存储
- 消息生成者发送消息
- mq收到消息,将消息进行持久化,在存储中新增一条记录
- 返回ack给生产者
- mq push消息给对应的消费者,然后等待消费者返回ack
- 如果消息者在指定时间内成功返回ack,那么mq 认为消息消费成功,在存储中删除消息,即执行第6步;如果mq在指定时间内没有收到ack,则认为消息消费失败,会尝试重新push消息,重复执行push操作
- mq删除消息
存储介质
- 关系型数据库
普通关系型数据库(如Mysql)在单表数据量达到千万级别的情况下,其IO读写性能往往 会出现瓶颈。在可靠性方面,该种方案非常依赖DB,如果一旦DB出现故障,则MQ的消息就无法落盘存储会导致线上故障。
- 文件系统(推荐)
目前业界较为常用的几款产品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盘 至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,否则一般是不会出现无法持 久化的故障问题。
- 性能对比
文件系统 > 关系型数据库
消息的存储和发送
- 消息存储
RocketMQ的消息用顺序写, 保证了消息存储的速度。目前的高性能磁盘,顺序写速度可以达到600MB/s, 超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍
- 消息发送
Linux操作系统分为【用户态】和【内核态】,文件操作、网络操作需要涉及这两种形态 的切换,免不了进行数据复制。 一台服务器 把本机磁盘文件的内容发送到客户端,一般分为两个【读】和【写】两个步骤,这两个看似简单的操作,实际进行了4 次数据复制,分别是:
- 从磁盘复制数据到内核态内存;
- 从内核态内存复 制到用户态内存;
- 然后从用户态 内存复制到网络驱动的内核态内存;
- 最后是从网络驱动的内核态内存复 制到网卡中进行传输。
通过使用mmap的方式(零拷贝),可以省去向用户态的内存复制,提高速度。这种机制在Java中是 通过MappedByteBuffer实现的 RocketMQ充分利用了上述特性,提高消息存盘和网络发送的速度。
这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G 的文件至用户态的虚拟内存,这也是为何RocketMQ 默认设置单个CommitLog日志数据文件为1G的原因了。
消息存储结构
- 简介
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。
每个Topic下的每个Message Queue都有一个对应 的ConsumeQueue文件。
- 存储结构例图
-
CommitLog:存储消息的元数据
-
ConsumerQueue:存储消息在CommitLog的索引,(即使ConsumerQueue丢失也没关系,可以通过CommitLog恢复回来)
-
IndexFile:为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程
-
配置文件对应参数
#commitLog 存储路径
storePathCommitLog=/usr/local/java/rocketmq/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/usr/local/java/rocketmq/store/consumequeue
#消息索引存储路径
storePathIndex=/usr/local/java/rocketmq/store/index
刷盘机制
- 简介
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式,分布式同步刷盘和异步刷盘。
- 同步刷盘
在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
- 异步刷盘
在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
- 配置
同步刷盘还是异步刷盘,都是通过Broker配置文件里的flushDiskType参数设置的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH中的一个。
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
高可用性机制
- RocketMQ集群架构图
- Master和Slave配合达到高可用
RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。
Master和Slave的区别:
- 在Broker的配置文件中,参数brokerId的值为0表明这个Broker是Master,大于0表明这个Broker是Slave,同时brokerRole参数也会说明这个Broker是Master还是Slave。
- Master角色的Broker支持读和写,Slave角色的Broker仅支持读,也就是Producer只能和Master角色的Broker连接写入消息;Consumer可以连接Master角色的Broker,也可以连接Slave角色的Broker来读取消息。
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SLAVE
消息消费高可用
- 自动切换机制
在Consumer的配置文件中,并不需要设置是从Master读还是从Slave 读,当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave读。有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序。
消息发送高可用
- Broker组
在创建Topic的时候,把Topic的多个Message Queue创建在多个Broker组上(相同 Broker名称,不同 brokerId的机器组成一个Broker组),这样当一个Broker组的 Master不可 用后,其他组的Master仍然可用,Producer仍然可以发送消息。
RocketMQ目前还不支持把Slave自动转成Master,如果机器资源不足,需要把Slave转成Master,则要手动停止Slave角色Broker,更改配置文件,用新的配置文件启动Broker。
消息主从复制
- 复制方式
如果一个Broker组有Master和Slave,消息需要从Master复制到Slave 上,有同步和异步两种复制方式。
- 同步复制
同步复制方式是等Master和Slave均写成功后才反馈给客户端写成功状态。在同步复制方式下,如果Master出故障, Slave上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。
- 异步复制
异步复制方式是只要Master写成功即可反馈给客户端写成功状态。在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果Master出了故障,有些数据因为没有被写入Slave,有可能会丢失;
- 配置
同步复制和异步复制是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成ASYNC_MASTER、 SYNC_MASTER、SLAVE三个值中的一个
- 总结
异步刷盘保证吞吐量,主从复制保证数据不丢失
实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式, 尤其是SYNC_FLUSH 方式,由于频繁地触发磁盘写动作,会明显降低性能。通常情况下,应该把Master和Slave配置成ASYNC_FLUSH的刷盘方式,主从之间配置成SYNC_MASTER的复制方式,这样即使有一台机器出故障,仍然能保证数据不丢,是个不错的选择。
负载均衡
Producer负载均衡
-
概念
Producer端,每个实例在发消息的时候,默认会轮询所有的message queue发送,以达到让消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息 就发送到不同的broker下
-
样例图
Consumer负载均衡
- 集群模式
在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。
- AllocateMessageQueueAveragely算法(默认)
- AllocateMessageQueueAveragelyByCircle算法
- 广播模式
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。 在实现上,就是在consumer分配queue的时候,所有consumer都分到所有的queue。
- 注意事项
- 集群模式下,queue都是只允许分配只一个实例。这是由于如果多个实例同时消费一个queue的消息,由于拉取哪些消息是consumer主动控制的,那样会导致同一个消息在不同的实例下被消费多次,所以算法上都是一个queue只分给一个 consumer实例,一个consumer实例可以允许同时分到不同的queue。
- 集群模式下,需要控制让queue的总数量大于等于consumer的数量。通过增加consumer实例去分摊queue的消费,可以起到水平扩展的消费能力的作用。而有实例下线的时候,会重新触发负载均衡,这时候原来分配到的queue将分配到其他实例上继续消费。 但是如consumer实例的数量比message queue的总数量还多的话,多出来的 consumer实例将无法分到queue,也就无法消费到消息,也就无法起到分摊负载的作用了
消息重试
顺序消息的重试
- 概念
对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
无序消息的重试
- 概念
对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。
无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
- 重试次数
一条消息无论重试多少次,这些重试消息的MessageID不会改变
- 配置方式
1、配置重试方式
集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种)
2、配置消息不重试方式
集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回Action.CommitMessage,此后这条消息将不会在重试
3、自定义消息最大重试次数
4、获取消息重试次数
死信队列
- 概念
当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。
死信特性
- 死信消息特性
- 不会再被消费者正常消费
- 有效期与正常消息相同,均为3天,3天后会被自动删除。
- 死信队列具有以下特性
- 一个死信队列对应一个Group ID,而不是对应单个消费者实例
- 如果一个Group ID未产生死信消息,消息队列RocketMQ不会为其创建相应的死信队列
- 一个死信队列包含了对应Group ID产生的所有死信消息,不论该消息属于哪个Topic
查看死信信息
- 控制台查询出现死信队列主题信息
- 消息界面根据主题查询死信消息
- 选择重新发送消息
一条消息进入死信队列,意味着某些因素导致消息者无法正常消费该消息,因此,通常需要对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列RocketMQ控制台重新发送该消息,让消费者重新消费一次
消费幂等
- 概念
消息队列RocketMQ消费者在接收到消息以后,有必要根据业务上的唯一key对消息做幂等处理的必要性
- 消息幂等必要性
处理方式