消息队列的作用
- 削峰填谷 : 某个时刻请求过大,避免高负荷拖垮服务器,将这些请求放入消息队列内,web服务器按照能力从消息队列消费请求
- 同步转异步:上游服务器调用下游服务器,下游服务器处理缓慢,上游不得不同步等待下游处理完成,有些时候并不需要同步等待下游完成,甚至有时下游服务器执行结果对上游来说完全不重要。将请求放入MQ,让下游自己消费。
- 程序间解耦合:让下游服务器从消息中间件消费上游发出的请求,消息中间件使得上游和下游服务器间的依赖变为间接依赖实现解耦合
- 数据最终一致性:
为什么选择RocketMQ
- 成熟、稳定可靠的解决方案:经过阿里巴巴大厂极致需求的洗礼,成功支撑了双11万亿级别消息流量
- Java语言开发,开源:可以进行二次开发然后集成到现有Java系统
- 社区活跃:平台完善,更新活跃
RockerMQ
- NameServer:存放一些元数据,类似注册中心,rocketmq早期也尝试过用zookeeper作为注册中心 producer和consumer都需要从NameServer获取到broker的信息
- Broker:实际存放消息的地方。
- 生产者组:标识同一类生产者。
- 消费者组:标识同一类消费者,同一组中的消费者必须保持相同的消费逻辑和配置。
- Topic:消息传输和存储的分组容器。一个主题由多个消息队列组成,这些消息队列用于存储消息和扩展主题。
- Queue:消息队列,存放在消息队列里的消息按照先进先出的形式被消费者消费。
- 订阅关系:由Topic和tag共同组成,消费者组内消费者通常会明确指定要消费的Topic以及消费的消息的tag。同一组消费者的topic和tag必须保持一致。
生产者往对应Topic发送消息,如果不明确指定队列,客户端会采用默认的策略选择对应Topic一个队列发送消息。Broker在收到消息后,存储对应的消息,并往对应的Topic的队列入一个元素(元素里存储了消息的索引)。
发布/订阅模式
- 消费独立: 相比队列模型的匿名消费方式,发布订阅模型中消费方都会具备的身份,一般叫做订阅组(订阅关系),不同订阅组之间相互独立不会相互影响。
- 一对多通信 基于独立身份的设计,同一个主题内的消息可以被多个订阅组处理,每个订阅组都可以拿到全量消息。因此发布订阅模型可以实现一对多通信。
消费者组订阅了某个Topic后,消费者组内的消费者会根据策略然后分配到自己负责的对应的主题的队列,队列的消费位点会在消费者消费消息时变化。
一个队列只会被一个消费者组内的一个消费者消费,不同消费者组对应的队列的消费位点相互独立。
生产者往对应Topic发送消息,如果不明确指定队列,客户端会采用默认的策略选择对应Topic一个队列发送消息。Broker在收到消息后,存储对应的消息,并往对应的Topic的队列入一个元素(元素里存储了消息的索引)。消费者启动后,订阅了指定的Topic后,从broker哪里获得消费者组相关的信息,然后采取指定的Rebalance策略获取到自己要消费的队列。消费时先拉取要消费的消息到本地,消费成功后根据策略选择指定时机往broker同步队列的消息位点。消费者会按照tag进行过滤出自己要消费的消息。
Rebalance是 RocketMQ 中消费者端实现负载均衡的核心机制,用于在同一消费者组(Consumer Group)中的多个消费者之间重新分配消息队列(MessageQueue)。其主要目的是提升消息处理的并行能力,确保消费者组内的负载均衡。
每当消费者组内的成员发生变动,都会触发Rebalance。
生产者
- 生产者组:标识同一类生产者,对于事物消息来说,原始生产者崩溃时,同组其他生产者可以继续处理事务
- Topic:主题名称,若干Queue组成
生产者类型
- 默认消息生产者:DefaultMQProducer ,生产普通消息、顺序消息、单向消息、批量消息、延迟消息
- 事物生产者:TransactionMQProducer,生产事物消息
消息
public class Message implements Serializable {
private String topic; //所属的主题
private int flag;
private Map<String, String> properties; // 消息扩展信息 Tag:消费时可以根据tag过滤消息;keys:用于标识消息,方便检索; 延迟级别:用于延迟消息
private byte[] body; // 发送的消息需要转换成字节数组
private String transactionId;
...
}
消息类型
- 普通消息:并发消息,生产者在不指定topic队列时,会采取策略发送消息到topic的队列中,同一时刻不同队列的消费者不一样,使得topic中消息能够并发的被消费。 同一个队列的消息能够保证消息消费具有先后顺序。
如果实现全局有序消息?topic的队列设置为1,就能保证topic内消息被顺序消费 - 延迟消息:延迟被消费者消费
- 事物消息:保证本地操作执行完毕后,才真正的发送消息。
- 顺序消息:5.x之后的版本支持顺序消息,通过messageGroup使得消息都进入到同一个队列内
生产者属性
- namesrvAddr:表示 RocketMQ 集群的 Namesrv 地址
- clientIP:使用的客户端程序所在机器的 IP地址。运行在 Docker 容器中,获取的 IP 地址是容器所在的 IP 地址
- instanceName:实例名,每个实例都需要取唯一的名字,因为有时我们会在同一个机器上部署多个程序进程,如果名字有重复就会导致启动失败。
- producerGroup:生产者组名,这是一个必须传递的参数。
- sendMsgTimeout:发送超时时间,单位为ms。
- compressMsgBodyOverHowmuch:消息体的容量上限,超过该上限时消息体会通过ZIP进行压缩,该值默认为4MB。
- retryTimesWhenSendFailed/retryTimesWhenSendAsyncFailed:重试次数,默认为2,也就是有3次机会。
生产者核心方法
- start():这是启动整个生产者实例的入口
- shutdown():关闭本地已注册的生产者
- fetchPublishMessageQueues(Topic):获取一个Topic有哪些Queue。
- send(Message msg) :同步发送普通消息。
- send(Message msg,long timeout) :同步发送普通消息(超时设置)。
- send(Message msg,SendCallback sendCallback) :异步发送普通消息。
- sendOneway(Message msg) :发送单向消息。只负责发送消息,不管发送结果。
- send(Message msg,MessageQueue mq) :同步向指定队列发送消息。
- send(Message msg,MessageQueue mq,long timeout) :同步向指定队列发送消息(超时设置)。
- send(Message msg,MessageQueue mq,SendCallback sendCallback)
- send(Message msg,MessageQueue mq,SendCallback sendCallback,long timeout)
- send(Message msg,MessageQueueSelector selector,Object arg,SendCallback sendCallback)
- send(Collection<Message>msgs) :批量发送消息。
- createTopic(String key,String newTopic,int queueNum):创建Topic。
生产者如何保证高可用? 生产者如何处理故障时使得消息不出现未知状态和丢失?
客户端:
- 重试机制:同步和异步发送都支持失败重试 retryTimesWhenSendFailed : 消费用于设置重试次数的参数,默认为2,也就是有3次机会。retryTimesWhenSendAsyncFailed 表示异步重试的次数,同样的默认值。
- 客户端会选择一个发送延迟级别低的Broker来发送消息
Broker:
- broker主从集群支持同步复制和异步复制,对于可用性要求及高的场景采用同步复制
- broker消息刷盘机制:支持配置同步刷盘和异步刷盘,对于可用性要求及高的场景采用同步刷盘
RocketMQ网络通信采用Netty实现,Netty本质是异步网络通信框架,如何实现同步调用的? 等价于Netty如何实现同步调用
- 利用countDownLatch:发送完请求后在Handler调用await,请求返回时,重置对于channel中的countDown
- RocketMQ中发送完同步请求后调用waitResponse(也是用了countDownLatch),响应到达后通过putResponse()方法释放锁
普通消息
Maven依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
private static DefaultMQProducer initDefaultProduer() throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("group_test");
producer.setNamesrvAddr("192.168.169.223:9876");
producer.start();
return producer;
}
生产者端:
@Test
public void test02() throws Exception {
DefaultMQProducer defaultMQProducer = initDefaultProduer();
Message msg = new Message("topic_test01", "tags233", "hello3 3world2".getBytes(StandardCharsets.UTF_8));
SendResult send = defaultMQProducer.send(msg, 10000); // 发送会等broker响应后才进行下一步操作
System.out.println(send);
// 单向消息:只管发送
msg.setBody("单向消息:不管成功发送与否".getBytes());
defaultMQProducer.sendOneway(msg);
// 批量消息
List<Message> messages=new LinkedList<>();
Message msg2 = new Message("topic_test01", "tags233", "批量消息1".getBytes(StandardCharsets.UTF_8));
Message msg3 = new Message("topic_test01", "tags233", "批量消息2".getBytes(StandardCharsets.UTF_8));
messages.add(msg2);
messages.add(msg3);
SendResult send2 = defaultMQProducer.send(messages, 10000);
System.out.println(send2);
}
消费者端:
// 普通消息/顺序消息
@Test
public void test02() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_5");
consumer.setNamesrvAddr("192.168.169.223:9876");
consumer.subscribe("topic_test01", "*");
consumer.subscribe("topic_test02", "*");
consumer.subscribe("topic_test03", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
try {
// 处理消息
String topic = msg.getTopic();
String tags = msg.getTags();
String body = new String(msg.getBody());
System.out.printf("Received message - Topic: %s, Tags: %s, Body: %s%n",
topic, tags, body);
} catch (Exception e) {
// 处理异常,返回重试
System.err.println("Process message failed: " + e.getMessage());
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
// 消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("Consumer started successfully");
while (true){
Thread.sleep(10000);
}
}
顺序消息
新版本rocketmq给message提供了messageGroup的属性用于实现顺序消息,本质还是发往同一个队列
- 顺序消息: 需要考虑生成端顺序生成消息(有顺序的往mq中发),这里可以考虑单一生产者
生产者: 消费者和顺序消息一致,这里同时启动3个生产者,会发现只有一个生产者消费且顺序消费
// 顺序消息
@Test
public void test03() throws Exception {
DefaultMQProducer defaultMQProducer = initDefaultProduer();
Message msg = new Message("topic_test01", "tags233", "-1".getBytes(StandardCharsets.UTF_8));
for (int i = 0; i < 10; i++) {
SendResult send = defaultMQProducer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 固定选择queueId=0的队列
for (MessageQueue mq : mqs) {
System.out.println(mq.getQueueId());
if (mq.getQueueId() == 0) {
return mq;
}
}
// 如果没有queueId=0,返回第一个队列
return mqs.get(0);
}
}, null);
System.out.println(send);
String body = i + "";
msg.setBody(body.getBytes());
}
}
延迟消息
生产者
// 延迟消息
@Test
public void test04() throws Exception {
DefaultMQProducer defaultMQProducer = initDefaultProduer();
Message msg = new Message("topic_test02", "第三次", "-1".getBytes(StandardCharsets.UTF_8));
// messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
for (int i = 0; i < 10; i++) {
String body = System.currentTimeMillis() + "";
msg.setBody(body.getBytes());
msg.setDelayTimeLevel(2); // 延迟5s后才能被消费
SendResult send = defaultMQProducer.send(msg);
System.out.println(send);
Thread.sleep(1000);
}
}
消费者
// 测试延迟消息
@Test
public void test03() throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_5");
consumer.setNamesrvAddr("192.168.169.223:9876");
consumer.subscribe("topic_test01", "*");
consumer.subscribe("topic_test02", "*");
consumer.subscribe("topic_test03", "*");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
try {
// 处理消息
String body = new String(msg.getBody());
// 消息体是一个时间戳==>输出时间戳对应的时间
Date now=new Date(System.currentTimeMillis());
Date Start=new Date(Long.parseLong(body));
System.out.printf("Received message - start: %s, now: %s",
sdf.format(Start),sdf.format(now));
System.out.println(" 消费完成");
} catch (Exception e) {
// 处理异常,返回重试
System.err.println("Process message failed: " + e.getMessage());
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
// 消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("Consumer started successfully");
while (true){
Thread.sleep(10000);
}
}
事务消息
本地事务执行完毕才真正的发送消息到MQ
生产者:同时启动test05和test06,test05执行异常的消息在test05关闭后会被test06处理
消费者端采用普通消息的消费者即可
// 事务消息:
static class myTransactionListener implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.println("业务参数(根据业务参数执行不同业务类型): "+arg+ "执行事务 : " + new String(msg.getBody()));
// 模拟业务逻辑
double random = Math.random();
if (random > 0.5) {
System.out.println("事务执行完毕:消息正常发送。");
return LocalTransactionState.COMMIT_MESSAGE;
} else if (random > 0.3) {
System.out.println("本地能检查到执行失败,主动回滚。");
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 异常:执行异常rocketmq会帮助捕获异常,然后通知broker
return null;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("事务回查 : " + new String(msg.getBody()));
try {
// 检查本地事务状态
boolean transactionCommitted = Math.random() > 0.3;
if (transactionCommitted) {
// 重新提交事务
return LocalTransactionState.COMMIT_MESSAGE;
}
// 执行失败回滚
return LocalTransactionState.ROLLBACK_MESSAGE;
} catch (Exception e) {
System.err.println("回查异常: " + e.getMessage());
return LocalTransactionState.UNKNOW;
}
}
}
@Test
public void test05() throws Exception {
TransactionMQProducer transactionMQProducer = new TransactionMQProducer("Transaction_group_test05");
transactionMQProducer.setNamesrvAddr("192.168.169.223:9876");
transactionMQProducer.setTransactionListener(new myTransactionListener());
transactionMQProducer.start();
for (int i = 0; i < 10; i++) {
String body="事务消息 "+i;
Message message = new Message("topic_test03", "tags233", body.getBytes(StandardCharsets.UTF_8));
TransactionSendResult sendResult = transactionMQProducer.sendMessageInTransaction(message, "business_arg");
System.out.println("发送结果: " + sendResult.getSendStatus());
System.out.println("事务状态: " + sendResult.getLocalTransactionState());
System.out.println("消息ID: " + sendResult.getMsgId());
}
transactionMQProducer.shutdown();
}
// 检查事务:同一个消费者组帮助兜底
@Test
public void test06() throws Exception {
TransactionMQProducer transactionMQProducer = new TransactionMQProducer("Transaction_group_test05");
transactionMQProducer.setTransactionListener(new myTransactionListener());
transactionMQProducer.setNamesrvAddr("192.168.169.223:9876");
transactionMQProducer.start();
String body="回查生产者 事务消息 ";
Message message = new Message("topic_test03", "tags233", body.getBytes(StandardCharsets.UTF_8));
TransactionSendResult sendResult = transactionMQProducer.sendMessageInTransaction(message, "business_arg");
System.out.println("发送结果: " + sendResult.getSendStatus());
System.out.println("事务状态: " + sendResult.getLocalTransactionState());
System.out.println("消息ID: " + sendResult.getMsgId());
while (true) {
Thread.sleep(50000);
}
}