1. 介绍
RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议。
RabbitMQ目前支持7种模型,还有以下特点:
- 开源、性能优秀,稳定性保障
- 提供可靠性消息投递模式、返回模式
- 与Spring AMQP完美整合,API丰富
- 集群模式丰富,表达式配置,HA模式,镜像队列模型
- 保证数据不丢失的前提做到高可靠性、可用性
2. 安装(Windows版本)
-
下载安装RabbitMQ安装包和Erlang语言
-
下载完Erlang语言后,设置环境变量
-
新建系统变量
ERLANG_HOME,值为安装路径 -
编辑系统变量的
path变量,新建%ERLANG_HOME%\bin
-
-
安装完RabbitMQ后,进入安装目录
sbin文件夹,使用以下命令开启管理界面rabbitmq-plugins.bat enable rabbitmq_management -
在开始菜单里可以开启RabbitMQ服务,进入
http://127.0.0.1:15672网址,默认账号密码为guest/guest
2.1 配置虚拟主机
3. Hello World模型
3.1 引入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
3.2 生产者
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的用户
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 获取连接对象
Connection connection = connectionFactory.newConnection();
// 获取连接的通道
Channel channel = connection.createChannel();
// 通道绑定对应消息队列
// 参数1:队列名称,队列不存在则会自动创建
// 参数2:队列特性是否持久化
// 参数3:是否为独占队列(即只有当前连接可用)
// 参数4:是否消费完成后删除队列
// 参数5:额外参数
channel.queueDeclare("hello", false, false, false, null);
// 发布消息
// 参数1:交换机
// 参数2:队列名称
// 参数3:传递消息的额外设置,MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化
// 参数4:消息的字节数组
channel.basicPublish("", "hello", null, "hello RabbitMq".getBytes());
channel.close();
connection.close();
}
发送完后管理页面会看到有一条消息,且未被消费
3.3 消费者
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/ems");
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", false, false, false, null);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 最后一个参数是队列中取出的消息
System.out.println("msg = " + new String(body));
}
};
// 消费消息,会消费全部消息
// 参数1:队列名称
// 参数2:消息的自动确认机制,见章节4.4
// 参数3:消费消息时的回调接口
channel.basicConsume("hello", true, consumer);
// 如果这里把连接关闭,会出现来不及用回调函数打印的情况
// channel.close();
// connection.close();
}
3.4 提炼工具类
public class RabbitMqUtils {
private static ConnectionFactory factory;
static {
factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setVirtualHost("/ems");
factory.setUsername("guest");
factory.setPassword("guest");
}
public static Connection getLocalConnection() {
try {
return factory.newConnection();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
return null;
}
public static void closeConnectionAndChannel(Connection connection, Channel channel) {
try {
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
}
4. Work queues模型
4.1 生产者
public static void main(String[] args) throws IOException {
Connection localConnection = RabbitMqUtils.getLocalConnection();
Channel channel = localConnection.createChannel();
channel.queueDeclare("work", true, false, false, null);
for (int i = 1; i <= 10; i++) {
String msg = "我是消息" + i;
channel.basicPublish("", "work", null, msg.getBytes());
}
RabbitMqUtils.closeConnectionAndChannel(localConnection, channel);
}
4.2 消费者
// 多个消费者的代码一样,只是消费者名字不同
public static void main(String[] args) throws IOException {
Connection localConnection = RabbitMqUtils.getLocalConnection();
Channel channel = localConnection.createChannel();
channel.queueDeclare("work", true, false, false, null);
channel.basicConsume("work", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1: " + new String(body));
}
});
}
4.3 运行结果
-
默认情况下,各个消费者消费的消息是一样多的,即各个消费者之间是轮询关系
-
在SpringBoot整合的AMQP中,默认是公平消费,如需实现"能者多劳"则要额外配置
4.4 消费者的消息自动确认机制
即消费者自动向RabbitMQ确认消息消费了。这种机制下,不会关心消费者拿到消息后是否处理完业务代码,只告诉RabbitMQ消费者已经消费了消息,这个时候RabbitMQ就会删除掉分配给该消费者的消息。就会出现以下场景:假如消费者分配了5条消息,但是在处理第3条的时候宕机了,但由于自动确认机制,RabbitMQ已经删除掉了这5条消息,那么就会造成消息丢失,所以一般都会关掉该机制。
public static void main(String[] args) throws IOException {
Connection localConnection = RabbitMqUtils.getLocalConnection();
Channel channel = localConnection.createChannel();
// 一次消费一条消息
channel.basicQos(1);
channel.queueDeclare("work", true, false, false, null);
// 第二个参数置为false,关闭自动确认机制
channel.basicConsume("work", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1: " + new String(body));
// 执行完业务代码后,手动告诉RabbitMQ已经消费了该消息
// 参数1:手动确认消息标识,确认队列的哪个具体消息
// 参数2:是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
5. Publish/Subscribe模型
即发布/订阅(广播)模式,在该模式下:
- 可以有多个消费者,每个消费者都有自己的队列
- 每个队列都要绑定到交换机
- 生产者发布的消息,只能发送到交换机,由交换机决定发给哪个队列,生产者无法决定
- 交换机把消息发送给绑定过的队列,队列对应的消费者消费消息,实现一条消息被多个消费者消费
5.1 生产者
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getLocalConnection();
Channel channel = connection.createChannel();
// 指定交换机
// 参数1:交换机名称
// 参数2:交换机类型(fanout:广播类型)
channel.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
// 发布消息
// 参数1:交换机名称
// 参数2:路由key
// 参数3:传递消息的额外设置,MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化
// 参数4:消息的字节数组
channel.basicPublish("logs", "", null, "fanout type msg".getBytes());
RabbitMqUtils.closeConnectionAndChannel(connection, channel);
}
5.2 消费者
// 多个消费者的代码基本一致,只是消费消息的业务代码不同
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getLocalConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机
channel.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
// 临时队列的名字
String tempQueueName = channel.queueDeclare().getQueue();
// 绑定交换机和临时队列
// 参数1:队列名字
// 参数2:交换机名字
// 参数3:路由key
channel.queueBind(tempQueueName, "logs", "");
// 消费消息
channel.basicConsume(tempQueueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1: " + new String(body));
}
});
}
6. Routing模型
6.1 Direct(直连)
在广播模式下,一条消息会被所有订阅的队列消费。但是在某些场景下,我们希望不同的消息被不同的队列消费,这个时候就需要用到Direct类型的交换机。在该模式下:
- 交换机与队列的绑定,不再是任意绑定,而是要指定一个RoutingKey(路由key)
- 生产者向交换机发送消息时,也需要指定RoutingKey
- 交换机不会再把消费发送给每个订阅的队列,而是根据RoutingKey进行判断,当队列的RoutingKey和消息的RoutingKey一致时,才会接收消息
6.1.1 生产者
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getLocalConnection();
Channel channel = connection.createChannel();
// 参数分别是:交换机名称,交换机类型
channel.exchangeDeclare("logs_direct", BuiltinExchangeType.DIRECT);
// 发布消息
String routingKey = "error";
channel.basicPublish("logs_direct", routingKey, null, ("这是" + routingKey + "级别的日志").getBytes());
RabbitMqUtils.closeConnectionAndChannel(connection, channel);
}
6.1.2 消费者
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getLocalConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("logs_direct", BuiltinExchangeType.DIRECT);
String queueName = channel.queueDeclare().getQueue();
// 基于路由key绑定交换机和队列
channel.queueBind(queueName, "logs_direct", "info");
channel.queueBind(queueName, "logs_direct", "warning");
channel.queueBind(queueName, "logs_direct", "error");
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-2: " + new String(body));
}
});
}
示例中的消费者绑定了3种路由key,其他消费者可以根据具体的业务需求来绑定对应的路由key,来实现不同的消息可以被不同的队列消费。
6.2 Topics
Topics和Direct相似,都是可以根据路由key把消息发到不同的队列,但是Topics类型的交换机可以让队列在绑定路由key的时候使用通配符。这种通配符一般由一个或多个单词组成,多个单词之间以.分割,如item.insert。*可以匹配一个单词,#可以匹配一个或多个单词。
6.2.1 生产者
生产者代码与Direct类型的相似 ,只是声明的交换机的类型变了
channel.exchangeDeclare("logs_direct", BuiltinExchangeType.TOPIC);
// 路由key使用多个单词的形式
String routingKey = "user.save";
6.2.2 消费者
消费者的改动也一样
channel.exchangeDeclare("logs_direct", BuiltinExchangeType.TOPIC);
String queueName = channel.queueDeclare().getQueue();
// 基于路由key绑定交换机和队列
channel.queueBind(queueName, "logs_direct", "user.*");
7. SpringBoot整合
7.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
7.2 配置application.yml
spring:
application:
name: rabbitmq_app
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /ems
7.3 各模型生产者
@SpringBootTest(classes = RabbitMQApp.class)
@RunWith(SpringRunner.class)
public class TestRabbitMQ {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testHelloWorld() {
rabbitTemplate.convertAndSend("hello", "HelloWorld模型");
}
@Test
public void testWorkQueues() {
for (int i = 1; i <= 10; i++) {
rabbitTemplate.convertAndSend("work", "WorkQueues模型" + i);
}
}
@Test
public void testFanout() {
rabbitTemplate.convertAndSend("logs", "", "Fanout模型");
}
@Test
public void testRoutingDirect() {
String routingKey = "error";
String msg = "RoutingDirect模型:这是" + routingKey + "级别的日志";
rabbitTemplate.convertAndSend("routingDirect", routingKey, msg);
}
@Test
public void testRoutingTopics() {
String routingKey = "user.delete";
String msg = "RoutingTopics模型:" + routingKey;
rabbitTemplate.convertAndSend("routingTopics", routingKey, msg);
}
}
7.4 各模型消费者
@Component
@RabbitListener(queuesToDeclare = @Queue(value = "hello"))
public class HelloCustomer {
@RabbitHandler
public void receive(String message) {
System.out.println("message = " + message);
}
}
@Component
public class WorkCustomer {
@RabbitListener(queuesToDeclare = @Queue(value = "work"))
public void receive1(String message) {
System.out.println("消费者1:" + message);
}
@RabbitListener(queuesToDeclare = @Queue(value = "work"))
public void receive2(String message) {
System.out.println("消费者2:" + message);
}
}
@Component
public class FanoutCustomer {
@RabbitListener(bindings = {
// @Queue不加名字则表明创建临时队列
@QueueBinding(value = @Queue, exchange = @Exchange(value = "logs", type = "fanout")),
})
public void receive1(String message) {
System.out.println("消费者-1:" + message);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue, exchange = @Exchange(value = "logs", type = "fanout")),
})
public void receive2(String message) {
System.out.println("消费者-2:" + message);
}
}
@Component
public class RoutingDirectCustomer {
@RabbitListener(bindings = {
@QueueBinding(value = @Queue,
exchange = @Exchange(value = "routingDirect", type = "direct"),
key = {"info", "warning", "error"})
})
public void receive1(String message) {
System.out.println("消费者-1:" + message);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue,
exchange = @Exchange(value = "routingDirect", type = "direct"),
key = {"error"})
})
public void receive2(String message) {
System.out.println("消费者-2:" + message);
}
}
@Component
public class RoutingTopicsCustomer {
@RabbitListener(bindings = {
@QueueBinding(value = @Queue,
exchange = @Exchange(value = "routingTopics", type = "topic"),
key = {"user.delete"})
})
public void receive1(String message) {
System.out.println("消费者-1:" + message);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue,
exchange = @Exchange(value = "routingTopics", type = "topic"),
key = {"user.*"})
})
public void receive2(String message) {
System.out.println("消费者-2:" + message);
}
}