MQ背景&选型
消息队列作为高并发系统的核心组件之一,能够帮助业务系统解构提升开发效率和系统稳定性。主要具有以下优势:
削峰填谷(主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题)
系统解耦(解决不同重要程度、不同能力级别系统之间依赖导致一死全死)
提升性能(当存在⼀对多调用时,可以发一条消息给消息系统,让消息系统通知相关系统)
蓄流压测(线上有些链路不好压测,可以通过堆积一定量消息再放开来压测)
目前主流MQ对比如下:
根据实际业务需要,选择合适的MQ,我们这篇主要来学习RocketMQ。
RocketMQ有以下几点优势:
支持事务性消息
支持延迟消息
支持重复消费
⽀持指定次数和时间间隔的失败消息重发
RocketMQ架构图以及角色
首先,我们要了解各个角色的功能。
Broker
理解成RocketMQ本身
broker主要用于producer和consumer接收和发送消息
broker会定时向nameserver提交自己的信息
是消息中间件的消息存储、转发服务器
每个broker节点,在启动时,都会遍历NameServer列表,与每个NameServer建立长连接,注册自己的信息,之后定时上报,定时(每隔30s)注册Topic信息到所有NameServer。NameServer定时(每隔10s)扫描所有存活broker的连接,如果NameServer超过2分钟没有收 到⼼跳,则NameServer断开与Broker的连接。
Nameserver
理解成zookeeper的效果,只是他没用zk,而是自己写了个nameserver来替代zk
底层由netty实现,提供了路由管理、服务注册、服务发现的功能,是一个无状态节点
nameserver是服务发现者,集群中各个角色(producer、broker、consumer等)都需要定时向nameserver上报自己的状态,以便互相发现彼此,超时不上报的话,nameserver会把它从列表中剔除
nameserver可以部署多个,当多个nameserver存在的时候,其他角色同时向他们上报信息,以保证高可用
NameServer集群间互不通信,没有主备的概念
nameserver内存式存储,nameserver中的broker、topic等信息默认不会持久化,所以他是无状态节点
Producer
消息的生产者
随机选择其中一个NameServer节点建立长连接,获得Topic路由信息(包括topic下的queue,这些queue分布在哪些broker上等等)
接下来向提供topic服务的master建立长连接(因为rocketmq只有master才能写消息),且定时向master发送心跳
Consumer
消息的消费者
通过NameServer集群获得Topic的路由信息,连接到对应的Broker上消费消息
由于Master和Slave都可以读取消息,因此Consumer会与Master和Slave都建立连接进行消费消息
核心流程
Broker都注册到Nameserver上
Producer发消息的时候会从Nameserver上获取发消息的topic信息
Producer向提供服务的所有master建立长连接,且定时向master发送心跳
Consumer通过NameServer集群获得Topic的路由信息
Consumer会与所有的Master和所有的Slave都建立连接进行监听新消息
核心概念
Message
消息载体。Message发送或者消费的时候必须指定Topic。Message有一个可选的Tag项用于过滤消息,还可以添加额外的键值对。
topic
消息的逻辑分类,发消息之前必须要指定一个topic才能发,就是将这条消息发送到这个topic上。消费消息的时候指定这个topic进行消费。就是逻辑分类。
queue
1个Topic会被分为N个Queue,数量是可配置的。message本身其实是存储到queue上的,消费者消费的也是queue上的消息。多说一嘴,比如1个topic 4个queue,有5个Consumer都在消费这个topic,那么会有一个consumer浪费掉了,因为负载均衡策略,每个consumer消费1个queue,5>4,溢出1个,这个会不工作。
Tag
Tag 是 Topic 的进一步细分,顾名思义,标签。每个发送的时候消息都能打tag,消费的时候可以根据tag进行过滤,选择性消费。
ACK
首先要明确一点:ACK机制是发生在Consumer端的,不是在Producer端的。也就是说Consumer消费完消息后要进行ACK确认,如果未确认则代表是消费失败,这时候Broker会进行重试策略(仅集群模式会重试)。ACK的意思就是:Consumer说:ok,我消费成功了。这条消息给我标记成已消费吧。
自己安装一下MQ,接下来看一下代码。
Producer
pom引入
org.apache.rocketmq rocketmq-client 4.7.0 发消息肯定要必备如下几个条件:
指定生产组名(不能用默认的,会报错)
配置namesrv地址(必须)
指定topic name(必须)
指定tag/key(可选)
验证消息是否发送成功:消息发送完后可以启动消费者进行消费,也可以去管控台上看消息是否存在。
单条发送
public class Producer {
public static void main(String[] args) throws Exception {
// 指定生产组名为my-producer
DefaultMQProducer producer = new DefaultMQProducer(“my-producer”);
// 配置namesrv地址
producer.setNamesrvAddr(“124.57.180.156:9876”);
// 启动Producer
producer.start();
// 创建消息对象,topic为:myTopic001,消息内容为:hello world
Message msg = new Message(“myTopic001”, “hello world”.getBytes());
// 发送消息到mq,同步的
SendResult result = producer.send(msg);
System.out.println("发送消息成功!result is : " + result);
// 关闭Producer
producer.shutdown();
}
//发送消息成功!result is : SendResult [sendStatus=SEND_OK, msgId=A9FE854140F418B4AAC26F7973910000, offsetMsgId=7B39B49D00002A9F00000000000589BE, messageQueue=MessageQueue [topic=myTopic001, brokerName=broker-a, queueId=0], queueOffset=7]
}
批量发送
public class ProducerMultiMsg {
public static void main(String[] args) throws Exception {
// 指定生产组名为my-producer
DefaultMQProducer producer = new DefaultMQProducer(“my-producer”);
// 配置namesrv地址
producer.setNamesrvAddr(“124.57.180.156:9876”);
// 启动Producer
producer.start();
String topic = "myTopic001";
// 创建消息对象,topic为:myTopic001,消息内容为:hello world1/2/3
Message msg1 = new Message(topic, "hello world1".getBytes());
Message msg2 = new Message(topic, "hello world2".getBytes());
Message msg3 = new Message(topic, "hello world3".getBytes());
// 创建消息对象的集合,用于批量发送
List<Message> msgs = new ArrayList<>();
msgs.add(msg1);
msgs.add(msg2);
msgs.add(msg3);
// 批量发送的api的也是send(),只是他的重载方法支持List<Message>,同样是同步发送。
SendResult result = producer.send(msgs);
System.out.println("发送消息成功!result is : " + result);
// 关闭Producer
producer.shutdown();
System.out.println("生产者 shutdown!");
}
}
sendCallBack(异步)
sendOneway(不要求结果)
自己可以试一下这两种
Consumer
每个consumer只能关注一个topic。
发消息肯定要必备如下几个条件:
指定消费组名(不能用默认的,会报错)
配置namesrv地址(必须)
指定topic name(必须)
指定tag/key(可选)
Clustering:集群模式,默认。
比如启动五个Consumer,Producer生产一条消息后,Broker会选择五个Consumer中的其中一个进行消费这条消息,所以他属于点对点消费模式。
public static void main(String[] args) throws Exception {
// 指定消费组名为my-consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(“my-consumer”);
// 配置namesrv地址
consumer.setNamesrvAddr(“124.57.180.156:9876”);
// 订阅topic:myTopic001 下的全部消息(因为是*,指定的是tag标签,代表全部消息,不进行任何过滤)
consumer.subscribe(“myTopic001”, "");
// 注册监听器,进行消息消息。
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : msgs) {
String str = new String(msg.getBody());
// 输出消息内容
System.out.println(str);
}
// 默认情况下,这条消息只会被一个consumer消费,这叫点对点消费模式。也就是集群模式。
// ack确认
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者
consumer.start();
System.out.println(“Consumer start”);
}
}
Broadcasting:广播模式。比如启动五个Consumer,Producer生产一条消息后,Broker会把这条消息广播到五个Consumer中,这五个Consumer分别消费一次,每个都消费一次。
// 代码里只需要添加如下这句话即可:
consumer.setMessageModel(MessageModel.BROADCASTING);
两种模式对比
集群默认是默认的,广播模式是需要手动配置。
一条消息:集群模式下的多个Consumer只会有一个Consumer消费。广播模式下的每一个Consumer都会消费这条消息。