MQ背景&选型
消息队列作为高并发系统的核心组件之一,能够帮助业务系统解构提升开发效率和系统稳定性。主要具有以下优势:

削峰填谷(主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题)

系统解耦(解决不同重要程度、不同能力级别系统之间依赖导致一死全死)

提升性能(当存在⼀对多调用时,可以发一条消息给消息系统,让消息系统通知相关系统)

蓄流压测(线上有些链路不好压测,可以通过堆积一定量消息再放开来压测)

目前主流MQ对比如下:

消息队列 并发_System

根据实际业务需要,选择合适的MQ,我们这篇主要来学习RocketMQ。

RocketMQ有以下几点优势:

支持事务性消息

支持延迟消息

支持重复消费

⽀持指定次数和时间间隔的失败消息重发

RocketMQ架构图以及角色

消息队列 并发_java_02

首先,我们要了解各个角色的功能。

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都会消费这条消息。