RabbitMQ

docker安装

拉取镜像

shell 复制代码
docker pull rabbitmq:3.12-management

运行容器

shell 复制代码
docker run -itd --name rabbitmq -p 5673:5672 -p 15673:15672 rabbitmq:rabbitmq:3.12-management

或者可设置默认用户名和密码

shell 复制代码
docker run -itd --name rabbitmq -e RABBITMQ_DEFAULT_USER mrtuzi -e RABBITMQ_DEFAULT_PASS 123456 -p 5673:5672 -p 15673:15672 rabbitmq:latest

进入容器运行management

shell 复制代码
docker exec -it 容器id /bin/bash
rabbitmq-plugins enable rabbitmq_management

通过ip访问web界面

http://ip:15673,用户名和密码默认是guest

RabbitMQ原理

work模型

默认情况下,它会将消息依次推送给订阅队列的每一个消费者,并不会考虑消费者是否处理完消息,可能会造成消息堆积

yml 复制代码
spring:
  rabbitmq:
    host: 192.168.174.128
    port: 5673
    username: guest
    password: guest
    listener:
      simple:
        # 每次只能获取1条消息,处理完才能获取下一条
        prefetch: 1
  • 多个消费者绑定同一个队列,可以加快消费的速度
  • 同一条消息只会被一个消费者处理
  • 通过prefetch控制消费者预取的数量,消费完再处理下一条消息,这样,能力强的消费者可以消费更多的消息

fanout交换机

真正生产环境都会经过exchange来发送消息,而不是直接发送到消息队列。

交换机类型:

  1. fanout 广播
  2. direct 定向
  3. topic 话题

Fanout:广播

fanout交换机会将接收到的消息广播到每一个消息队列。

Direct:定向

direct交换机会将接收到的消息根据规则发送到消息队列。

  • 每个一队列都与交换机设置一个bindingKey
  • 生产者发布消息时指定一个routingKey
  • 交换机将消息发送到bindingKey和routingKey一致的队列

Topic: 主题

topic交换机和direct类似,但是direct只能是完整的词,而topic交换机的routingKey可以是多个词,每个词以英文点**.**隔开。

bindingKey通配符:

  • #: 0个或者多个词
  • *: 1个词

Spring整合

pom文件引入amqp依赖

xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

yml配置文件

yml 复制代码
spring:
  rabbitmq:
    host: 192.168.174.128
    port: 5673
    username: guest
    password: guest

使用RabbitTemplate发送消息

java 复制代码
    @Resource
    RabbitTemplate rabbitTemplate;

    @Test
    void testDirect() {
        String msg = "hello world order";
        String exchange = "direct.ex";
        rabbitTemplate.convertAndSend(exchange, "order", msg);
        log.info("发送成功");
    }

使用RabbitListener监听队列消息

队列和交换机的绑定可以使用配置类配置,也可以使用注解配置。

配置类配置

java 复制代码
@Configuration
public class DirectConfig {
    @Bean
    public DirectExchange directExchange() {
        return ExchangeBuilder.directExchange("demo.direct").build();
    }

    @Bean(name = "directQueue")
    public Queue directQueue() {
        return QueueBuilder.durable("demo.direct.queue").build();
    }

    @Bean(name = "directBinding")
    public Binding directBinding(@Qualifier("directQueue") Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("red");
    }
}
java 复制代码
    @RabbitListener(queues = {"demo.direct.queue"})
    public void listenerSimpleQueueUser(String msg) {
        log.info("接收到消息2: {}", msg);
    }

注解配置

java 复制代码
    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue(name = "direct.queue2"),
                    exchange = @Exchange(name = "direct.ex", type = ExchangeTypes.DIRECT),
                    key = {"blue", "red"}
            )
    })
    public void listenerDirectQueue(String msg) {
        log.info("接收到消息direct.queue2: {}", msg);
    }

配置消息序列化

java 复制代码
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

消息可靠性

1. 生产者可靠

连接重连

当前重连是阻塞式重连,会阻塞当前业务,可异步执行发送消息。

yml 复制代码
spring:
  rabbitmq:
    host: 192.168.174.128
    port: 5673
    username: guest
    password: guest
    # 超时时间
    connection-timeout: 1s
    template:
      retry:
        # 开启重连
        enabled: true
        # 失败后初始等待时间
        initial-interval: 1000ms
        # 失败后下次等待时间倍数, 下次等待时长=initial-interval * multiplier
        multiplier: 1
        # 最大重试次数
        max-attempts: 3

生产者确认

rabbitmq提供了Publisher Confirm和Publisher Return确认机制。开启后,MQ收到消息后会返回确认消息给生产者,有几种情况:

  1. 消息投递到了MQ,但是路由失败。这时会通过PublisherReturn返回路由异常原因,饭后返回ack,告知投递成功
  2. 临时消息投递到了MQ,并且入队成功,返回ack,告知投递成功
  3. 持久消息投递到了MQ,并且入队完成了持久化,返回ack,告知投递成功
  4. 其它情况都会返回nack,告知投递失败

生产者确认消息需要额外的网络开销,尽量不使用;如果要使用不需要开启return机制,一般路由失败是代码问题;对nack消息可以进行重试,可以记录失败异常消息。

yml 复制代码
spring:
  rabbitmq:
    host: 192.168.174.128
    port: 5673
    username: guest
    password: guest
    # 开始confirm机制, none: 关闭, simple: 同步阻塞等待MQ回执, correlated: 异步回调等待MQ回执
    publisher-confirm-type: correlated
    # 开始return机制,一般不需要开启
    publisher-returns: true
java 复制代码
    @Test
    void testSend() {
        String queueName = "demo.queue2";
        JsonResult jsonResult = new JsonResult();
        jsonResult.setMsg("成功了啊");
        jsonResult.setCode("200");
        CorrelationData correlationData = new CorrelationData();
        correlationData.getFuture().addCallback(new ListenableFutureCallback<>() {
            @Override
            public void onFailure(Throwable ex) {
                // future发生异常时触发,基本不会触发
            }

            @Override
            public void onSuccess(CorrelationData.Confirm result) {
                // 回执处理
                if (result.isAck()) {
                    // 发送消息成功
                    log.info("发送消息成功");
                } else {
                    // 发送消息失败
                    log.error("发送消息失败");
                }
            }
        });
        rabbitTemplate.convertAndSend(queueName, jsonResult, correlationData);
        log.info("发送成功");
    }

2. MQ可靠

数据持久化

如果同时开启持久化和消息确认机制,MQ只有在消息完成持久化才发送ack回执。

  1. 交换机持久化
    将Durability设置为durable
  2. 队列持久化
    将Durability设置为durable
  3. 消息持久化
    将delivery-mode设置为2

lazy queue

从3.6.0版本开始,增加了Lazy Queue,惰性队列。

3.12版本后,所有队列都是Lazy Queue模式,无法更改。

  • 接收到消息后直接存入磁盘而不是内存,内存中只保留最近的消息,默认2048条
  • 消费者要消费时才会从磁盘中读取加入到内存
  • 支持数百万的消息存储

3. 消费者可靠

消费者确认机制

为了确认消费者是否成功处理消息,MQ提供了消费者确认机制,(Consumer Acknowledgement)。当消费者处理完消息后,应该向MQ发送一个回执,告诉MQ自己是否处理完成。

三种回执:

  • ack:成功处理消息,MQ从队列中删除消息
  • nack:失败处理消息,MQ再次投递消息
  • reject:失败并拒绝该消息,MQ从队列中删除消息
    SpringAMQP实现了消费者确认机制,还允许通过配置文件选择ack处理方式,
  • none:不处理。消费者接收到消息后直接发送ack,消息会立刻被删除。不安全,不建议使用
  • manual:手动模式。需要在业务中调用api,发送ack或者reject,存在业务入侵,但是灵活
  • auto:自动模式。SpringAMQP利用AOP对消息处理逻辑做了增强,当业务正常执行时自动返回ack,当业务出现异常时,根据异常判断返回不同结果-如果时业务异常会自动返回nack,如果是消息处理或校验异常会自动返回reject
yml 复制代码
spring:
  rabbitmq:
    host: 192.168.174.128
    port: 5673
    username: guest
    password: guest
    # 开始confirm机制, none: 关闭, simple: 同步阻塞等待MQ回执, correlated: 异步回调等待MQ回执
    publisher-confirm-type: none
    # 开始return机制
    publisher-returns: false
    # 超时时间
    connection-timeout: 1s
    template:
      retry:
        # 开启重连
        enabled: true
        # 失败后初始等待时间
        initial-interval: 1000ms
        # 失败后下次等待时间倍数, 下次等待时长=initial-interval * multiplier
        multiplier: 1
        # 最大重试次数
        max-attempts: 3
    listener:
      simple:
        prefetch: 1
        # 消费者确认机制
        acknowledge-mode: none

消费者失败重试

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

可以使用spring的重试机制,防止无限重试。

yml 复制代码
spring:
  rabbitmq:
    host: 192.168.174.128
    port: 5673
    username: guest
    password: guest
    # 开始confirm机制, none: 关闭, simple: 同步阻塞等待MQ回执, correlated: 异步回调等待MQ回执
    publisher-confirm-type: none
    # 开始return机制
    publisher-returns: false
    # 超时时间
    connection-timeout: 1s
    template:
      retry:
        # 开启重连
        enabled: true
        # 失败后初始等待时间
        initial-interval: 1000ms
        # 失败后下次等待时间倍数, 下次等待时长=initial-interval * multiplier
        multiplier: 1
        # 最大重试次数
        max-attempts: 3
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto
        retry:
          # 开启重试机制
          enabled: true
          # 初始的失败等待时长,1秒
          initial-interval: 1000ms
          # 失败下次等待时间倍数
          multiplier: 1
          # 重试次数
          max-attempts: 2
          # true 无状态;false 有状态。如果业务包含事务,则改为false
          stateless: true

重试失败处理策略

开启重试后,重试多次依旧失败,则可以通过MessageRecoverer接口来处理,包含三种实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息,默认的方式
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定交换机
java 复制代码
/**
 * 只有开启消费失败重试才生效,所以使用ConditionalOnProperty判断开启重试才启动当前配置类
 */
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry", name = "enabled", havingValue = "true")
public class ErrorExchangeConfig {
    @Bean
    public DirectExchange errorExchange() {
        return ExchangeBuilder.directExchange("error.direct").build();
    }

    @Bean(name = "errorQueue")
    public Queue directQueue() {
        return QueueBuilder.durable("error.queue").build();
    }

    @Bean(name = "errorBinding")
    public Binding directBinding(@Qualifier("errorQueue") Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("error");
    }

    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }
}

总结:保证消费者的可靠性,需要开启消费者消息确认机制为auto,让Spring确认消息处理成功后返回ack,异常时返回nack;开启失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到用来专门处理异常的交换机,由人工处理。

业务幂等性

唯一消息id

每一条消息生成一个唯一id,利用id区分消息是否为重复消息。唯一id和消息一起发送给消费者,消费者接收消息处理完业务,将消息id存到数据库,如果后面收到相同id,判断是否存在,如果存在则不处理。

java 复制代码
    @Bean
    public MessageConverter messageConverter() {
        Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        converter.setCreateMessageIds(true);
        return converter;
    }
结合业务

假设如果需要修改订单状态,正常来说,需要先根据订单号查询订单,判断订单是否存在,判断订单状态是否需要修改才需要修改,但是需要高并发的时候,可能会出错。所以可以在sql中进行条件判断。

如果MQ最终都失败了,可以添加一个定时任务定期查询订单,将失败的订单状态都修改完成。

延迟消息

延迟消息可以指定未来的任意时间,在指定时间内让消费者接收到消息。

三种方案:死信交换机,延迟消息插件,取消超时订单。

死信交换机

成为死信的情况:

  • 消费者使用basic.reject或者basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息,达到了队列或者消息本身设置的过期时间,超时了没有被消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信
    如果队列通过deal-letter-exchange属性指定一个交换机,那么该队列中的死信就会投递到该交换机中。这个交换机成为死信交换机(deal letter exchange,DLX)。

注意:死信交换机的Routing key需要和产生死信的交换机的Routing key相同。

延迟消息插件

MQ官方插件,具备延迟消息功能。是一种支持延迟消息功能的交换机,当消息投递到交换机可以暂存一定时间,到时再投递到队列。

java 复制代码
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue", durable = "true"),
            exchange = @Exchange(value = "delay.direct", delayed = "true"),
            key = "haha"
    ))
    public void listenerDelayQueue(JsonResult msg) {
        log.info("接收到消息delay.queue: {}, {}", msg, JSON.toJSONString(msg));
    }
java 复制代码
    @Test
    void testDelaySend() {
        JsonResult jsonResult = new JsonResult();
        jsonResult.setMsg("成功了啊");
        jsonResult.setCode("200");

        String exchange = "delay.direct";
        MessagePostProcessor processor = message -> {
            // 设置延迟时间5000毫秒
            message.getMessageProperties().setDelay(5000);
            return message;
        };
        rabbitTemplate.convertAndSend(exchange, "haha", jsonResult, processor);
        log.info("发送成功");
    }
取消超时订单

订单为例,如果超时时间为30分钟,如果并发过高,30分钟内可能会有大量消息堆积,对MQ压力过大,多数订单在短时间内完成支付,消息却需要堆积30分钟,浪费资源。

可以将长时间,比如30分钟改成多个短时间,比如每5分钟,也就是6份,创建一个延迟消息队列,队列消息类型包含时间和订单号,每次监听到消息,判断订单状态(如果已支付则完成),判断是否是最后一份时间(如果是最后一份,则订单超时,否则重发,并减少一份)。

相关推荐
P.H. Infinity5 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
WX187021128737 小时前
在分布式光伏电站如何进行电能质量的治理?
分布式
不能再留遗憾了10 小时前
RabbitMQ 高级特性——消息分发
分布式·rabbitmq·ruby
茶馆大橘10 小时前
微服务系列六:分布式事务与seata
分布式·docker·微服务·nacos·seata·springcloud
材料苦逼不会梦到计算机白富美13 小时前
golang分布式缓存项目 Day 1
分布式·缓存·golang
想进大厂的小王13 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情13 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
许苑向上14 小时前
【零基础小白】 window环境下安装RabbitMQ
rabbitmq
ZHOU西口15 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
zmd-zk15 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka