Rabbitmq

RabbitMQ

  • 消息可靠性(消息至少被成功消费一次)
  • 实现延迟消息投递 (延迟对列)
  • 高可用(集群)
  • 解决消息堆积问题(持久化)

消息可靠性

生产者消息丢失:–>生产者确认机制

 - 消息未到达交换机 (成功ACK 不成功NACK)
 - 消息到达交换机,未到达队列 (不成功ACK+报错)

MQ服务挂了,队列中的消息丢失 -->队列持久化( durable )
consumer接收消息但是未成功消费 -->消费者确认机制+消费者失败重试机制 (成功ACK 失败NACK)

生产者确认机制:

spring:
     rabbitmq:
    host: wudw.top # rabbitMQ的ip地址
    port: 5672 # 端口
    username: wudw
    password: wdwroot
    virtual-host: /
    publisher-confirm-type: correlated #消息结果处理 correlated异步回调->ConfirmCallBack  simple 同步等待
    publisher-returns: true #定义ReturnCallback
    template:
      mandatory: true  #true 调用ReturnCallback ,false直接丢弃消息

ReturnCallback (全局只能定义一份)

@Configuration
@Slf4j
public class CommonConfig implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                //判断是否是延迟消息 如果是延迟队列中的消息 由队列获取消息在固定时间内不放行 
                //所以如果是延迟队列 会报错
                if (message.getMessageProperties().getReceivedDelay()>0) {
                    //是延迟消息 忽略错误
                    return;
                }
                log.info("消息发送到队列失败,应答码{},原因{},交换机{},路由键{},消息{}",
                        replyCode, replyText, exchange, routingKey, message.toString());
            }
        });

    }
}

ConfirmCallBack(每个方法定义一份)

  CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        correlationData.getFuture().addCallback(result -> {
                    if (result.isAck()) {
                        log.debug("消息发送成功到交换机,ID:{}", correlationData.getId());
                    } else {
                        log.error("消息发送到交换机失败,ID:{},,原因:{}", correlationData.getId(), result.getReason());
                    }
                }, ex -> log.error("消息发送异常,id:{},原因:{}", correlationData.getId(), ex.getMessage())
        );
        Message message = MessageBuilder.withBody(delay.getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .setHeader("x-delay", 6000).build();  //设置为延迟消息 必须未mq配置延迟插件
        rabbitTemplate.convertAndSend("delayExchange", "delay", message, correlationData);
        log.info("发送延迟消息");
        //睡一秒钟是因为消息发送是异步的,可能消息成功发送,但是主方法执行完了还没有收到返回,直接报错
        Thread.sleep(1000);

消息持久化:

  1. 创建持久的Exchange (默认就是持久的)

    //第一种
    ExchangeBuilder.directExchange("durableDirectExchange").durable(true).build();
    //第二种
    new DirectExchange("durableDirectExchange", true, false);
    //第三种 注解方式 @Exchange -->delayed = “true”
     @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = "delayExchange",delayed = "true",durable = "true",type = ExchangeTypes.DIRECT),
            value = @Queue(value = "delayQueue",durable = "true"),
            key = {"delay"}
    ))
    
  2. 创建持久队列(默认就是持久的)

    //1.第一种
    QueueBuilder.durable("durableQueqe1").build();
    //第二种 注解方式 @Queue -->delayed = “true”
     @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = "delayExchange",delayed = "true",durable = "true",type = ExchangeTypes.DIRECT),
            value = @Queue(value = "delayQueue",durable = "true"),
            key = {"delay"}
    ))
    
  3. 消息持久化(如果通过Darshbord 默认不持久化,代码 默认持久化)

    //持久化(PERSISTENT)   Delivery(传送)
    Message message = MessageBuilder.withBody(delay.getBytes(StandardCharsets.UTF_8))
                    .setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
    

消费者确认机制:

RabbitMQ是阅后即焚机制,RabbitMQ确认消息被消费者消费后会立刻删除。
而SpringAMQP则允许配置三种确认模式:

  • manual:手动ack,需要在业务代码结束后,调用api发送ack。

  • auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack。
    (auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack)

spring.abbitmq.listener.simple.acknowledge-mode: auto # 关闭ack
  • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

消费者失败重试机制

出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力。

开启本地重试:(默认最大重试后 直接丢弃消息)

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto #三种机制 NONE 直接丢弃 但是retry也可以用,MANUAL 编程式事务重试,AUTO SpringAOP自动重试
        retry: #默认最大重试后 直接丢弃消息
          enabled: true #开始重试机制
          initial-interval: 1000 #重试间隔1000ms
          multiplier: 2 #下次重试时间是上次重试间隔的几倍
          max-attempts: 5 #最大重试次数
          stateless: true #无状态 默认无状态 如果业务中有事务 一定要设为false false可以保留spring的事务

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:

 /**
     * 消息投递失败达到application.yml中retry指定次数后,存到errorQueue保存
     * RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
     * RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
     * ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
     *
     * @param rabbitTemplate
     * @return
     */
    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
        return new RepublishMessageRecoverer(rabbitTemplate, "errorExchange", "error");
    }

总结
如何确保RabbitMQ消息的可靠性?

开启生产者确认机制,确保生产者的消息能到达队列
开启持久化功能,确保消息未消费前在队列中不会丢失
开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理

死信交换机

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false

  • 消息是一个过期消息,超时无人消费

  • 要投递的队列消息满了,无法投递
    Rabbitmq_第1张图片
    死信交换机和普通交换机类似,死信队列用来接收死信交换机中的消息。另外,队列将死信投递给死信交换机时,必须知道两个信息:

    	1. 死信交换机名称
    	2. 死信交换机与死信队列绑定的RoutingKey
    

可以给队列指定死信交换机,则该队列产生的死信都会发送给指定的死信交换机。

QueueBuilder.durable("ttlQueue")
                .ttl(5000) //指定队列的过期时间
                .deadLetterExchange("deadLetterExchange")
                .deadLetterRoutingKey("dealLetter").build();

延时队列

1.使用TTS+死信队列实现延时队列的效果

一个队列中的消息如果超时未消费,则会变为死信,超时分为两种情况:
超时时间取以下两个中小的那个
1. 消息所在的队列设置了超时时间
2. 消息本身设置了超时时间
//消息设置超时时间
MessageBuilder.withBody(delay.getBytes(StandardCharsets.UTF_8))
                .setExpiration(6000).build();

实现: 代码就不拷贝了 没啥新奇
1.创建队列 指定队列的过期时间
2.队列中的消息过期了 自动丢到死信交换机->死信队列
3.监听死信队列消息

2.延迟队列

虽然叫延迟队列 但是用的是DelayExchange
如果想使用延时队列:

  1. 将延迟队列插件上传进docker容器内部
    docker cp /usr/wudw/rabbitmq/plugins/xxx rabbitmq:/plugins
  2. 进入容器内部运行插件
    docker exec -it rabbitmq bash
    rabbitmq-plugins enable xxx
    
  3. 重启容器
    docker restart rabbitmq
    重启以后,就可以使用了
    使用:
    1. 声明延迟交换机
    使用Bean的方式:
    ExchangeBuilder.delayed().build();
    使用注解方式:
    @Exchange(name=“delay.direct”,delayed=‘true’)
    2. 发送消息:
    发送消息时,一定要携带x-delay属性,指定延迟的时间:
    MessageBuilder.withBody(“hello”.getBytes(StandardCharsets.UTF_8)).setHeader(“x-delay”,10000).build();

3.惰性队列(默认消息放在内存中)

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之前发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题。
解决消息堆积有两种思路:

  • 增加更多消费者,提高消费速度。也就是我们之前说的work queue模式
  • 扩大队列容积,提高堆积上限
    要提升队列容积,把消息保存在内存中显然是不行的。
    从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。惰性队列的特征如下:
  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持数百万条的消息存储
    基于命令行设置lazy-queue:
 rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues

@SpringAMQP声明lazy-queue

//第一种
QueueBuilder.lazy();
//第二种
@RabbitListener(queuesToDeclare=@Queue(
	name="lazy.queue",
	durable = "true",
	arguments =@Argument(name="x-queue-mode",value="lazy")
))
public void listenLazyQueue(String msg){
	xxxxxx
}

总结
消息堆积问题的解决方案?

- 队列上绑定多个消费者,提高消费速度
- 使用惰性队列,可以再mq中保存更多消息

惰性队列的优点有哪些?

- 基于磁盘存储,消息上限高
- 没有间歇性的page-out,性能比较稳定

惰性队列的缺点有哪些?

- 基于磁盘存储,消息时效性会降低
- 性能受限于磁盘的IO

集群

  1. 普通集群 或者叫标准集群(classic cluster),具备下列特征:

    - 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。
    - 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回 	
    - 队列所在节点宕机,队列中的消息就会丢失
    

    Rabbitmq_第2张图片

  2. 镜像集群镜像集群:本质是主从模式,具备下面的特征:

     - 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
     - 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
     - 一个队列的主节点可能是另一个队列的镜像节点
     所有操作都是主节点完成,然后同步给镜像节点
     - 主宕机后,镜像节点会替代成新的主
    

3.仲裁队列
仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:

- 与镜像队列一样,都是主从模式,支持主从数据同步
- 使用非常简单,没有复杂的配置
- 主从同步基于Raft协议,强一致(集群是最终一致性 可能会造成数据丢失)

4. SpringAMQP
与其它配置唯一不同点:

spring:
  rabbitmq:
    addresses: ip1:port1,ip2:port2 #改成集群后 ,需要指定集群的iP:Port

你可能感兴趣的:(rabbitmq)