RabbitMQ笔记(自用)

113 阅读8分钟

1. Exchange

  • fanout:把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中
  • direct:把消息路由到BindingKeyRoutingKey完全匹配的队列中
  • topic:匹配规则:
    • RoutingKey 为一个 点号.分隔的字符串。 比如: java.xiaoka.show
    • BindingKey和RoutingKey一样也是点号.分隔的字符串
    • BindingKey可使用 * 和 # 用于做模糊匹配,*匹配一个单词,#匹配多个或者0个
  • headers:不依赖路由键匹配规则路由消息。是根据发送消息内容中的headers属性进行匹配,性能差,基本用不到

2. Listenter

@org.springframework.amqp.rabbit.annotation.RabbitListener(
    bindings = @QueueBinding(
        value = @Queue(name = "direct.ms", declare = "true"),
        exchange = @Exchange(name = "sun.direct", type = ExchangeTypes.DIRECT, declare = "true"),
        key = {"green"}
    ))

3. 消息转换器 MessageConverter

  • 默认消息转换器使用(jdk) SimpleMessageConverter
  • 自定义转换器使用(jackson) Jackson2JsonMessageConverter,消费者、生产者需要配置相同消息转换器,防止消息转换出错
@Bean
public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
}

/**
     * 配置并返回一个MessageConverter实例,用于消息的转换
     * 该方法使用了Spring的@Bean注解,表明它负责创建并配置一个Bean
     * (在这里是MessageConverter的实例)供Spring容器管理
     * @return MessageConverter 实例,用于将消息转换为JSON格式
     */
@Bean
public MessageConverter messageConverter() {
    // 创建一个Jackson2JsonMessageConverter实例,用于将消息转换为JSON格式
    Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
    // 设置converter以自动生成消息ID
    converter.setCreateMessageIds(true);
    // 返回配置好的MessageConverter实例
    return converter;
}

4. 发送者可靠性

4.1. 发送者重连机制

spring:
    rabbitmq:
        connection-timeout: 1s    # 设置连接超时时间
        template:
            retry:
                enabled: true    # 开始超时重试机制
                initial-interval: 1000ms    # 失败后初始等待时间    
                max-attempts: 3    # 最大重试次数
                multiplier: 1    # 失败后下次的等待时长倍数,下次等待时长=initial-interval*multiplier
                
# springAQMP 消息重试机制是阻塞式的重试,多次重试时会阻塞当前线程,影响性能。
# 在业务性能有要求时,需要禁用重试机制。当业务不得不使用时,需合理配置等待时长和重试次数,或考虑异步线程执行消息发送。

4.2. 发送者确认机制

springAMQP 提供Publisher Confirm 和Publisher Return 两种机制,开启确认机制后,当发送者发送消息给MQ后,MQ会返回确认结果给发送者,有以下情况:

  • 消息投递MQ,路由失败。此时通过PublisherReturn 返回失败原因,然后返回ACK,告知投递成功(投递成功MQ已经接收到消息投递的请求,但路由分发失败)
  • 临时消息(no durable queue)投递MQ,且入队成功,返回ACK,告知投递成功
  • 持久消息(durable queue)投递MQ,且入队完成持久化(写入磁盘),返回ACK,告知投递成功
  • 其他情况返回NACK,告知投递失败

其中ack和nack属于Publisher Confirm机制,ack是投递成功;nack是投递失败。而return则属于Publisher Return机制。 默认两种机制都是关闭状态,需要通过配置文件来开启。

spring:
   rabbitmq:
       # 开启PublisherConfirm机制,设置confirm类型(SIMPLE,CORRELATED,NONE)
       publisher-confirm-type: correlated
       # 开启PublisherReturn机制
       publisher-returns: true

publisher-confirm-type有三种模式:

  • none:关闭confirm机制
  • simple:同步阻塞等待MQ的回执消息
  • correlated:MQ异步回调方式返回回执消息
示例:
// 创建一个CorrelationData对象,用于确认消息是否发送成功
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());

// 为CorrelationData对象的Future添加回调,处理消息发送成功或失败的情况
correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
    // 发送失败时执行的方法
    @Override
    public void onFailure(Throwable ex) {
        log.error("send fail", ex);
    }

    // 发送成功时执行的方法
    @Override
    public void onSuccess(CorrelationData.Confirm result) {
        if (result.isAck()){
            log.info("send success");
        }else{
            log.error("send fail");
            log.error("cause: {}", result.getReason());
        }
    }
});
// 发送消息到指定的队列中,这里使用convertAndSend方法,参数分别为交换机名称、路由键、消息内容和CorrelationData对象
rabbitTemplate.convertAndSend("sun.direct", "red", "hello rabbitmq red", correlationData);

5. MQ可靠性

5.1. 数据持久化

RabbitMQ 实现持久化三个方面:

  • 交换机持久化(默认)
  • 消息队列持久化(默认) *
  • 消息持久化(单独指定,springAMQP 默认消息持久化):设置Delivery 属性
    • MessageDeliveryMode.NON_PERSISTENT(枚举值为 1):非持久化
    • MessageDeliveryMode.PERSISTENT(枚举值 为 2):持久化

SpringAMQP 非持久化设置:

String exchangeName = "sun.direct";
String msg = "hello rabbitmq";
Message message = MessageBuilder.withBody(msg.getBytes(StandardCharsets.UTF_8))
        .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)
        .build();
rabbitTemplate.convertAndSend(exchangeName, message);
  1. 消息非持久化在内存写满之后会产生阻塞,内存数据写入磁盘,写入速度严重下降
  2. 消息持久化消息每次写入都会写入磁盘,不会产生阻塞请求,写入速度稳定(缺点:并发有所下降)

5.2. Lazy Queue

从RabbitMQ3.6.0版本开始,引入Lazy Queue概念(延迟队列)

延迟队列特征:

  1. 接收到消息后直接写入磁盘,不在写入内存
  2. 消费者需要消费消息时从磁盘中读取并加载到内存中(可以提前缓存部分消息到内存中,最多2048条)

注意:在3.12版本后,所有队列均是Lazy Queue模式,无法更改

注:3.12之前如下设置:

@org.springframework.amqp.rabbit.annotation.RabbitListener(
	bindings = @QueueBinding(
		value = @Queue(name = "queue",  declare = "true", arguments = @Argument(name = "x-queue-mode", value = "lazy")),
		exchange = @Exchange(name = "exchange", type = ExchangeTypes.DIRECT, declare = "true"),
		key = {"email"}
	)
) 

设置队列模式为:lazy (@Argument(name = "x-queue-mode", value = "lazy"))

6. 消费者可靠性

6.1. 消费者确认机制

消费者确认机制(Consumer ACK)是为了确认消费者是否成功处理消息。当消费者处理消息结束后,应该向RabbitMQ发送回执,告知RabbitMQ消息处理状态:

  • ack:消息处理成功,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

SpringAMQP已经实现消息确认机制,通过配置文件选择ACK处理方式,支持三种方式:

  • none:不处理,消息投递给消费者后立即ack,消息会立刻从MQ删除。不安全,不建议使用
  • manual:手动模式,需要业务代码中调用api,发送ack或reject,存在业务侵入,但灵活性高
  • auto:自动模式,SpringAMQP利用AOP对消息处理做环绕增强,当业务正常执行时,自动返回ack

当业务执行异常时,根据异常判断返回不同结果:

  1. 若业务异常,自动返回nack
  2. 若消息处理异常或校验异常,自动返回reject
spring:
	rabbitmq:
		listener:
	        simple:
	            acknowledge-mode: auto

6.2. 失败重试机制

SpringAMQP支持消费者失败重试机制,在消费者出现异常时使用本地重试,而非无限requeue到MQ。通过添加配置启用消费者本地重试机制:

spring:
    rabbitmq:
        listener:
            simple:
                retry:
                    enabled: true   # 开启重试
                    initial-interval: 1000ms    # 初始失败等待时长
                    max-attempts: 3    # 最大重试次数
                    multiplier: 1   # 重试间隔倍数
                    stateless: true # 是否无状态,业务中包含事务,则设置为false

在启用失败重试机制后,重试次数耗尽,若消息处理依然失败,需要在MessageRecoverer接口处理,存在三种不同实现:

  • RejectAndDontRequeueRecoverer:重试次数耗尽,直接reject,丢弃消息。默认方法
  • ImmediateRequeueMessageRecoverer:重试次数耗尽,返回nack,消息重新入队
  • RepublishMessageRecoverer:重试次数耗尽,将失败消息投递指定交换机

修改失败处理策略为RepublishMessageRecoverer:

  1. 定义接收失败消息的交换机、队列及其绑定关系
  2. 定义RepublishMessageRecoverer
@Bean
public MessageRecoverer errorMessageRecoverer(RabbitTemplate rabbitTemplate) {
	return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}

6.3. 业务幂等性

同一个业务执行一次或多次对业务影响是一致的。

方案一:唯一消息id

  1. 每条消息生成一个唯一的id,与消息一起投递给消费者
  2. 消费者接收消息后处理自身业务,业务处理成功后将消息id存入数据库
  3. 若下次收到相同消息,去数据库查询判断是否存在,存在则视为重复消息放弃处理
@Bean
public MessageConverter messageConverter() {
	Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
	converter.setCreateMessageIds(true);
	return converter;
}

方案二:业务判断

具体方案需要基于业务指定策略

7. 延迟消息

延迟消息:发送者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息

延迟任务:设置在一定时间之后才执行的任务

方案一:死信交换机

当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter):

  • 消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信

若队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。该交换机称为死信交换机(Dead Letter Exchange,简称DLX)

实现示例:

@Bean
public Queue normalQueue() {
    return QueueBuilder
        // 持久化
        .durable("normal.queue")
        // 绑定死信交换机
        .deadLetterExchange("dlx.direct").build();
}

//消息发送:
public void publishDlxQueue() {
    String exchangeName = "normal.direct";
    String msg = "hello rabbitmq dlx";
    rabbitTemplate.convertAndSend(exchangeName, "dlx", msg, new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            message.getMessageProperties().setExpiration("10000");
            return message;
        }
    });
}

方案二:延迟消息插件

该插件可以将普通交换机改造为支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后载投递到队列。

示例一(监听器):

@org.springframework.amqp.rabbit.annotation.RabbitListener(
        bindings = @QueueBinding(
                value = @Queue(name = "delay.queue", declare = "true"),
                exchange = @Exchange(name = "delay.direct", type = ExchangeTypes.DIRECT, declare = "true", delayed = "true"),
                key = {"delay"}
        )
)
public void listerDelayMessage(String msg) {
    try {
        log.info("delay message: " + msg + ", " + LocalDateTime.now());
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

示例二(声明DirectExchange交换机):

@Bean
public DirectExchange delayExchange() {
    return ExchangeBuilder
            .directExchange("delay.direct")
            //设置delayed属性,表示延迟交换机
            .delayed()
            //设置持久化
            .durable(true)
            .build();
}

消息发送:
public void delayQueue() {
    String msg = "hello rabbitmq delay";
    // 发送消息,利用消息后置处理器设置消息的过期时间
    rabbitTemplate.convertAndSend("delay.direct", "delay", msg, new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            // 设置消息的过期时间,单位为毫秒
            message.getMessageProperties().setDelay(10000);
            return message;
        }
    });
}