系统之间传递消息,传统方式是系统A直接发消息给系统B,而消息队列则是在二者之间作为“中间件”的作用,其最大的好处在于实现了消息收发的解耦、异步、削峰。
详细了解传送门(写得深入浅出 非常推荐):什么是消息队列
通过这种在二者加入中间件提升系统整体效率的例子,最典型的就是用货币这个媒介替代传统的以物易物。
目前主流的消息队列有:
本文使用的是阿里开源的rocketmq作为案例介绍
首先前往官网下载:https://rocketmq.apache.org/zh/download/
下载完成后,将zip压缩包上传到Linux服务器,并使用unzip命令解压缩。
# 如果提示没有unzip命令 先用yum安装即可
yum install -y unzip zip
# 解压缩到当前文件夹
unzip rocketmq-all-4.9.3-bin-release.zip
# 将解压后的目录移动到根目录的opt目录下 该目录约定俗成用于存放第三方软件
mv rocketmq-4.9.3 /opt/
# 进入配置文件所在目录
cd /opt/rocketmq-4.9.3/bin
# 修改runbroker.sh 进入后输入 :set nu 显示行号
vim runbroker.sh
# 修改第85行 值供参考
85 JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m"
# 修改runserver.sh
vim runserver.sh
# 修改第71行 值供参考
71 JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn64m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=80m"
需要放行4个端口,9876,10911,10912,10909。
参考:RocketMQ服务中各端口号说明
# 放行4个端口
firewall-cmd --add-port=9876/tcp --zone=public --permanent
firewall-cmd --add-port=10909/tcp --zone=public --permanent
firewall-cmd --add-port=10911/tcp --zone=public --permanent
firewall-cmd --add-port=10912/tcp --zone=public --permanent
# 放行后需要重新加载防火墙配置
firewall-cmd --reload
# 确保当前处于rocketmq的根目录
cd /opt/rocketmq-4.9.3
# 运行mqnamesrv
nohup sh bin/mqnamesrv &
# 运行broker
nohup sh bin/mqbroker -n localhost:9876 &
# 查看日志输出
tailf nohup.out
在进行工具测试消息收发之前,我们需要告诉客户端NameServer的地址,这里我们使用环境变量NAMESRV_ADDR
告诉客户端地址。
# 测试消息发送
export NAMESRV_ADDR=localhost:9876 # 配置环境变量
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
# 测试消息接收
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
# 确保目录在mq的根目录下(以下两行命令任选一)
sh bin/mqadmin updatetopic -n localhost:9876 -t TestTopic -c DefaultCluster
# 或者如下命令 -b broker地址 表示topic建在该broker
sh bin/mqadmin updatetopic -n localhost:9876 -b localhost:10911 -t TestTopic
在java中实现消息收发,先在pom.xml中引入依赖。
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-client-javaartifactId>
<version>4.9.3version>
dependency>
新建一个类:Producer
// 创建消息生产者Producer
public class Producer {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
// 1.创建并启动生产者
DefaultMQProducer producer = new DefaultMQProducer("simple_producer_group");// 任取一消息群组名
producer.setNamesrvAddr("192.168.xxx.xxx:9876");// 设置端口和ip
producer.start();// 启动producer
// 2.准备消息对象Message并发送
String content = "hello";// 要发送的内容
Message message = new Message("TestTopic", content.getBytes(StandardCharsets.UTF_8));// 创建消息对象 设置topic名
SendResult result = producer.send(message);// 发送消息
System.out.println(result);// 输出发送的结果
// 3.关闭producer
producer.shutdown();
}
}
新建一个类:Consumer
// 创建消息消费者Consumer
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建消息消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("simple_consumer_group");// 群组名与生产者一致
consumer.setNamesrvAddr("192.168.xxx.xxx:9876");// 与生产者一致
// 2.订阅特定的topic *表示所有
consumer.subscribe("TestTopic", "*");
// 3.设置消息监听器
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, consumeConcurrentlyContext) -> {
for (MessageExt ext : msgs)
System.out.println("收到消息:"+new String(ext.getBody(), StandardCharsets.UTF_8));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 4.启动消息消费者
consumer.start();
}
}
消息队列发送消息有3种方式,分别是同步发送、异步发送和单向模式发送。
同步发送:是最常用的方式,是指消息发送方发出一条消息后,会在收到服务端同步响应之后才发下一条消息的通讯方式,可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。
异步发送:发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。
单向模式发送:发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
消息过滤:在发送消息时带上tag便签,在消费端只接收指定tag的消息。
同步发送的整个代码流程如下:
// 同步发送 - 消息生产者(消费者代码不变 参考上文)
public class ProducerDemo {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
// 1.创建并启动producer 消息生产者
DefaultMQProducer producer = new DefaultMQProducer("simple_producer_group");
producer.setNamesrvAddr("192.168.xxx.xxx:9876");
producer.start();
// 2.消息的内容
String content = "hello";
// 3.创建并发送消息(同步)
Message message = new Message("TestTopic", content.getBytes(StandardCharsets.UTF_8));
SendResult result = producer.send(message);
System.out.println(result);// 输出返回结果
// 4.producer不再使用 关闭producer
producer.shutdown();
}
}
异步发送需要实现异步发送回调接口(SendCallback)。
消息发送方在发送了一条消息后,不需要等待服务端响应即可发送第二条消息,发送方通过回调接口接收服务端响应,并处理响应结果。异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景。例如,视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
异步发送与同步发送代码唯一区别在于调用send接口的参数不同,异步发送不会等待发送返回,取而代之的是send方法需要传入 SendCallback 的实现,SendCallback 接口主要有onSuccess 和 onException 两个方法,表示消息发送成功和消息发送失败。
如下是示例代码。
// 异步发送 - 消息生产者(消费者代码不变 参考上文)
public class ProducerDemo {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
// 1.创建并启动producer 消息生产者
DefaultMQProducer producer = new DefaultMQProducer("simple_producer_group");
producer.setNamesrvAddr("192.168.xxx.xxx:9876");
producer.start();
// 2.消息的内容
String content = "hello";
// 3.创建并发送消息(异步)
for (int i = 0; i < 10; i++) {
Message message = new Message("TestTopic", (content + i).getBytes(StandardCharsets.UTF_8));
producer.send(message, new SendCallback() {
@Override// 返回成功时的操作
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override// 返回失败时的操作
public void onException(Throwable e) {
System.out.println(e.getMessage());
}
});
}
// 4.producer不再使用 关闭producer
producer.shutdown();
}
}
消息发送者只负责发,不考虑响应返回的问题。单向模式调用sendOneway,不会对返回结果有任何等待和处理。
以下是示例代码。
// 单向模式发送 - 消息生产者(消费者代码不变 参考上文)
public class ProducerDemo {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
// 1.创建并启动producer 消息生产者
DefaultMQProducer producer = new DefaultMQProducer("simple_producer_group");
producer.setNamesrvAddr("192.168.xxx.xxx:9876");
producer.start();
// 2.消息的内容
String content = "hello";
// 3.单向模式发送
Message message = new Message("TestTopic", content.getBytes(StandardCharsets.UTF_8));
producer.sendOneway(message);
// 4.producer不再使用 关闭producer
producer.shutdown();
}
}
消息生产者在发送Message时增加tag参数,即带上标签,消息消费者可订阅特定的标签。
// 消息生产者
public class ProducerDemo {
public static void main(String[] args) {
// 1.创建并启动producer 消息生产者
...
// 2.消息的内容
...
// 3.发送消息(使用tag)
String tag = "good";// 设置tag
Message message = new Message("TestTopic", tag, content.getBytes(StandardCharsets.UTF_8));// 带上tag
producer.sendOneway(message);// 发送消息
// 4.producer不再使用 关闭producer
...
}
}
// 消息消费者(体现在第2步)
public class ConsumerDemo {
public static void main(String[] args) throws MQClientException {
// 1.创建消息接收者consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("simple_consumer_group");
consumer.setNamesrvAddr("192.168.xxx.xxx:9876");
// 2.订阅特定的topic(tag) *表示订阅所有
consumer.subscribe("TestTopic", "good");
// 3.消息监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt ext : msgs){
System.out.println("收到消息:"+new String(ext.getBody(), StandardCharsets.UTF_8));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 4.启动消息消费者
consumer.start();
}
}
关于消息队列在springboot项目中的使用,详见我后续的文章。