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份,创建一个延迟消息队列,队列消息类型包含时间和订单号,每次监听到消息,判断订单状态(如果已支付则完成),判断是否是最后一份时间(如果是最后一份,则订单超时,否则重发,并减少一份)。

相关推荐
初次攀爬者13 分钟前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者2 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧3 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖3 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农3 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者3 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀3 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式
Ronin3053 天前
信道管理模块和异步线程模块
开发语言·c++·rabbitmq·异步线程·信道管理
Asher05093 天前
Hadoop核心技术与实战指南
大数据·hadoop·分布式
凉凉的知识库3 天前
Go中的零值与空值,你搞懂了么?
分布式·面试·go