RabbitMQ
简介
官方文档:https://www.rabbitmq.com/documentation.html
RabbitMQ是实现了AMQP协议的高性能开源的消息队列技术。RabbitMQ支持多种语言、多种框架的客户端调用。它有生产者和消费者两种角色,生产者是将消息数据发送给MQ处理,消费者是监听MQ队列中产生的消息,获取消息。
特点及适用场景:
- 分布式系统下,具有异步、削峰、限流、负载均衡等高级功能;
- 实现生产者和消费者之间的解耦;
- 拥有持久化机制(收到消息后,会先提交日志文件后才发送响应);
- 实现服务间的异步通讯;
- 实现定时任务;
核心组件:
- 连接对象:使用基于TCP的长连接,但并不是直接调用连接操作,不需要连接池提升并发性能,并发由MQ集中处理;直接操作是基于长连接建立channel连接信道对象来实现,消耗资源少,可以频繁创建、销毁;
- 交换机(exchange):由并发语言erlang编写,并发能力极高,生产者是将消息发送给交换机处理,由交换机处理并发。
- 队列组件(queue):在内存中管理,有固定的结构,绑定对应的交换机,如果交换机没有队列绑定,则该消息会直接消失,若队列中有多个消费者,则消息将以循环的方式发送给消费者。
安装
检查安装erlang:
- 检查erl版本,若版本不对需要先卸载(在rabbit官网查看对应版本);
- 卸载erl(执行
yum list | grep erlang
只要有结果就执行yum remove xxx
); - erlang官网下载对应版本的源码包;
- 安装erl依赖:
yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc
; - 解压并进入目录执行:
./configure --prefix=/data/otp_erlang --without-javac
; - 安装erlang:
make && make install
; - 添加erlang的环境变量;
安装socat(MQ内部调用执行的一些插件命令环境);
- 搜索下载socat源码包->解压->
./configure
->make && make install
;
安装rabbitMQ(上面的方式安装的erl不能安装mq的rpm包);
- 官网下载:rabbitmq-server-generic-unix-3.7.14.tar.xz;
- xz -d解压->tar解压;
- 将sbin目录配置到环境变量;
启动及初始化:
- 启用web管理页面:
rabbitmq-plugins enable rabbitmq_management
; - 启动:
rabbitmq-server
,加参数-detached
为后台运行,关闭:rabbitmqctl stop
- 添加用户:
rabbitmqctl add_user root 1234
;(guest只能本地登录) - 授予管理员权限:
rabbitmqctl set_user_tags admin administrator
; - 在浏览器通过root/1234在
http://ip:15672
登录(生产者、消费者访问端口是5672); - 添加virtual host为"/"的权限;
使用(可以直接通过web页面添加用户了)
- 添加用户(Admin->Users下);
- 添加虚拟机(Admin->virtualHost下);
- 绑定用户与虚拟机(点击用户名->选择虚拟机);
四种交换机
direct(默认)
名称为空字符串的交换机,会使用队列名称作为路由Key的交换机。发送端和接收端都不需要创建或者声明交换机。该交换机具有以下特点:
- 公平调度:direct会轮训公平的分发给每个消费者(需要消息确认正确);
- 消息发后即忘:既接受者不知道消息的来源,如果要指定来源,需要包含在消息体中;
- 消息确认:若消费者消息收到未确认,则不会在发送更多消息,在消息确认之前,可以与rabbitmq断开连接,则消息回到ready状态,发送给其他消费者,也可以拒绝消息(
channel.basicReject
),根据参数确定是发给其他消费者还是放入死信。(经过测试,当使用QueueingConsumer
时,即使没有确认消息,一直调用接收方法也可以收到消息。)
fanout
发布订阅模式,会广播消息到所有绑定该交换机的队列。使用时在需要先创建/声明交换并指定名称和类型(channel.exchangeDeclare("name", "fanout")
),还需要将需要接收的队列绑定到该交换机。(该交换机会忽略路由Key参数)
topic
和fanout类似,但可以通过路由Key更灵活的匹配队列。使用该交换机时需要先创建或者声明交换机(channel.exchangeDeclare(“name”, "topic")
),同时,在发送消息和绑定队列时还需要指定路由Key。路由Key名称不得超过255字节,用"."分割多个关键字,还可以使用通配符(#
:匹配0或者多个字符;*
匹配一个分段的关键字)。另外topic和fanout都是没有历史数据的,中途创建的队列不能接收之前的消息。
header
功能和direct一样,不同之处在于它是使用消息的header部分匹配队列,性能很差,没用。
使用模式
https://www.rabbitmq.com/getstarted.html
- 简单模式(一对一):交换机收到消息,若没有绑定队列则消息扔到垃圾桶,否则发送给队列,队列接收消息并存储在内存,等待消费者连接监听,消费成功后返回确认。(QQ、短信)
- 工作模式(资源争抢):一个队列被多个消费者同时监听,交换机收到消息,发送给绑定的后端队列。(抢红包)
- 发布订阅(交换机类型:fanout):交换机后绑定很多队列,收到消息后,交换机复制同步消息到后端所有队列中。(邮件群发、广告)
- 路由模式(交换机类型:direct):队列绑定交换机时提供了一个路由key(routingKey),(发布订阅时,所有fanout类型的交换机的后端队列的路由key都是"");(错误消息的接收展示);
- 主题模式(交换类型:topic):与路由模式很相似,做到按类划分消息,且路由key可以使用通配符:#(任意字符)、*(没有特殊字符的字符串)。
消息确认机制
生产端:即生产者投递消息后,如果broker收到消息,则会给生产者一个应答,生产者可以接收应答,以确保这条消息正常发送;开启方式如下:
- 发送消息前,在channel上开启确认模式:
channel.confirmSelect()
- channel上添加监听:
channel.addConfirmListener(ConfirmListener listener)
;根据返回结果确定后续处理; - 也可以添加监听:
channel.addReturnListener(ReturnListenerlistener)
,获取更详细的确认消息;
说明:开启消息确认模式后,则在该信道上发布的所有消息都会被指派一个唯一的id,消息保存成功后,信道会异步的向生产者发送一个包含id的确认,如果保存失败则会发送nack消息;
消费端:消费者在声明队列时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息,也可以通过basicNack(long deliveryTag, boolean multiple, boolean requeue)
方法设置消息重回队列;默认情况下,消息的消费具有如下特点:
- RabbitMQ只有接收到了消费者的确认,才会安全的把消息从队列中删除;
- 如果消费者在确认消息之前断开了连接或者取消订阅,RabbitMQ会将消息重新发给下一个订阅的消费者;
- 如果消费者没有确认消息,没有断开连接,也没有取消订阅,则不会给该消费者分发更多消息;
由上可以,要保证消息的精确一次消费,就要求在消息体中包含一个全局唯一的业务id,作为去重的依据;
集群
普通模式:默认集群模式,多个节点仅有相同的元数据,消息只存在于其中一个节点,当消费者从另一个节点拉取时,消息会在集群内部传输;
搭建普通模式:
- 按单机模式的方法在多台节点安装;
- 查找文件:
find / -name .erlang.cookie
, - 将每个节点的该文件内容设置为一样,且权限为400;接着重启MQ;
镜像模式:把需要的队列做成镜像模式,存在于多个节点,消息实体会主动在镜像节点之间同步;
搭建镜像模式:镜像模式是基于普通模式的,在普通模式的基础上,直接在web页面配置策略即可:admin->Policies
;(集群中任意节点启用策略,策略会自动同步到集群节点)
ha-mode
:
- all:所有节点都会同步镜像;
- exactly:指定数量的节点同步镜像;若节点数量少于此数,则所有节点都同步;若多于次数,则当一个包含镜像的节点停止时,不会在另外的节点创建镜像,即不会发生迁移;
- nodes:指定节点列表同步镜像;
Java客户端
官方文档:https://www.rabbitmq.com/api-guide.html
初始化
初始化连接工厂:
private Channel channel;
@Before
public void init(){
ConnectionFactory factory= new ConnectionFactory();
// 配置连接参数(前面创建的用户/虚拟机)
factory.setUsername("demo");
factory.setPassword("1234");
factory.setVirtualHost("/demoHost");
factory.setHost("192.168.48.101");
factory.setPort(5672);
// 也可以这样
// factory.setUri("amqp://demo:1234@192/168.48.101:5672/demoHost");
// 从连接工厂获取长连接
try{
Connection conn = factory.newConnection();
//从长连接获取短连接信道对象
channel=conn.createChannel();
}catch(Exception e){
e.printStackTrace();
}
// channel.close();
// conn.close();
}
简单模式
生产者:
channel.queueDeclare
方法声明/创建队列,方法参数依次为队列名称、是否持久化、是否专属连接、是否自动删除、消息的其他属性配置等,部分参数含义可以参考在web管理的新增队列页面。- 通过channel信道的
basicPublish
方法发送消息,方法参数有:交换机名称、路由Key、消息的属性、消息的byte数组,部分参数含义可以参考web管理的发送信息页面。(简单模式就使用队列名作为路由key)
消费者(已过时,有内存泄漏的风险):
channel.queueDeclare
方法声明/创建队列(如果已有队列,则该方法可用可不用);channel.basicQos
方法设置空闲时最多收到的消息数量;- 创建
QueueingConsumer
对象; channel.basicConsume
绑定队列和Consumer,同时设置自动确认/手动确认(自动确认是在消费者刚刚接收到消息就返回确认;手动确认是在手动返回确认前,消息队列会一直保留消息,消费者断开重连还可以接收该消息,直到返回确认,队列才会删除该消息);consumer.nextDelivery()
监听并接收消息,若为手动确认,则需要通过channel.basicAck
方法返回确认。
消费者:
- 声明/创建队列;
- 如果需要,设置空闲时最多收到的消息数;
channel.basicConsume
方法绑定队列和消费者,消费者使用DefaultConsumer
,重载handleDelivery
方法;这种方式的消费者,只要进程没有退出,就可以一直接收消息,每次收到消息,就会执行handleDelivery方法。该方式中:- 自动确认:先接收队列中所有消息,然后对每个消息执行一次重载的方法完毕就返回确认,
- 手动确认:每次手动调用
channel.basicAck
方法后才会接收下一个消息。
//生产者
@Test
public void senderTest() throws IOException {
channel.queueDeclare("queue", false, false, false, null);
String msg = "hello rabbitmq";
channel.basicPublish("", "queue", null, msg.getBytes());
}
//消费者
@Test
public void consumer() throws IOException, InterruptedException {
channel.queueDeclare(queue, false, false, false, null);
channel.basicQos(1);
//该消费者已经过时,因为有内存泄漏的风险
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queue, false, consumer);
Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者接收到消息:" + msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}
//消费者新
@Test
public void consumer02() throws IOException, InterruptedException {
channel.basicQos(1);
channel.basicConsume(queue, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
while(true){
Thread.sleep(1000);
}
}
工作模式
工作模式具体代码与简单模式大致一样,不同的是有多个消费者进程接收同一个队列的消息;
发布订阅模式
该模式结构为一个生产者和多个消费者,
消费者:在简单模式中消费者的基础上,还需要在监听消息之前:
exchangeDeclare
方法声明交换机名称和类型;queueBind
方法绑定队列和交换机,同时设置路由Key为""。
生产者:在简单模式中生产者的基础上,还需要在发布消息之前:
- 通过
exchangeDeclare
方法声明交换机名称和类型, - 声明多个消息队列,每个消费只需要绑定一个队列即可;
@Test
public void sender() throws IOException {
channel.queueDeclare("queue1", false, false, false, null);
channel.queueDeclare("queue2", false, false, false, null);
channel.exchangeDeclare("ex01", "fanout");
for (int i = 0; i < 100; i++) {
String msg = "fanout mode: " + i;
channel.basicPublish("ex01", "", null, msg.getBytes());
System.out.println("生产端发送了" + (i + 1) + "条消息到默认交换机");
}
}
@Test
public void consumer() throws IOException, InterruptedException {
channel.queueDeclare("queue1", false, false, false, null);
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume("queue01", false, consumer);
channel.exchangeDeclare("ex01", "fanout");
channel.queueBind("queue01", "ex01", "");
while (true) {
Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者01接收到消息:" + msg);
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}
}
路由模式
该模式与发布订阅模式基本一样,不同之处仅仅是:
- 交换机类型改为:
direct
- 生产者发送消息时路由Key的参数不再是"",而是具体的值;
- 消费在绑定交换机是添加了路由Key参数。
主题模式
只是在路由模式的基础上,交换机类型改为:topic
,消费者路由Key使用了通配符。
定时
RabbitMQ本身没有延迟队列,只能通过死信交换机和消息存活时间来实现;
死信交换机,其实就是个普通的交换机,用于存放过期的消息,满足以下条件的消息,就会放入该交换机:
- 当消息被拒收,且拒绝参数的requeue是false;
- 消息的过期时间到了(队列和消息都可以设置过期时间,最终会以小的为准);
- 队列长度限制满了,排在前面的消息;
实现定时的思路如下:
- 创建自动过期的消息队列,或者对消息设置过期时间(该队列不设置消费者);
- 设置消息过期后要进入的交换机;
- 创建真正的消息处理队列,并绑定导死信交换机;