RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
RabbitMQ 是一个消息代理:它接受和转发消息。您可以将其视为邮局:当您将要投递的邮件放入邮箱时,您可以确定信件承运人最终会将邮件递送给您的收件人。在这个比喻中,RabbitMQ 是一个邮箱、一个邮局和一个信件载体。
RabbitMQ 和邮局之间的主要区别在于它不处理纸张,而是接受、存储和转发二进制数据 blob消息
一 服务器搭建
1.1 版本选择
1.1.1 erlang和RabbitMQ的版本选择
RabbitMQ是用erlang语言编写的,所以想要要运行RabbitMQ,必须先安装erlang环境。
elang版本必须与RabbitMQ版本匹配,可以通过RabbitMQ官网查看各版本的对应关系。
1.1.2 erlang和CentOS的版本选择
可以通过github官网看到,CentOS-7只可以使用Erlang23,CentOS-8才可以使用Erlang24,也支持Erlang23.
1.2 安装
1.2.1 依赖包
yum update -y
yum install socat logrotate -y
1.2.2 Erlang
-
最新版
根据github官网指导下载。1 导入签名秘钥
2 编辑 /etc/yum.repos.d/rabbitmq_erlang.repo
3 安装
-
指定旧版本
1 从github官网下载rpm。
2 上传
3 安装rpm -ivh erlang-23.3.4.8-1.el8.x86_64.rpm
4 验证Erlang是否安装成功
erl -version
1.2.3 RabbitMQ
1 从github官网下载要安装的RabbitMQ的rpm。进入官网后下滑找到下面的位置,选择要下载的rpm
2 上传rpm
3 安装rpm -ivh rabbitmq-server-3.9.11-1.el8.noarch.rpm
二 常用命令
如果rabbitmq-plugin和rabbitmqctl命令无法使用,可能是/etc/hosts文件和/etc/hostname文件不对应。把下面对应位置修改为hostname。
2.1 插件
-
查看插件
rabbitmq-plugins list
-
开启web管理插件:
rabbitmq-plugins enable rabbitmq_management
开放端口15672或关闭防火墙
firewall-cmd --zone=public --add-port=15672/tcp --permanent firewall-cmd --reload
访问地址:ip+端口15672
默认用户guest无法远程登录 -
关闭插件
rabbitmq-plugins disable rabbitmq_management
2.2 服务
-
开启rabbitmq
systemctl start rabbitmq-server
-
查看rabbitmq状态:
systemctl status rabbitmq-server
-
关闭rabbitmq:
systemctl stop rabbitmq-server
-
设置开启自动启动:
chkconfig rabbitmq-server on
2.3 用户
-
查看当前所有用户:
sudo rabbitmqctl list_users
-
添加用户:
sudo rabbitmqctl add_user username password
-
删掉用户:
sudo rabbitmqctl delete_user username
-
设置用户tag
sudo rabbitmqctl set_user_tags username administrator
-
赋予用户vhost(虚拟主机)的全部操作权限:
sudo rabbitmqctl set_permissions -p / username ".*" ".*" ".*"
-
查看用户权限
sudo rabbitmqctl list_user_permissions username
三、开发使用
3.1 环境搭建
-
创建项目,引入依赖;
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
-
创建虚拟主机、用户,用户关联虚拟主机;
-
配置文件:
spring: rabbitmq: host: 192.168.10.128 port: 5672 username: ems password: ems virtual-host: /ems
3.2 五种模式
3.2.1 工作队列模式
工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务而不得不等待它完成。相反,我们安排任务稍后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的消费者将弹出消息并最终执行任务。当您运行许多消费者时,任务将在他们之间共享。
生产者P发送消息到队列,消费者C从队列取出消息,执行任务。当多个消费者绑定同一个队列,消息将被他们轮询消费。
3.2.1.1 消息的发布与接收
-
发布
@Test void workQueue() { for (int i = 1; i < 11; i++) { Message message = MessageBuilder.withBody(("work queue message-" + i + "!").getBytes()).build(); // 设置消息是否持久化 message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); rabbitTemplate.convertAndSend("workQueue", message); } }
-
接收
@Component public class WorkQueueCustomerTest { @RabbitListener(queuesToDeclare = @Queue(value = "workQueue", durable = "false")) public void customer1(Message massage, Channel channel) throws IOException { channel.basicQos(1); System.out.println("massage-1 = " + new String(massage.getBody(), StandardCharsets.UTF_8)); channel.basicAck(massage.getMessageProperties().getDeliveryTag(), true); } @RabbitListener(queuesToDeclare = @Queue(value = "workQueue", durable = "false")) public void customer2(Message massage, Channel channel) throws IOException { channel.basicQos(1); System.out.println("massage-2 = " + new String(massage.getBody(), StandardCharsets.UTF_8)); channel.basicAck(massage.getMessageProperties().getDeliveryTag(), true); } @RabbitListener(queuesToDeclare = @Queue(value = "workQueue", durable = "false")) public void customer3(Message massage, Channel channel) throws IOException { channel.basicQos(1); System.out.println("massage-3 = " + new String(massage.getBody(), StandardCharsets.UTF_8)); channel.basicAck(massage.getMessageProperties().getDeliveryTag(), true); } @RabbitListener(queuesToDeclare = @Queue(value = "workQueue", durable = "false")) public void customer4(Message massage, Channel channel) throws IOException { channel.basicQos(1); System.out.println("massage-4 = " + new String(massage.getBody(), StandardCharsets.UTF_8)); channel.basicAck(massage.getMessageProperties().getDeliveryTag(), true); } @RabbitListener(queuesToDeclare = @Queue(value = "workQueue", durable = "false")) public void customer5(Message massage, Channel channel) throws IOException { channel.basicQos(1); System.out.println("massage-5 = " + new String(massage.getBody(), StandardCharsets.UTF_8)); channel.basicAck(massage.getMessageProperties().getDeliveryTag(), true); } }
3.2.1.2 消息确认
RabbitMQ 将消息传递给消费者后,它会立即将其标记为删除。而消费者可能在接到消息,处理完任务之前死亡,这会导致消息丢失。为了确保消息永远不会丢失,RabbitMQ 支持 消息确认。消费者发回确认消息,告诉 RabbitMQ 特定消息已被接收、处理,并且 RabbitMQ 可以自由删除它。
自动消息确认:可以提高吞吐量,但无法保证数据安全,因此仅推荐给能够以稳定的速率高效处理交付的消费者。
手动消息确认:保证数据安全性,不会丢失数据。通常与有界通道预取一起使用,该预取限制通道上未完成(“正在进行”)交付的数量。因此会降低吞吐量。
默认开启手动确认
-
消费者确认消息已成功消费
@RabbitListener(queues = "workQueue") public void customer1(Message massage, Channel channel) throws IOException { // 通道上允许的最大未确认消息数,值为0 被视为无限,100 到 300 范围内的值通常可提供最佳吞吐量 channel.basicQos(1); // 打印获取到的消息 System.out.println("massage-1 = " + new String(massage.getBody(), StandardCharsets.UTF_8)); // 确认消息已成功消费。其中第一个参数是消息的唯一标识,第二个参数是‘是否同时确认所有消息的消费情况’(批处理,可以减少网络流量) channel.basicAck(massage.getMessageProperties().getDeliveryTag(), false); }
-
消费者确认消息未成功消费
@RabbitListener(queues = "workQueue") public void customer1(Message massage, Channel channel) throws IOException { // 通道上允许的最大未确认消息数,值为0 被视为无限,100 到 300 范围内的值通常可提供最佳吞吐量 channel.basicQos(1); // 打印获取到的消息 System.out.println("massage-1 = " + new String(massage.getBody(), StandardCharsets.UTF_8)); // 确认消息未成功消费。其中第一个参数是消息的唯一标识,第二个参数是‘是否同时确认所有消息的消费情况’(批处理,可以减少网络流量),第三个参数是‘是否重新排队’(false不排队将丢失消息) channel.basicNack(massage.getMessageProperties().getDeliveryTag(), false, true); }
或
@RabbitListener(queues = "workQueue") public void customer1(Message massage, Channel channel) throws IOException { // 打印获取到的消息 System.out.println("massage-1 = " + new String(massage.getBody(), StandardCharsets.UTF_8)); // 确认消息未成功消费。其中第一个参数是消息的唯一标识,第二个参数是‘是否重新排队’(false不排队将丢失消息) channel.basicReject(massage.getMessageProperties().getDeliveryTag(), true); }
3.2.1.3 消息持久化
当RabbitMQ退出或崩溃时,会丢失队列与消息。要降低消息丢失的几率,我们需要将队列与消息都设置为持久的(durable),这样RabbitMQ重启后队列与消息仍然存在,并可以被继续消费。
消息持久化并不能保证消息一定不会丢失。比如RabbitMQ收到消息但还没来得及保存,也不是所有消息都会写入磁盘,可能只是保存到缓存中。如果需要更强的保证,那么可以使用 发布者确认。
3.2.2 发布-订阅模式
向多个消费者传递同一条消息(广播),这种模式就是发布-订阅模式。
生产者将消息发送到交换机,交换机将消息发送给所有绑定的消息队列,消费者再从对应的消息队列中取出消息。即交换机可以把一条消息发送给多个消息队列,但消息队列只能把一条消息发送给一个消费者,从而实现发布-订阅模式。
该模式是在工作队列模式上的改进。工作队列模式是一对一,发布-订阅模式是一对多。
就算在工作队列模式中,我们也不是直接向队列发送消息,而是将消息发送给默认交换机,然后交换机会将消息发送给指定的队列。
3.2.2.1 发布消息
发布消息的时候指定交换机的名称,不指定路由标识(第二个入参 routingKey 为空)。
@Test
void fanout() {
rabbitTemplate.convertAndSend("fanout-test", "", "fanout test!");
}
3.2.2.2 订阅消息
绑定要订阅的交换机名称,交换机类型设置为 fanout(ExchangeTypes.FANOUT)。队列可以不设置具体信息,会自动创建一个名字随机的、独占的、自动删除的队列。
@Component
public class FanoutCustomerTest {
@RabbitListener(bindings = {
@QueueBinding(value = @Queue, exchange = @Exchange(value = "fanout-test", type = ExchangeTypes.FANOUT))
})
public void customer1(String massage) {
System.out.println("message-1 = " + massage);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue, exchange = @Exchange(value = "fanout-test", type = ExchangeTypes.FANOUT))
})
public void customer2(String massage) {
System.out.println("message-2 = " + massage);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue, exchange = @Exchange(value = "fanout-test", type = ExchangeTypes.FANOUT))
})
public void customer3(String massage) {
System.out.println("message-3 = " + massage);
}
}
3.2.3 路由模式
路由模式是在发布-订阅模式上的改进。发布-订阅相当于无脑广播,而路由模式会根据路由标识(routingKey)进行精准广播。
3.2.3.1 发布消息
第一个参数为交换机名称,第二个参数为路由标识,只有拥有该路由标识的队列才会收到该消息,第三个参数为要发送的消息。
@Test
void routing() {
rabbitTemplate.convertAndSend("routing-test", "info", "info routing test!");
rabbitTemplate.convertAndSend("routing-test", "error", "error routing test!");
}
3.2.3.2 路由消息
路由模式中交换机使用 direct(ExchangeTypes.DIRECT) 模式,也是交换机的默认模式。
其中 @QueueBinding 中的 key就是要绑定的路由标识(routingKey)。
@Component
public class RoutingCustomerTest {
@RabbitListener(bindings = {
@QueueBinding(value = @Queue, exchange = @Exchange(value = "routing-test"), key = {"info", "error"})
})
public void customer1(String massage) {
System.out.println("message-1 = " + massage);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue, exchange = @Exchange(value = "routing-test"), key = {"error"})
})
public void customer2(String massage) {
System.out.println("message-2 = " + massage);
}
}
3.2.4 主题模式
主题模式是在路由模式上的改进,它可以支持动态匹配路由标识。
在主题模式中,路由标识(routingKey)必须是一个由点分割的单词列表(如:student.teacher.name),路由标识中可以有任意多个单词,但最大大小为255字节。
其中通过 *(星号)和 #(井号)来实现动态匹配
- *(星号):匹配一个单词
- #(井号):匹配零个或多个单词
如果都使用 # 绑定,就相当于发布-订阅模式;
如果都不使用 # 或 *,就相当于路由模式
3.2.4.1 发布消息
第一个参数为交换机名称,第二个参数为路由标识,第三个参数为要发送的消息。
@Test
void topics() {
rabbitTemplate.convertAndSend("topics-test", "user", "user topics test!");
rabbitTemplate.convertAndSend("topics-test", "name.password", "name.password topics test!");
rabbitTemplate.convertAndSend("topics-test", "user.name.password", "user.name.password topics test!");
}
3.2.4.2 接收消息
主题模式绑定的交换机类型为 topic(ExchangeTypes.TOPIC),@QueueBinding 的 key 中设置匹配路由标识的规则。
@Component
public class TopicsCustomerTest {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "topics-test", type = ExchangeTypes.TOPIC),
key = {"user.name.*"}
)
})
public void customer1(String massage) {
System.out.println("message-1 = " + massage);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "topics-test", type = ExchangeTypes.TOPIC),
key = {"user.*"}
)
})
public void customer2(String massage) {
System.out.println("message-2 = " + massage);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "topics-test", type = ExchangeTypes.TOPIC),
key = {"user.#"}
)
})
public void customer3(String massage) {
System.out.println("message-3 = " + massage);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "topics-test", type = ExchangeTypes.TOPIC),
key = {"*.name.#"}
)
})
public void customer4(String massage) {
System.out.println("message-4 = " + massage);
}
}
3.2.5 远程过程调用(RPC)模式
四、集群
-
同步erlang cookie,可以在其中一台服务器找到.erlang.cookie文件,复制到其他服务器上。
/var/lib/rabbitmq/.erlang.cookie
-
在后台启动所有服务
rabbitmq-server -detached
注:在后台启动时,无法查看web页面。
-
停掉主节点以外的其他服务
rabbitmqctl stop_app
-
其他服务加入主节点
rabbitmqctl join_cluster rabbit@rabbit1 # 其中rabbit1为主节点主机名称
-
启动其他节点服务
rabbitmqctl start_app
这时可以访问web页面了。
-
查看集群状态,任意节点执行:
rabbitmqctl cluster_status
也可以登录web页面查看。
注:此时的集群,从节点只能备份交换机,无法备份队列,只是可以看到队列,如果此时主节点宕机,其他节点无法代替主节点对外提供服务。此时的集群被称为普通集群,只有分摊主节点请求压力的作用。
-
查看当前策略:
rabbitmqctl list_policies
-
添加策略:
rabbitmqctl set_policy [-p <vhost>] [--priority <priority>] [--apply-to <apply-to>] <name> <pattern> <definition>
其中,-p vhost 可选参数,针对vhost下的queue进行设置;
ha-mode是policy(策略)的优先级,可选参数(数字越大级别越高);
name是policy的名称;
pattern是queue的匹配模式(正则表达式);
definition是镜像定义,包括三个部分:- ha-mode:指明镜像队列的模式,有all、exactly、nodes
all:表示在集群中所有节点上进行镜像;
exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定:
nodes:表示在指定的节点上进行镜像,节点名称由ha-params指定: - ha-params:ha-mode模式需要用到的参数;
- ha-sync-mode:进行队列中消息同步的方式,有automatic和manual;
比如:ha-all策略对以hello开头的所有队列以automatic同步消息的方式进行镜像:
rabbitmqctl set_policy ha-all "^hello" '{"ha-mode":"all","ha-sync-mode":"automatic"}'
- ha-mode:指明镜像队列的模式,有all、exactly、nodes
-
删除策略:
rabbitmqctl clear_policy ha-all