RabbitMQ的工作机制:
首先要知道RabbitMQ的三种角色:生产者、消费者、消息服务器
- 生产者:消息的创建者,负责创建和推送消息到消息服务器
- 消费者:消息的接收方,接受消息并处理消息
- 消息服务器:其实RabbitMQ本身,不会产生和消费消息,相当于一个中转站,将生产者的消息路由给消费者
RabbitMQ的一些角色
- ConnectionFactory:连接管理,应用程序或消费方与RabbitMQ建立连接的管理器
- Channel:信道,推送消息的通道
- Exchange:交换机,用于接收分配消息到队列中
- Queue:保存消息
- Routingkey:消息会携带routingKey,决定消息最终的队列
- BindingKey:Queue通过bindingKey与交换机绑定
BindingKey是Exchange和Queue绑定的规则描述,这个描述用来解析当Exchange接收到消息时,Exchange接收到的消息会带有RoutingKey这个字段,Exchange就是根据这个RoutingKey和当前Exchange所有绑定的BindingKey做匹配,如果满足要求,就往BindingKey所绑定的Queue发送消息,这样我们就解决了我们向RabbitMQ发送一次消息,可以分发到不同的Queue的过程。
串起来就是,生产者通过ConnectionFactory连接RabbitMQ服务器,我们通过channel发送带有routingKey的消息到Exchange交换机,Exchange交换机根据消息的RoutingKey与当前Exchange所有绑定的BindingKey做匹配,如果满足要求,就往BindingKey所绑定的Queue发送消息。而却rabbitmq的routingkey还可以用来过滤从队列中取的的信息
交换机Exchange的四种类型
http://tryrabbitmq.com/这个网站提供了很好的交换机和队列消息转发的演示
- 1.Direct exchange(直连交换机) 直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列,步骤如下: 将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key) 当一个携带着路由值为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。
- 2.Fanout exchange(扇型交换机) 扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列。不同于直连交换机,路由键在此类型上不启任务作用。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的发送给这所有的N个队列
- 3.Topic exchange(主题交换机) 主题交换机(topic exchanges)中,队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。 扇型交换机和主题交换机异同: 对于扇型交换机路由键是没有意义的,只要有消息,它都发送到它绑定的所有队列上 对于主题交换机,路由规则由路由键决定,只有满足路由键的规则,消息才可以路由到对应的队列上
- 4.Headers exchange(头交换机) 类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。 此交换机有个重要参数:”x-match” 当”x-match”为“any”时,消息头的任意一个值被匹配就可以满足条件 当”x-match”设置为“all”的时候,就需要消息头的所有值都匹配成功 性能很差,一般不会使用
默认交换机
默认交换机(default exchange)实际上是一个由RabbitMQ预先声明好的名字为空字符串的直连交换机(direct exchange)。它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。
如:当你声明了一个名为”hello”的队列,RabbitMQ会自动将其绑定到默认交换机上,绑定(binding)的路由键名称也是为”hello”。因此,当携带着名为”hello”的路由键的消息被发送到默认交换机的时候,此消息会被默认交换机路由至名为”hello”的队列中。即默认交换机看起来貌似能够直接将消息投递给队列。
Dead Letter Exchange(死信交换机)
在默认情况,如果消息在投递到交换机时,交换机发现此消息没有匹配的队列,则这个消息将被悄悄丢弃。为了解决这个问题,RabbitMQ中有一种交换机叫死信交换机。当消费者不能处理接收到的消息时,将这个消息重新发布到另外一个队列中,等待重试或者人工干预。这个过程中的exchange和queue就是所谓的”Dead Letter Exchange 和 Queue”
交换机的属性
除交换机类型外,在声明交换机时还可以附带许多其他的属性,其中最重要的几个分别是:
- Name:交换机名称
- Durability:是否持久化。如果持久性,则RabbitMQ重启后,交换机还存在
- Auto-delete:当所有与之绑定的消息队列都完成了对此交换机的使用后,删掉它
- Arguments:扩展参数
RabbitMQ消息的发送原理
首先我们必须要连接到RabbitMQ才能发送消息,我们在往RabbitMQ服务器发送消息之前,首先应用程序会通过TCP和RabbitMQ服务器打开一个TCP连接,并对应用程序连接到RabbitMQ发送的用户名和密码进行验证,类似连接数据库,一旦通过认证应用程序就和RabbitMq之间建立了一条AMQP通道Channel。Channel是建立在TCP上的虚拟连接。AMQP命令都是通过信道发送出去的,每个信道都会有一个唯一的ID,不论是发布消息,订阅队列或者介绍消息都是通过信道完成
为什么不通过TCP直接发送命令?
对于操作系统来说创建和销毁TCP会话是非常昂贵的开销,假设高峰期每秒有成千上万条连接,每个连接都要创建一条TCP会话,这就造成了TCP连接的巨大浪费,而且操作系统每秒能创建的TCP也是有限的,因此很快就会遇到系统瓶颈。如果我们每个请求都使用一条TCP连接,既满足了性能的需要,又能确保每个连接的私密性,这就是引入信道概念的原因。
死信队列
死信队列:“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
- a.消息被否认,使用channel.basicNack或者channel.basicReject,并且此时requeue配置为false
- b.消息队列中消息的存活时间超过TTL
- c.消息队列的消息数量超过最大队列长度
死信消息会被RabbitMQ消息特殊处理, 如果配置了死信队列消息,那么该消息就会被丢进死信队列中,如果没有配置,那么消息就会被丢弃
死信交换机
在定义业务队列的时候,要考虑指定一个死信交换机,死信交换机可以和任何一个普通的队列进行绑定,然后在业务队列出现死信的时候就会将数据发送到死信队列。死信队列实际上就是一个普通的队列,只是这个队列跟死信交换机进行了绑定,用来存放死信而已
注意:死信交换机(DLXs)就是普通的交换机,可以是任何一种类型,也可以用普通常用的方式进行声明。对于任何一个队列,死信交换机可以通过在客户端使用队列参数进行声明,或者是在服务器使用policy命令进行声明创建。
死信交换机的设置
方式一:通过命令的policy设置
方式二:也可以通过可选的队列参数配置(Configuration Using Optional Queue Arguments)如下
//下列代码声明了一个新的名为some.exchange.name的交换机,
//并且设置这个新的交换机作为一个新队列的死信交换机。
//注意,并不要求在声明队列时死信交换机必须已经被声明,但是当消息需要死信路由时,该交换机必须存在,否则,消息将会被丢弃。
channel.exchangeDeclare("some.exchange.name", "direct");
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "some.exchange.name");
channel.queueDeclare("myqueue", false, false, false, args);
你也可以指定一个路由关键字在死信路由时使用,如果没有设置,那么就会使用消息自身原来的路由关键字。
args.put("x-dead-letter-routing-key", "some-routing-key");
当一个死信交换机被指定时,除了通常的配置声明队列的权限外,用户还需要队列的读权限和死信交换机的写权限。当声明对队列时,将会校验这些权限。
路由死信消息(Routing Dead-Lettered Messages)
死信消息将被队列的死信交换机路由到其他队列,在路由时有两种情况:
- 使用在声明队列时指定的死信路由关键字
- 没有设置时,使用消息自身原来的路由关键字
例如,如果你使用foo作为路由关键字发送了一条消息到交换机,当消息成为死信后,它使用foo作为路由关键字被发送到队列的死信交换机。如果队列在声明时指定"x-dead-letter-routing-key"的值为bar,那么消息被发送到死信交换机时将会使用bar作为路由关键字。
注意,如果队列没有设置死信路由关键字,那消息被死信路由时将会使用它自身的原始路由关键字。这包含了CC和BCC头参数设置的路由关键字。
当死信消息被重新发送时,消息确认机制也会在内部被开启,因此,在原始队列删除这条消息之前,消息最终到达的队列—死信队列必须确认该消息。换句话说,发送队列在接收到死信队列的确认消息之前不会删除原始消息。注意,如果在特殊情况下服务器宕机,那么同样的消息将会在原始队列和死信队列中同时出现。
消息的死信路由可能会形成一个循环。比如,一个队列的死信的消息没有使用指定的死信路由关键字被发送到默认的交换机时。消息在整个循环(消息到达同一个队列两次)中没有被拒绝,那么消息将被丢弃。
RabbitMQ实现延迟消息
方式一:通过设置消息的TTL,和配置消息队列的死信队列实现
设置
方式二:借用Redis的有序集合Zset实现,实现思路如下
Zset本质就是Set结构上加了个排序的功能,除了添加数据value之外,还提供另一属性score,这一属性在添加元素时候可以指定,每次指定score后,Zset会自动重新按新的值调整顺序。
1.如果score代表的是想要执行时间的时间戳,在某个时间将它插入Zset集合中,它会按照时间戳大小进行排序,也就是对执行时间前后进行排序。
> ZADD delay_queue 1581309229 taskId_1
(integer) 1
> ZADD delay_queue 1581309129 taskId_2
(integer) 1
> ZADD delay_queue 1581309329 taskId_3
(integer) 1
2.不断地进行取第一个key值,如果当前时间戳大于等于该key值的socre就将它取出来进行消费删除,就可以达到延时执行的目的。 注意不需要遍历整个Zset集合,以免造成性能浪费。
> ZRANGE delay_queue 0 -1 withscores
1) "taskId_2"
2) "1581309129"
3) "taskId_1"
4) "1581309229"
5) "taskId_3"
6) "1581309329"
注意事项:
- 遍历逻辑,删除逻辑,注意使用 Redis Lua 封装,确保原子性操作。更要注意 Redis Lua 在 Redis Cluster 的伪集群问题。
- 若是JAVA 语言可以直接使用 redisson,封装了 DelayedQueue 的实现。
3.JDK的DelayQueue
说明:DelayQueue是一个基于堆排序的延迟阻塞队列。插入和删除的时间负责度都是对数阶。
DelayQueue使用了leader/follower模式:
leader线程去轮询头节点的任务状态,其他线程await。如果头节点到了可以执行的时间点,leader会去await线程池唤醒一个线程来接替自己当leader,并同时去执行头节点的任务。
4.Beanstalkd,一个高性能、轻量级的分布式内存队列系统。支持过有9.5 million用户的Facebook Causes应用。后来开源,现在有PostRank大规模部署和使用,每天处理百万级任务。
5.使用quartz,timer,scheduledExecutorService等方式来轮询数据库数据状态。最2b的方式,消耗太多IO资源,效率低下。