Rabbitmq
消息队列解决的问题
- 异步
A系统处理用户A的请求并写到A库中需要3ms,但是在A调用B,C,D的接口时用了900多ms,对于这样的情况,A可以将响应直接返回给用户,把消息发给rabbitmq上,增强了用户的体验
- 解耦
A系统需要发送数据给B,C,D系统,以调用接口的方式,但是如果此时D系统不需要这些数据了呢?又有一个新的E系统需要这些数据呢…这样会造成系统之间的严重耦合,可以用mq的方式,A将数据发到mq上,然后需要的系统自己来取数据,这样就避免了A系统频繁的代码更改
-
削峰
当在某一段时间内用户并发暴增,达到1w+/s,那么A系统可能会向mysql发送执行sql 1w+/s,这可能会导致mysql挂掉
可以这样解决:
引入mq的缺点:
概念
Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker
Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似
于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出
多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP
Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程
序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客
户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的
Connection 极大减少了操作系统建立 TCP connection 的开销
Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发
消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout
(multicast)
Queue:消息最终被送到这里等待 consumer 取走
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保
存到 exchange 中的查询表中,用于 message 的分发依据
安装rabbitmq
mq会创建默认用户guest,但是只能在本机登录
rabbitmqctl list_users 查看所有用户
rabbitmqctl add/delete_user [username] [password] 添加/删除用户
rabbitmqctl set_user_tags [username] [角色] 为指定角色赋予权限
rabbitmqctl list_user_permissions [username] 查看角色权限
对应角色的权限:
(1) 超级管理员(administrator)
可登陆管理控制台(启用management plugin的情况下),可查看所有的信息,并且可以对用户,策略(policy)进行操作。
(2) 监控者(monitoring)
可登陆管理控制台(启用management plugin的情况下),同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
(3) 策略制定者(policymaker)
可登陆管理控制台(启用management plugin的情况下), 同时可以对policy进行管理。但无法查看节点的相关信息
与administrator的对比,administrator能看到这些内容
(4) 普通管理者(management)
仅可登陆管理控制台(启用management plugin的情况下),无法看到节点信息,也无法对策略进行管理。
(5) 其他
无法登陆管理控制台,通常就是普通的生产者和消费者。
连接mq客户端
导入依赖
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.12.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
-
工作线程
避免阻塞等待着资源密集型任务(如网络IO)的执行,可以把任务封装成消息,发送到队列,然后让后台的多个工作线程去执行它们
消息应答
- 当消费者接收并处理掉消息时,告诉rabbitmq消息已经被处理了,可以删除消息了
- 防止消费者在处理消息时突然挂掉,而rabbitmq会丢失正在处理的消息,和后续可能发送给它的消息,因为它已经无法接收消息了
-
自动应答
消息在发送后就认为已经传输成功,这种方式仅适用于消费者能够高效处理消息的场景
这种方式并没有考虑消费者或channel连接突然断开,或者消费者并不能很快的处理消息,导致接受很多来不及处理的消息 -
手动应答
Channel.basicAck(用于肯定确认) RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了 Channel.basicNack(用于否定确认) 不处理该消息了直接拒绝,可以将其丢弃了 Channel.basicReject(用于否定确认) 不能批量应答
关于mutiple参数:
为true时,会将channel里的所有消息都进行应答
为false时,只应答当前consumerTag中的消息 -
消息重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息
未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者
可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确
保不会丢失任何消息。
rabbitmq持久化
默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。
确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化
-
队列持久化
channel.queueDeclare("ack_queue",false,false,false,null); 在声明队列时将durable属性设置为true,但是不能更改已经创建的队列的持久化属性
-
消息持久化
channel.basicPublish("","ack_queue",MessageProperties.PERSISTENT_TEXT_PLAIN,scanner.next().getBytes(StandardCharsets.UTF_8)); 将props属性设置为MessageProperties.PERSISTENT_TEXT_PLAIN 将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是 这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没 有真正写入磁盘。持久性保证并不强
-
不公平分发
对于消费者处理能力差异很大的情况,不应该采用轮询分发,应该根据消费者的能力分发消息(不公平分发)
channel.basicQos(1); 将消费者设置为不公平分发(实质上设置了消息的预取数量(prefetch))
发布确认
-
概念
生产者将信道设置成confirm模式,一旦信道进入 confirm 模式,所有在该信道上面发布的
消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker
就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队
列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传
给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置
basic.ack 的 multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信
道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调
方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消
息,生产者应用程序同样可以在回调方法中处理该 nack 消息
-
开启发布确认
channel.confirmSelect();
-
单个发布确认
是一种同步确认的方式,会一直阻塞到阻塞消息被确认
waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
-
批量发布确认
也是一种同步确认方式,发送一定数量的消息后一起确认
-
异步发布确认
通过回调函数来保证消息可靠性
增加一个监听器来实现 channel.addConfirmListener( //消息确认的回调 (deliveryTag, multiple) -> { System.out.println(deliveryTag+"消息已经被确认"+multiple); }, //消息未确认的回调 (deliveryTag, multiple) -> { } );
处理确认失败的消息
采用并发跳表来完成消息的确认和删除 Channel channel = MqUtils.getChannel(); channel.confirmSelect(); ConcurrentSkipListMap<Long, Object> skipListMap = new ConcurrentSkipListMap<>(); channel.addConfirmListener( //消息确认的回调 (deliveryTag, multiple) -> { if (multiple){ ConcurrentNavigableMap<Long, Object> confirmed = skipListMap.headMap(deliveryTag);//包含小于等于该消息编号的所有消息 confirmed.clear(); }else { skipListMap.remove(deliveryTag); } System.out.println(deliveryTag+"消息已经被确认"+multiple); }, //消息未确认的回调 (deliveryTag, multiple) -> { } ); for (int i = 0; i < 1000; i++) { String message = "lala"; channel.basicPublish("","confirm_queue",null,message.getBytes(StandardCharsets.UTF_8)); System.out.println(channel.getNextPublishSeqNo()); skipListMap.put(channel.getNextPublishSeqNo()-1,message);//收集已发送的消息 }
交换机
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产
者甚至都不知道这些消息传递传递到了哪些队列中
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来
自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消
息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
-
绑定(bindings)
binding用于描述队列和交换机的绑定关系,这个角色通常由路由键来扮演
根据绑定特性的不同,交换机分为fanout,topic,direct三种
-
fanout
将消息发送到所有与它建立了绑定关系的队列上channel.exchangeDeclare("fanoutExchange","fanout"); 创建fanout交换机 channel.queueBind(queue,"fanoutExchange",""); 绑定交换机和队列
-
direct
将消息发送到指定路由键的队列中
channel.exchangeDeclare("directExchange","direct"); 创建direct交换机
-
topic
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:“stock.usd.nyse”, “nyse.vmw”,
“quick.orange.rabbit”.这种类型的。当然这个单词列表最多不能超过 255 个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词