Rabbitmq

一、Rabbitmq简介

RabbitMQ是使用Erlang语言开发并基于AMQP协议来实现的开源消息队列系统。其主要目的是为了解决传统的消息传输上管理困难,效率不高的问题。

  1. docker拉取RabbitMQ镜像
docker pull rabbitmq
  1. 使用官方定义的端口启动RabbitMQ容器
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:latest
  1. 进入刚启动的容器
docker exec -it 108435a3ea5f /bin/bash
  1. 在容器交互页面执行下面命令下载插件
rabbitmq-plugins enable rabbitmq_management
  1. 打开浏览器,输入服务器ip+端口号回车,进入客户端登录页面,输入默认的账号密码:guest/guest就可以登录到RabbitMQ服务了。
二、消息服务规范
  1. AMQP(Advanced Message Queuing Protocol)高级消息队列协议:是一个消息代理的规范,兼容JMS,是跨语言,跨平台的。
  2. JMS(java message service) java消息服务:基于jvm消息代理的规范,只能用于Java平台。
三、Rabbitmq优点
  1. 解耦:在微服务的架构中,经常会遇到在某个服务中的操作完成之后需要将这个操作的结果通过其他服务提供的接口传递给其他服务,再由其他服务对该操作的结果进行相应的处理,那么这个操作所涉及的服务间的耦合度就很高。而如果将这个操作的结果直接发送到Rabbitmq上,再由其他的服务去Rabbitmq中拿出这个操作的结果,这样就使的完成这个操作的服务与其他想要知道这个操作结果的服务完成了解耦,期间就不再需要双方通过接口去传递结果,直接由Rabbitmq负责传递结果。
  2. 异步:Rabbitmq还可以达到异步效果,极大地提升了消息传输的效率。发送方在发送消息后不需要关心消费方是否能消费完成,还可以继续发送其他消息。
  3. 削峰:在一些高并发的情况下,如果大量的消息一下全都发送给接收方,这样对于接收方的处理压力是很大的,甚至导致接收方服务崩溃。而通过Rabbitmq可以达到一个缓冲的效果,将大量的消息都存放到Rabbitmq中,然后接收方根据自己的处理能力不断的去Rabbitmq中拿取相应的信息处理完,从而达到削减接收方处理的信息量的作用。
四、Rabbitmq缺点
  1. 系统可用性降低:如果Rabbitmq服务挂了,即使其他依靠Rabbitmq相互通信的服务依然正常运行,由于Rabbitmq服务的宕机,会导致服务间的信息无法传递,从而影响其他服务的功能不可用,降低了服务的可用性。
  2. 系统的复杂性提高:本来直接是通过服务间的接口进行信息传递,现在多加入一个Rabbitmq服务,需要考虑信息传递过程中可能出现的各种问题。
  3. 可能导致数据不一致:生产者发生消息成功,接收这个消息的消费者可能会有多个,一旦其中的某个消费者接收失败了,就可能导致某个模块与其他模块的数据不一致问题。
五、Rabbitmq应用场景
  1. 高并发的业务:若在极短时间内大量的请求访问到接口,从而造成系统负载严重。像这类业务使用消息队列,当存入消息队列的消息满了,就拒绝后续的请求,使其让请求不进入后面的业务代码。
  2. 耗时长的业务:若某些操作处理逻辑复杂,从而业务处理生成时间比较缓慢,但是又没有实时性的要求,就可以使用MQ进行异步处理。
  3. 耦合度高的业务:若某些操作所需要牵涉的业务模块比较多,如果仅在一个服务通过接口调用的方式完成整体操作的话,难免代码过于耦合,此场景就可使用MQ进行解耦,将不同业务模块需要的数据放入到队列中,各个业务模块再去MQ中拿相应的数据。
六、Rabbitmq系统架构

在这里插入图片描述

  1. Broker:要使用 RabbitMQ 来收发消息,必须要安装一个 RabbitMQ 的服务,可以安装在 Windows 上面也可以安装在 Linux 上面,默认是 5672 的端口。这台安装 RabbitMQ的服务器的机器我们把它叫做 Broker。
  2. Vhost:在一个Broker上面划分出多个隔离的环境,这多个环境就可以理解成是Vhost。每个Vhost相当月一个相对独立的RabbitMQ服务器,每个Vhost之间是相互隔离的。一个Vhost里面可以有若干个Exchange和Queue,同一个Vhost里面不能有相同名称的Exchange或者Queue,每个Vhost中的exchange、queue、message不能互通。
  3. Connection:无论是生产者还是消费者,都需要和 Broker 建立连接,这个连接就是Connection,这个连接是一个 TCP 的长连接。一个生产者或一个消费者与 Broker 之间只有一个Connection,即只有一条TCP连接。
  4. Channel:消息推送使用的通道,如果每一次访问消息队列中间件都建立一个TCP连接的话,那么系统资源会被大量的占用,效率也会降低,所以AMQP提供了Channel机制,共享同一个TCP连接,而一个TCP连接里可以有大量的Channel,不同的Channel之间是完全隔离的。
  5. Exchange:交换机,用于接收消息,可根据路由键将消息转发到绑定的队列。
  6. Queue:也称作Message Queue,即消息队列,用于保存消息并将他们转发给消费者。
  7. Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来,在进行绑定的时候一般会指定一个binding key。
  8. Routing Key:一个路由规则,生产者将消息发送到交换机时,会在消息头上携带一个 key,这个 key就是routing key,来指定这个消息的路由规则。
  9. Producer:消息生产者,就是投递消息的程序。
  10. Consumer:消息消费者,就是接受消息的程序。
七、消息的处理流程

生产者:1.建立链接–>2.开起信道–>3.发送消息–>4.释放资源

消费者:1.建立链接–>2.开启信道–>3.准备接收消息–>4.broker推送消息–>5.发送确认–>6.释放资源

八、交换机的属性
  1. Name:交换机名称
  2. Type:交换机类型,direct,topic,fanout,headers
  3. Durability:是否需要持久化,如果持久性,则RabbitMQ重启后,交换机还存在
  4. Auto Delete:当最后一个绑定到Exchange上的队列删除后,自动删除该Exchange
  5. Internal:当前Exchange是否用于RabbitMQ内部使用,默认为false。
  6. Arguments:扩展参数,用于扩展AMQP协议定制使用。
九、消息的传输模式
  1. 简单模式:是最简单的消息传输模式,它包含一个生产者、一个消费者和一个队列。生产者向队列里发送消息,消费者从队列中获取消息并消费。
    在这里插入图片描述

  2. 工作模式:是指向多个互相竞争的消费者发送消息的模式,由一个生产者、一个队列和多个消费者组成。多个消费者订阅同一个队列,当有消息进入队列时,这多个消费者会竞争去获取队列的消息消费,对于任务过重或任务较多的情况使用工作队列可以提高任务处理速度。
    在这里插入图片描述

  3. 发布/订阅模式:该模式会将消息发送到交换机上,这种模式下交换机的类型为fanout,再根据该交换机所绑定的队列,将要发送的消息存放到这些队列中,最终将被订阅了这些队列的消费者消费掉,这种方式也就导致了一个消息可能会被多个消费者同时消费。
    在这里插入图片描述
    如上图所示:
    (1)生产者发送消息到交换机后,消息会同时存放到队列1和队列2中。

  4. 路由模式:这种模式也是将消息发送到交换机上,此模式下交换机的类型为direct,且生产者发送消息的时候会携带一个路由键Routing key。只有当这个Routing key与交换机绑定队列的binding key完全匹配时,这个消息就会进入到该binding key所对应的队列中。一个队列可以由不同的binding key来绑定,Routing key与其中任何一个binding key匹配都会进入到与之对应的队列中。 同一个binding key也可以绑定多个不同的队列,这时则跟fanout模式类似,当Routing key与这个binding key匹配时,则消息会进入到与这个binding key对应的多个队列中。
    在这里插入图片描述
    如上图所示:
    (1)当Routing key为key1时,消息会存放到队列1和队列3中;
    (2)当Routing key为key2或者key3时,消息会存放到队列2中。

  5. 主题模式:该模式与路由模式类似也是通过路由键去匹配要发送消息的队列,只不过该方式的Routing key必须具有固定格式:以.间隔的一串单词,比如:com.rabbit.message,且这种格式可以使用通配符*或者#来进行模糊匹配,*可以替代一个单词,#可以替代 0 或多个单词。该模式下的交换机类型为topic,且Routing key 最多不能超过255byte。
    在这里插入图片描述
    如上图所示:
    (1)当消息的Routing key为三个单词,且中间的单词为 rabbit 时,消息会存放到队列1中;
    (2)当Routing key以 com 开头时,消息会存放到队列2中;
    (3)当Routing key为三个单词,且最后一个单词为message的时候,消息会存放到队列3中。

  6. headers模式:该模式下交换机的类型为headers,不依赖于路由键的匹配规则来路由消息,是通过Headers头部来将消息映射到队列的,Headers头部携带一个Hash结构,Hash结构中要求携带一个键"x-match",这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了。当发送消息到交换器时,RabbitMQ会获取到该消息的headers,对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对。如果完全匹配,则路由该消息到此队列中。相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串String类型。headers类型的交换器的性能很差,不建议使用。

十、消息分发机制
  1. 轮询分发:是RabbitMQ 默认的分发机制,该机制主要是将队列中的消息依次分发给订阅了该队列的多个消费者。例如:一个队列中已经存放了很多的消息,这个对列被A、B、C三个消费者订阅了,第一个消息分发给A后,第二个消息只会在B和C之间选择一个消费者分发,假设第二个消息分发给B后,第三个消息就只能分发给C了,当把所有消费者都分发完一轮后,后面的消息再依次类推的分发。这种叫做轮询分发,也叫做公平分发。某种场景下这种策略并不是很好,不同的消费者处理消息的速度有快有慢的,就会导致这处理速度快的这个消费者很大一部分时间处于空闲状态。
  2. 不公平分发:RabbitMQ 会根据消费者处理消息速度的快慢来进行消息的分发,消费快的就多分发点消息,消费慢点就少分发点消息,这样消费快的消费者就不用再等消费慢点消费者处理完后才能获取消息,达到能者多劳的效果。可以通过设置参数 channel.basicQos(1)实现不公平分发策略。通过RabbitMq的Web管理页面,可以看到Channels的Prefetch count属性显示为1则表示不公平分发成功。
  3. 预取值分发:通过channel.basicQos(5)来设置预取值分发的大小,里面的参数5就是预取值,定义通道上允许的未确认消息的最大数量,且这个值要>1才行,=1的话是设置成不公平分发。当消息被消费者接收后,但是没有确认,此时这里就存在一个未确认的消息缓冲区,用于存储非被确认的消息,该缓存区的大小是没有限制的。一旦未确认消息数量达到配置的最大数量,RabbitMQ 将停止向该通道投递更多的消息,除非至少有一个未处理的消息被确认;例如:现在该通道上有A、B、C 三个被消费者消费但是未返回确认信息的消息,且预取值设置的也是3,此时 RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ack;假设A这个消息刚刚被确认 ACK,此时RabbitMQ 将会感知这个情况到并再发送一条消息到这个通道。如果消费者消费了大量的消息但是没有确认的话,就会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范 围内的值通常可提供最佳的吞吐量。
十一、 消息确认机制

生产端:

  1. 消息确认机制:是指生产者投递消息后, 如果Broker收到消息, 则会给我们产生一个应答结果,生产者进行接收应答结果, 用来确定这条消息是否正常发送到Broker, 这种方式也是消息的可靠性投递的核心保障。
    (1)常见的消息确认失败的场景:
            1. broker在返回应答信息的时候,网络中断,生产端无法接受确认消息。
            2. broker返回的应答信息是一些error类的信息,比如磁盘空间已满,无法写入等。

    (2)confirm确认机制的实现:
            1. 在生产者创建的channel上开启确认模式 : channel.confirmSelect()
            2. 在channel上添加监听 : 通过回调channel.addConfirmListener()函数来创建一个ConfirmListener,监听成功和失败的broker应答结果, 根据具体的应答结果对消息进行重新发送, 或记录日志等后续处理。

    (3)如果消息的routingKey是不可达,例如:消息在经过当前配置的 exchangeName 或 routingKey 没有找到指定的交换机,或没有匹配到对应的消息队列时,那么broker会返回成功接收的confirm确认消息,因为broker接收到了消息,只是没有符合的队列。如果只有消息确认机制的话,这种不可达的消息也会认为投递成功了,所以RabbitMQ中还有一个消息返回机制。

  2. 消息返回机制:主要用于处理一些不可路由的消息,如果消息不可达,则返回一个信号通知生产端,相反,如果消息可达,则不会返回任何信号。
    (1)return消息返回机制的实现:通过回调channel.addReturnListener()函数来创建一个ReturnListener,用于监听不可达的消息,然后进行后续的处理。

  3. 消息投递失败的重发机制:如果rabbitmq返回ack失败,生产端也无法确认消息是否真的发送成功,也会造成数据丢失。最好的办法是使用rabbitmq的事务机制,但是rabbitmq的事务机制效率极低,每秒钟处理的消息仅几百条,不适合并发量大的场景。或者生产端每次发送消息的时候,将消息存放到数据库或者缓存中,当生产者接收到broker的成功应答之后就删除数据库存储到消息,如果接收到的broker失败的应答就拿出这条信息重新发送。

消费端:
为了保证消息能可靠到达消费端,RabbitMQ也提供了消费端的消息确认机制。消费者在声明队列时,可以指定autoAck参数,当autoAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则,RabbitMQ会在队列中消息被消费后立即删除它。所以只要令autoAck=false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者显式调用basicAck为止。

  1. 消费端消息的确认分为
    (1)自动确认(默认):AcknowledgeMode.AUTO;
    (2)手动确认:AcknowledgeMode.MANUAL;
    (3)不确认:AcknowledgeMode.NONE;
  2. spring-boot中配置手动确认方法spring.rabbitmq.listener.simple.acknowledge-mode = manual;
  3. 消费成功手动确认方法:消费者成功处理消息后,手动调用void basicAck(long deliveryTag, boolean multiple)方法通知broker该消息已经消费成功。
    deliveryTag:该消息的index
    multiple:是否批量确认。true:将一次性ack所有小于deliveryTag的消息。
  4. 消费失败手动确认方法:消息处理失败后,通过回调void basicNack(long deliveryTag, boolean multiple, boolean requeue)方法或者void basicReject(long deliveryTag, boolean requeue)方法通知broker该消息消费失败。两种方法区别在于basicNack可以批量拒绝多条消息,而basicReject一次只能拒绝一条消息。
    deliveryTag:该消息的index。
    multiple:是否批量. true:将一次性拒绝所有小于deliveryTag的消息。
    requeue:被拒绝的是否重新入队列。
  5. 消费者手动确认可能出现的问题
    (1)消息无法ack:消费端在消费消息过程中出现异常,不能回复ack应答,消息将变成unacked状态,并且一直处于队列中。如果积压的过多将会导致程序无法继续消费数据。消费端服务重启,断开rabbitmq的连接后,unacked的消息状态会重新变为ready等待消费。但是如果不重启消费端服务,消息将一直驻留在MQ中。所以,可以捕获异常,然后调用Nack确认,然后消息进入队列重新消费。
    (2)无效消息循环重入队列:如果消费端捕获异常,并进行basicNack应答,并将消息重新放入队列中,可能会出现无效的消息循环入队列的问题。假设消息或者代码本身有bug,每次处理这个消息都会报异常,那消息将一直处于消费——>报异常——>重入队列——>继续消费——>报异常。。。的死循环过程。防止死循环有两种处理办法:一是:设置异常的类型,根据不同的异常类型选择那种异常是需要重新入队列;二是:将异常的消息放入专门存放异常信息的对列中,后期人工再去取处理这些异常信息。
十二、 消息幂等性

消息幂等性,其实就是保证同一个消息不被消费者重复消费两次。当消费者消费完消息之后,在返回ack应答确认的时候失败了,会导致生产者接收不到确认消息,往往会将这个消息重新发送给消费端,但是消费端在之前一次就已经完成消费了,如果不能保证幂等性的话,那么消费者就会出现重复消费同一个消息。实现消息幂等性的方案:
      生产者每次发送消息的时候会生成一个全局唯一的id放到信息中,每次消费消息之前根据这个全局id去查询db或者redis中是否存在该id的消息信息,如果有,则说明该消息已经消费过,直接返回不再做后续处理;如果没有,则说明该消息未被消费过,继续进行后续业务处理,处理成功之后再将该全局id插入到bd或者redis中。

十三、 RabbitMQ集群模式
  1. 普通集群模式:
    普通集群模式用于提高系统的吞吐量,通过添加节点来线性扩展消息队列的吞吐量。也就是在多台机器上启动多个 RabbitMQ 实例,而队列 queue 的消息只会存放在其中一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。消费的时候,如果连接到了另外的实例,那么该实例就会从数据实际所在的实例上的queue拉取消息过来,就是说让集群中多个节点来服务某个 queue 的读写操作。
    在这里插入图片描述
    缺点:
    (1)queue所在的节点宕机了,其他实例就无法从那个实例拉取数据。
    (2)RabbitMQ 内部也会产生大量的数据传输。
  2. 镜像集群模式
    镜像队列集群是RabbitMQ 真正的高可用模式,集群中一般会包含一个主节点master和若干个从节点slave,如果master由于某种原因失效,其下的slave会被提升为新的master。所有的消息只会向master发送,再由master将命令的执行结果广播给slave,所以master与slave节点的状态是相同的。也就是说每个 RabbitMQ 节点都有这个 queue 的完整数据,任何一个机器宕机了,其它机器节点依然包含了这个 queue 的完整数据,其他消费者都可以到其它节点上去消费数据。
    在这里插入图片描述

缺点:
(1) 性能开销大,消息需要同步到所有机器上,导致网络带宽压力和消耗很大。
(2)非分布式,没有扩展性,如果 queue 的数据量大到这个机器上的容量无法容纳了,这时候集群也无法在接收消息了。

十四、 RabbitMQ持久化机制

RabbitMQ的持久化队列分为:队列持久化、消息持久化、交换机持久化。

  1. 队列持久化:队列持久化后重启rabbit-server服务后该队列依然存在,而非持久化的队列则会丢失。
    (1)通过定义队列时的durable参数来实现的,Durable为true时,队列才会持久化。
// 参数1:名字  
// 参数2:是否持久化,
// 参数3:独du占的queue, 
// 参数4:不使用时是否自动删除,
// 参数5:其他参数
channel.queueDeclare(queueName,true,false,false,null);

(2)持久化的队列在web控制台中有一个D的标记。
在这里插入图片描述
2. 消息持久化:RabbitMQ默认把消息放在内存中是为了加快传输和消费的速度,消息持久化后将会存入磁盘中。当消息持久化后,重启rabbit-server服务,消息依然存在,而非持久化的消息则会丢失。消息持久化必须保证所在的队列也是持久化的,且将消息标记为持久化并不能完全保证不会丢失消息。因为当消息刚准备存储在磁盘的时候,但是还没有存储完,消息还在缓存的一个间隔点,此时宕机会导致消息丢失。

// 参数1:交换机的名字
// 参数2:队列名
// 参数3:是否进行消息持久化,设置为MessageProperties.PERSISTENT_TEXT_PLAIN代表消息持久化
// 参数4:发送消息的内容
channel.basicPublish(exchangeName, queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

  1. 交换机持久化:和队列一样,交换机也需要在定义的时候设置持久化的标识,否则在rabbit-server服务重启以后将丢失。
// 参数1:交换机的名字
// 参数2:交换机的类型,topic/direct/fanout/headers
// 参数3:是否持久化
channel.exchangeDeclare(exchangeName,direct,true);
十五、 Rabbitmq事务机制

Rabbitmq事务与db中的事务类似,db中的事务是为了保证一系列的数据库操作要么全部执行成功,要么都不执行,而 Rabbitmq事务则是为了保证生产者发送的消息要么都发送到Rabbitmq服务上,要么都不发送。Rabbitmq事务相关的三个方法txSelect(),txCommit()以及txRollback()
(1)txSelect():用于将当前channel设置成transaction模式,通过调用tx.select方法开启事务模式。
(2)txCommit():用于提交事务。
(3)txRollback():发生异常时,回滚事务。

在生产者发送消息的时候开启事务相关代码:

try {
    channel.txSelect();//开始事务
    channel.basicPublish(EXCHANGE_NAME,QUEUE_NAME,null,message.getBytes());
    channel.txCommit();//提交事务
}catch (Exception e){
    channel.txRollback();//回滚事务
}

如果txCommit提交成功了,则消息一定到达了broker了,如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。

十六、 Rabbitmq死信队列

死信队列:主要用于存放那些无法被消费的消息队列。当queue消息队列中的消息由于一些原因一直没办法被消费者消费掉的话,这个消息就会从这个正常的消息队列转移到死信消息队列中。

  1. 消息变成死信的3中原因:
    (1)消息过了过期时间TTL(time to live),消息队列中存放的消息一般都是有一定时间的,超过了这个时间,这条消息就会被放到死信队列中去;
    (2)消息队列满了,后续再投递到该队列中的消费会转存到死信队列去;
    (3)一直未收到消费者返回的ack确认信息的消息,那么RabbitMQ消息队列就不清楚这条消息到底有没有被消费成功,就会将这条消息存放到死信队列中。
  2. 死信队列的用法
    死信队列其实跟普通队列没什么区别,主要不同点在于死信队列是用来存放无法被消费的消息,其他与交换机的绑定都是一样的。死信队列的用法最主要的点是在于如何将普通队列的消息转存到死信队列中,所以只要在设置普通队列的时候绑定死信交换机和对应的死信RoutingKey后,后续该普通队列中无法被消费的消息就会发送到该死信交换机上,死信交换机再根据死信RoutingKey找到绑定的死信队列,最后将信息保存到死信队列中。代码如下:
//声明普通、死信交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.Direct);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.Direct);

//声明普通队列,通过参数设置死信交换机,死信RoutingKey
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("x-dead-letter-exchange", DEAD_EXCHANGE);
paramMap.put("x-dead-letter-routing-key", "dead_routingkey");
channel.qunueDeclare(NORMAL_QUEUE, false,false,false,paramMap);

//声明死信队列
channel.qunueDeclare(DEAD_QUEUE, false,false,false,null);

//绑定普通交换机与普通队列
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE,"normal_routingkey");

//绑定死信交换机与死信队列
channel.qunueBind(DEAD_QUEUE, DEAD_EXCHANGE,"dead_routingkey");
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值