持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
1 RabbitMQ提供的通讯方式
- Hello World--最基本的通讯方式,为了入门操作
- Work queues--一个队列被多个消费者消费
- Publish/Subscribe--手动创建交换机(FANOUT)
- Routing--手动创建交换机(FANOUT)
- Topic--手动创建Exchange(DIRECT)
- RPC--RPC方式
- Publisher Confirms--保证消息可靠性
2 构建Connection类
2.1 导入依赖:amqp-client
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.9.0</version>
</dependency>
2.2 构建工具类
public class RabbitMQConnectionUtil {
//RabbitMQ的地址
public static final String RABBITMQ_HOST = "139.155.78.166";
//端口
public static final int RABBITMQ_PORT = 5672;
//用户名
public static final String RABBITMQ_USERNAME = "guest";
//密码
public static final String RABBITMQ_PASSWORD = "guest";
//virtual host
public static final String RABBITMQ_VIRTUAL_HOST = "/";
//构建RabbitMQ的连接对象
public static Connection getConnection() throws Exception {
//1. 创建Connection工厂
ConnectionFactory factory = new ConnectionFactory();
//2. 设置RabbitMQ的连接信息
factory.setHost(RABBITMQ_HOST);
factory.setPort(RABBITMQ_PORT);
factory.setUsername(RABBITMQ_USERNAME);
factory.setPassword(RABBITMQ_PASSWORD);
factory.setVirtualHost(RABBITMQ_VIRTUAL_HOST);
//3. 返回连接对象
Connection connection = factory.newConnection();
return connection;
}
}
3 Hello World
在这种最简单的通讯模式下只有一个生产者,一个消费者,用的是默认的交换机,队列是自己创建的
3.1 生产者
public class Publisher {
public static final String QUEUE_NAME = "hello";
@Test
public void publish() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4. 发布消息
String message = "Hello World!";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送成功!");
}
}
构建队列时
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
参数解读:
queue队列名称durable是否需要持久化,若为true,则当服务重启的时候,队列将重新创建exclusive是否是一个排外的队列,当前队列只允许一个消费者消费autoDelete自动删除,当一个队列长时间没有使用的时候,会被删掉arguments其他参数 发布消息时channel.basicPublish("",QUEUE_NAME,null,message.getBytes());参数解读- 交换机的名称
默认交换机,就是
"" - 路由规则 routingKey是什么就路由到哪个队列,所以这里也是队列名称
- 消息的其他参数
- 消息内容
3.2 消费者
public class Consumer {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(Publisher.QUEUE_NAME, false, false, false, null);
//4. 监听消息
DefaultConsumer callback = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者获取到消息:" + new String(body, "UTF-8"));
}
};
channel.basicConsume(Publisher.QUEUE_NAME, true, callback);
System.out.println("开始监听队列");
System.in.read();
}
}
消费者构建队列时,参数需要与生产者一致。
监听消息channel.basicConsume(Publisher.QUEUE_NAME, true, callback);参数详解
- 监听的队列名称
- autoACK
- 回调函数
4 Work Queues
生产者还是通过默认的交换机把消息推送到队列中,但是有多个消费者同时消费这一个队列中的消息。
- RabbitMQ有一个特点,一个队列中的消息只会被一个消费者成功的消费。
- 默认情况下,RabbitMQ会将消息以轮询的方式交给不同的消费者。
- 消费者拿到消息之后,需要给RabbitMQ一个ack,只有当RabbitMQ收到这个ack,RabbitMQ才会认为消费者已经拿到消息了。当消费者拿到了消息,却没有发送ack的时候,则RabbitMQ将会把这条消息标记为unack的状态,并发送给其他消费者消费。
4.1 生产者
生产者和Hello World的形式是一样的,都是将消息推送到默认交换机。
public class Publisher {
public static final String QUEUE_NAME = "work";
@Test
public void publish() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4. 发布消息
for (int i = 0; i < 10; i++) {
String message = "Hello World!" + i;
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
System.out.println("消息发送成功!");
}
}
4.2 消费者
让消费者关闭自动ack,并且设置消息的流控,最终实现消费者可以尽可能去多消费消息
public class Consumer {
@Test
public void consume1() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(Publisher.QUEUE_NAME, false, false, false, null);
//3.5 设置消息的流控
channel.basicQos(3);
//4. 监听消息
DefaultConsumer callback = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者1号-获取到消息:" + new String(body, "UTF-8"));
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume(Publisher.QUEUE_NAME, false, callback);
System.out.println("开始监听队列");
System.in.read();
}
@Test
public void consume2() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(Publisher.QUEUE_NAME, false, false, false, null);
channel.basicQos(3);
//4. 监听消息
DefaultConsumer callback = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者2号-获取到消息:" + new String(body, "UTF-8"));
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume(Publisher.QUEUE_NAME, false, callback);
System.out.println("开始监听队列");
System.in.read();
}
}
5 Publish/Subscribe
自行构建Exchange并绑定指定队列(FANOUT类型)
5.1 生产者
那么如何构建一个自定义的交换机并指定类型为FANOUT呢?并且交换机需要和指定队列绑定在一起。
使用以下代码可以设定交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
绑定交换机和队列
channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME,"");
当同一个交换机绑定两个队列的时候,交换机将把同样的数据在两个队列都放一份。
public class Publisher {
public static final String EXCHANGE_NAME = "pubsub";
public static final String QUEUE_NAME1 = "pubsub-one";
public static final String QUEUE_NAME2 = "pubsub-two";
@Test
public void publish() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
//4. 构建队列
channel.queueDeclare(QUEUE_NAME1,false,false,false,null);
channel.queueDeclare(QUEUE_NAME2,false,false,false,null);
//5. 绑定交换机和队列,使用的是FANOUT类型的交换机,绑定方式是直接绑定
channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME,"");
channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"");
//6. 发消息到交换机
channel.basicPublish(EXCHANGE_NAME,"",null,"publish/subscribe!".getBytes());
System.out.println("消息成功发送!");
}
}
6 Routing
在绑定Exchange和Queue时,需要指定好routingKey,同时在发送消息时,也需指定routingKey,只有routingKey一致时,才会把指定的消息路由到指定的Queue
6.1 生产者
绑定交换机和队列,并指定routingkey,当一个队列需要绑定多个routingkey的时候,多绑定几次即可。
channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME,"ORANGE");
当指定的routingkey没有对应的队列的时候,则数据会直接丢失。
public class Publisher {
public static final String EXCHANGE_NAME = "routing";
public static final String QUEUE_NAME1 = "routing-one";
public static final String QUEUE_NAME2 = "routing-two";
@Test
public void publish() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//4. 构建队列
channel.queueDeclare(QUEUE_NAME1,false,false,false,null);
channel.queueDeclare(QUEUE_NAME2,false,false,false,null);
//5. 绑定交换机和队列
channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME,"ORANGE");
channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"BLACK");
channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"GREEN");
//6. 发消息到交换机
channel.basicPublish(EXCHANGE_NAME,"ORANGE",null,"大橙子!".getBytes());
channel.basicPublish(EXCHANGE_NAME,"BLACK",null,"黑布林大狸子".getBytes());
channel.basicPublish(EXCHANGE_NAME,"WHITE",null,"小白兔!".getBytes());
System.out.println("消息成功发送!");
}
}
7 Topic
TOPIC类型可以编写带有特殊意义的routingKey的绑定方式
- TOPIC类型的交换机在和队列绑定时,需要以aaa.bbb.ccc..方式编写routingkey
- 其中有两个特殊字符:*(相当于占位符),#(相当通配符)
7.1 生产者
public class Publisher {
public static final String EXCHANGE_NAME = "topic";
public static final String QUEUE_NAME1 = "topic-one";
public static final String QUEUE_NAME2 = "topic-two";
@Test
public void publish() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//4. 构建队列
channel.queueDeclare(QUEUE_NAME1,false,false,false,null);
channel.queueDeclare(QUEUE_NAME2,false,false,false,null);
//5. 绑定交换机和队列,
channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME,"*.orange.*");
channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"lazy.#");
//6. 发消息到交换机
channel.basicPublish(EXCHANGE_NAME,"big.orange.rabbit",null,"大橙兔子!".getBytes());
channel.basicPublish(EXCHANGE_NAME,"small.white.rabbit",null,"小白兔".getBytes());
channel.basicPublish(EXCHANGE_NAME,"lazy.dog.dog.dog.dog.dog.dog",null,"懒狗狗狗狗狗狗".getBytes());
System.out.println("消息成功发送!");
}
}
8 RPC
因为两个服务在交互时,可以尽量做到Client和Server的解耦,通过RabbitMQ进行解耦操作
客户端可以发送一个消息到MQ队列中,之后server会监听这个队列,拿到消息后会返回一个响应给客户端,但这个响应也不是直接给客户端的,而是把响应交给RabbitMQ,生产者会监听相应队列的消息,最终完成请求的闭环。
需要让Client发送消息时,携带两个属性:
replyTo告知Server将相应信息放到哪个队列correlationId告知Server发送相应消息时,需要携带位置标示来告知Client响应的信息
8.1 客户端:
public class Publisher {
public static final String QUEUE_PUBLISHER = "rpc_publisher";
public static final String QUEUE_CONSUMER = "rpc_consumer";
@Test
public void publish() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(QUEUE_PUBLISHER,false,false,false,null);
channel.queueDeclare(QUEUE_CONSUMER,false,false,false,null);
//4. 发布消息
String message = "Hello RPC!";
String uuid = UUID.randomUUID().toString();
AMQP.BasicProperties props = new AMQP.BasicProperties()
.builder()
.replyTo(QUEUE_CONSUMER)
.correlationId(uuid)
.build();
channel.basicPublish("",QUEUE_PUBLISHER,props,message.getBytes());
channel.basicConsume(QUEUE_CONSUMER,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String id = properties.getCorrelationId();
if(id != null && id.equalsIgnoreCase(uuid)){
System.out.println("接收到服务端的响应:" + new String(body,"UTF-8"));
}
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
System.out.println("消息发送成功!");
System.in.read();
}
}
8.2 服务端
package com.luke.rabbitmqdemo.rpc;
import com.luke.rabbitmqdemo.helloworld.Publisher;
import com.luke.rabbitmqdemo.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;
import java.io.IOException;
public class Consumer {
public static final String QUEUE_PUBLISHER = "rpc_publisher";
public static final String QUEUE_CONSUMER = "rpc_consumer";
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(QUEUE_PUBLISHER,false,false,false,null);
channel.queueDeclare(QUEUE_CONSUMER,false,false,false,null);
//4. 监听消息
DefaultConsumer callback = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者获取到消息:" + new String(body,"UTF-8"));
String resp = "获取到了client发出的请求,这里是响应的信息";
String respQueueName = properties.getReplyTo();
String uuid = properties.getCorrelationId();
AMQP.BasicProperties props = new AMQP.BasicProperties()
.builder()
.correlationId(uuid)
.build();
channel.basicPublish("",respQueueName,props,resp.getBytes());
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
channel.basicConsume(QUEUE_PUBLISHER,false,callback);
System.out.println("开始监听队列");
System.in.read();
}
}