服务异步通讯(基于RabbitMQ)

1.MQ的一些常见问题

消息可靠性问题:如何确保发送的消息至少被消费一次

延迟消息问题:如何实现消息的延迟投递

高可用问题:如何解决数百万消息堆积,无法及时消费的问题

消息堆积问题:如何避免单点的MQ故障而导致的不可用问题

2.消息可靠性

1.消息丢失场景

1.发送时丢失

        1.生产者发送的消息未送达exchange

        2.消息到达exchange后未到达queue

2.MQ宕机,queue将消息丢失

3.consumer接收到消息后未消费就宕机

2.生产者消息确认

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种:

1.publisher-confirm:发送者确认

        消息成功投递到交换机,返回ack

        消息未投递到交换机,返回nack

2.publisher-return:发送者回执

        消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。

确认机制发送消息时,需要给每个消息设置一个全局唯一id,以区分不同消息,避免ack冲突

3.SpringAMQP实现生产者确认

1.在生产者服务yml文件中添加配置

spring:
  rabbitmq:
    publisher-confirm-type: correlated  # 开启publisher-confirm
    publisher-returns: true  # 开启publisher-return功能,基于callback机制
    template:
      mandatory: true  # 定义消息路由失败时的策略。 true,则调用ReturnCallback,false,则直接丢弃消息

配置说明:

publish-confirm-type:开启publisher-confirm,支持两种类型

        simple:同步等待confirm结果,直到超时

        correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback

publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback

template.mandatroy:定义消息路由失败时的策略。true,则调用ReturnCallback,false,则直接丢弃消息

2.每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置。

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate对象
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 配置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 记录日志
            log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},路由key:{},消息:{}",
                    replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有需要的话,重发消息
        });
    }
}

3.发送消息,指定消息ID,消息ConfirmCallback

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2SimpleQueue() {
        String routingKey = "simple";
        // 1.准备消息
        String message = "hello, spring amqp!";
        // 2.准备CorrelationData
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        // 3.准备ConfirmCallback
        correlationData.getFuture().addCallback(result -> {
            // 判断结果
            if (result.isAck()) {
                // ACK
                log.debug("消息成功投递大宋交换机!消息ID:{}", correlationData.getId());
            } else {
                // NACK
                log.error("消息投递到交换机失败!消息ID:{}", correlationData.getId());
                // 重发
            }

        }, throwable -> {
            log.error("消息发送失败!", throwable);
            // 重发
        });
        rabbitTemplate.convertAndSend("camq.topic", routingKey, message, correlationData);
    }

4.消息持久化

MQ默认是内存存储消息,开启持久化功能可用确保缓存在MQ中的消息不丢失

1.交换机持久化

    @Bean
    public DirectExchange simpleDirect() {
        // 交换机名称  是否持久化  当没有queue与其绑定时是否自动删除
        return new DirectExchange("simple.direct", true, false);
    }

2.队列持久化 

    @Bean
    public Queue simpleQueue() {
        // 使用QueueBuilder构建队列,durable就是持久化的
        return QueueBuilder.durable("simple.queue").build();
    }

3.消息持久化

SpringAMQP中的消息默认是持久的,可以通过MessageProperties中的DeliveryMode来指定

    @Test
    public void testDurableMessage() {
        // 准备消息
        Message message = MessageBuilder.withBody("hello, spring".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .build();
        // 发送消息
        rabbitTemplate.convertAndSend("simple.queue", message);
    }

5.消费者消息确认

RabbitMQ支持消费者确认机制。消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。SpirngAMQP支持三种确认模式:

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

2.auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack,抛出异常则返回nack

3.none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

配置方式,消费者服务yam文件:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none

6.失败重试机制

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

解决:利用spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true  # 开启消费者失败重试
          initial-interval: 1000  # 重试的失败等待时长为1秒
          multiplier: 1  # 下次失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3  # 最大重试次数
          stateless: true  # true无状态,false有状态,如果业务中包含事务,这里改为false

7.消费者失败消息处理策略

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

1.RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认。

2.ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队。

3.RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。(优秀方案)

    @Bean
    public DirectExchange errorMessageExchange() {
        return new DirectExchange("error.direct");
    }
    
    @Bean
    public Queue errorQueue() {
        return new Queue("error.queue");
    }
    
    @Bean
    public Binding errorMessageBinding() {
        return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
    }
    
    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }

8.如何确保RabbitMQ消息的可靠性?

1.开启生产者确认机制,确保生产者的消息能到达队列

2.开启持久化功能,确保消息未消费前在队列中不会丢失

3.开启消费者确认机制为auto,由spring确认消息处理成功后完成ack

4.开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理。

3.死信交换机

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

1.消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue重试设置为false

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

3.要投递的队列消息堆积满了,最早的消息可能成为死信

如果该队列配置了dead-letter-exchange属性,指定了一个死信交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange, DLX)

1.TTL(Time-To-Live)

如果一个队列中的消息TTL结束仍未消费,则会变为死信,分两种情况:

        1.消息所在的队列设置了存活时间

        2.消息本身设置了存活时间

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dl.queue", durable = "true"),
            exchange = @Exchange(name = "dl.direct"),
            key = "dl"
    ))
    public void listenDLQueue(String msg) {
        log.info("消费者接收到了dl.queue的延迟消息");
    }
@Configuration
public class TTLMessageConfig {

    @Bean
    public DirectExchange ttlDirectExchange() {
        return new DirectExchange("ttl.direct");
    }

    @Bean
    public Queue ttlQueue() {
        return QueueBuilder
                .durable("ttl.queue")
                .ttl(10000)
                .deadLetterExchange("dl.direct")
                .deadLetterRoutingKey("dl")
                .build();
    }

    @Bean
    public Binding ttlBinding() {
        return BindingBuilder.bind(ttlQueue()).to(ttlDirectExchange()).with("ttl");
    }
}
    @Test
    public void testTTLMessage() {
        // 准备消息
        Message message = MessageBuilder.withBody("hello, ttl".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .setExpiration("5000")  // 消息设置超时时间
                .build();
        // 发送消息
        rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
    }

2.延迟队列

利用TTL结合死信交换机,实现了消息发出后,消费者延迟收到消息的效果。这种模式称为延迟队列(Delay Queue)模式

使用场景:1.延迟发送短信 2.用户下单,15分钟内未支付,则自动取消 3.预约工作会议,20分钟后自动通知所有参会人员

延迟队列插件安装

见附件

SpringAMQP使用延迟队列插件

声明交换机时设定delayed属性为true即可

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dl.queue", durable = "true"),
            exchange = @Exchange(name = "dl.direct", delayed = "true"),
            key = "dl"
    ))
    public void listenDLQueue(String msg) {
        log.info("消费者接收到了dl.queue的延迟消息");
    }

3.惰性队列

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。最早接收到的消息,可能就会变成死信,会被丢弃,这就是消息堆积。

解决思路:

        1.增加更多的消费者,提高消费速度

        2.在消费者内开启线程池加快消息处理速度

        3.扩大队列容积,提高堆积上限

从RabbitMQ的3.6.0版本,增加了惰性队列(Lazy Queues)的概念,特征如下:

        1.接收到消息后直接存入磁盘而非内存

        2.消费者要消费消息时才会从磁盘中读取并加载到内存

        3.支持数百万条的消息存储

优点:

        1.基于磁盘存储,消息上限高

        2.没有间歇性的page-out,性能比较稳定

缺点:

        1.基于磁盘存储,消息时效性会降低

        2.性能受限于磁盘的IO

设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可。

命令行方式

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' -- apply-to queues

@Bean方式

@Configuration
public class LazyConfig {
    
    @Bean
    public Queue lazyQueue() {
        return QueueBuilder.durable("lazy.queue")
                .lazy()
                .build();
    }
    
    @Bean
    public Queue normalQueue() {
        return QueueBuilder.durable("normal.queue").build();
    }
}

注解方式

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dl.queue", durable = "true", arguments = @Argument(name = "x-queue-mode", value = "lazy")),
            exchange = @Exchange(name = "dl.direct"),
            key = "dl"
    ))
    public void listenDLQueue(String msg) {
        log.info("消费者接收到了dl.queue的延迟消息");
    }

4.MQ集群

1.集群分类

1.普通集群:是一种分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力。

2.镜像集群:是一种主从集群,普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。

3.仲裁队列:镜像集群虽然支持主从,但主从同步并不是强一种的,某些情况下可能有数据丢失的风险。因此推出了仲裁队列代替镜像集群,底层采用Raft协议确保主从的数据一致性。

2.普通集群(标准集群,classic cluster)

特征:

1.会在集群的各个节点间共享部分数据,包括:交换机,队列元信息。不包含队列中的消息。

2.当访问集群节点是,如果队列不在该节点,会从数据所在节点传递到当前节点并返回。

3.队列所在节点宕机,队列中的消息就会丢失。

3.镜像集群

本质是主从模式,特征:

1.交换机,队列,队列中的消息会在各个mq的镜像节点之间同步备份。

2.创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。 

3.一个队列的主节点可能是另一个队列的镜像节点。

4.使用操作都是主节点完成,然后同步给镜像节点。

5.主节点宕机后,镜像节点会替代成新的主节点。

4.仲裁队列

特征:

1.与镜像队列一样,都是主从模式,支持主从数据同步

2.使用非常简单,没有复杂的配置

3.主从同步基于Raft协议,强一致

SpringAMQP连接集群配置

spring:
  rabbitmq:
    username: hzj
    password: 123
    virtual-host: /
    addresses: ip1:port1, ip2:port2

SpringAMQP创建仲裁队列


@Configuration
public class QuorumConfig {
    
    @Bean
    public Queue quorumQueue() {
        return QueueBuilder.durable("quorum.queue")
                .quorum()
                .build();
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值