springCloud-rabbitMQPro

1.RabbitMQ 消息可靠性 - 生产者确认机制

其中的每一步都可能导致消息丢失,常见的丢失原因包括:

  • 发送时丢失:

    • 生产者发送的消息未送达exchange

    • 消息到达exchange后未到达queue

  • MQ宕机,queue将消息丢失

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

针对这些问题,RabbitMQ分别给出了解决方案:

  • 生产者确认机制

  • mq持久化

  • 消费者确认机制

  • 失败重试机制

1.1 生产者确认机制

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

返回结果有两种方式:

  • publisher-confirm,发送者确认

    • 消息成功投递到交换机,返回ack (acknowledge)

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

  • publisher-return,发送者回执

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

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

1.1.1 实现-修改配置

首先,修改publisher服务中的application.yml文件,添加下面的内容:

spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true
    template:
      mandatory: true

说明:

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

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

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

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

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

1.1.2 实现-定义return回调

每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置:

修改publisher服务,添加一个:

Aware接口 : ApplicationContextAware Bean工厂的通知 , 当Spring的Bean工厂准备好之后会来通知你,并且把spring容器作为参数传递applicationContext给你.

package cn.itcast.mq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

@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.info("消息发送到队列失败,应答码{},原因{},交换机{},路由键{},消息{}",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有业务需要,可以重发消息
        });
    }
}

1.1.3.定义ConfirmCallback

ConfirmCallback可以在发送消息时指定,因为每个业务处理confirm成功或失败的逻辑不一定相同。

在publisher服务的cn.itcast.mq.spring.SpringAmqpTest类中,定义一个单元测试方法:

    @Test
    public void testSendMessage2SimpleQueue() throws InterruptedException {

        String message = "hello, spring amqp13!";

        CorrelationData correlationData =
                new CorrelationData(UUID.randomUUID().toString());

        correlationData.getFuture().addCallback(r -> {
             if(r.isAck()){
                 log.debug("消息成功投递到交换机!消息ID:{}",correlationData.getId());
             }else{
                 log.error("消息投递到交换机失败!消息ID:{},原因:{}",correlationData.getId(),r.getReason());
             }
        }, e -> {
            log.error("消息发送失败", e);
        });

        rabbitTemplate.convertAndSend("amq.topic", "simple.test", message,correlationData);

        TimeUnit.SECONDS.sleep(3);

        /**
         * 出现这个问题,是在看RabbitMQ实战指南的第一章生产和消费消息实例中出现的。其实这也不能完全算一个问题。主要原因就是我们的channel和connection已经被关闭了,肯定就消费不了消息了。我们是需要晚一点关闭这两个东西就行。
         * 也就是多休眠几秒
         */
    }
}

1.2 消息持久化

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

  • 交换机持久化

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

队列持久化

  • RabbitMQ中队列默认是非持久化的,mq重启后就丢失。

@Bean
public Queue simpleQueue(){
    // 使用QueueBuilder构建队列,durable就是持久化的
    return QueueBuilder.durable("simple.queue").build();
}
  • 消息持久化
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

  

    @Test
    public void testDurableMessage(){
        Message message = MessageBuilder.withBody("hello , Spring".getBytes(StandardCharsets.UTF_8)).
                setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();

        rabbitTemplate.convertAndSend("simple.queue", message );
    }
}

1.3 消费者消息确认

SpringAMQP则允许配置三种确认模式:

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

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

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

由此可知:

  • none模式下,消息投递是不可靠的,可能丢失

  • auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack

  • manual:自己根据业务情况,判断什么时候该ack

一般,我们都是使用默认的auto即可。

1.3.1 none

修改consumer服务的application.yml文件,添加下面内容:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none # 关闭ack auto

1.4 失败重试机制

当消费者出现异常后,消息会不断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

1.4.1 重试次数耗尽

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

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

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

  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机


测试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");
    }
  • 然后,定义RepublishMessageRecoverer

  @Bean                                              //注入
    public  MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
    }
  • consumer listener
@Slf4j
@Component
public class SimpleQueueListener {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "simple.queue"),
            exchange = @Exchange(name = "simple.exchange",durable = "true"),
            key = "simple"
    ))
    public void simpleQueueListener(String msg){
        log.info(msg);
        int a = 1/0;
        log.info("执行一些业务");
    }
}
  • 总结

    • 如何确保RabbitMQ消息的可靠性

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

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

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

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

2.死信交换机(垃圾桶)

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

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

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

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

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

2.1 TTL

TTL,也就是Time-To-Live.如果一个队列中的消息TTL结束仍然未被消费,则会变成死信,ttl超时分为两种情况 :

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

  • 消息本身设置了存活时间

利用TTL机制和死信交换机可以实现延迟消息机制

2.1.1 实现

  • consumer

    • 声明交换机和限时消息队列并且绑定死信交换机

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

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

    @Bean
    public Binding ttlBinding(){
        return BindingBuilder.bind(ttlQueue()).to(ttlDirectExchange()).with("ttl");
    }

consumer

  • 声明死信交换机和其绑定的队列

   @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的延迟消息");
    }

设置存活5秒的消息

  • 这里消息的存活时间为5秒,队列中超过10秒未被消费的消息会被放入死信交换机,以短的优先作为条件进入死信交换机

@Test
    public void testTTLMessage(){
        Message message = MessageBuilder.withBody("hello , ttl message".getBytes(StandardCharsets.UTF_8)).
                setDeliveryMode(MessageDeliveryMode.PERSISTENT).setExpiration("5000").build();

        rabbitTemplate.convertAndSend("ttl.direct","ttl", message );

        log.info("消息已经成功发送");
    }

2.1.2 总结

  • 给队列设置ttl属性,进入队列后超过ttl时间的消息变为死信

  • 给消息设置ttl属性,队列接收到消息超过ttl时间后变为死信

  • 两者共存时,以时间短的ttl为准

2.2 延迟队列

利用TTL结合死信交换机,我们实现了消息发出后,消费者延迟受到消息的效果.这种消息模式成为延迟队列(Delay queue)模式.

延迟队列的使用场景包括:

  • 延迟发送短信.

  • 用户下单,如果用户在15分钟内未支付,则自动取消.

  • 预约工作会议.

  • 安装和使用详见文档

2.2.1 实现

  • consumer 定义延迟交换机和队列

  //延迟交换机
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue",durable = "true"),
            exchange = @Exchange(name = "delay.exchange",delayed = "true",type = "direct"),
            key = "delay"
    ))
    public void listenDelayExchange(String msg){
        log.info("消费者接受到了delay.queue的延迟消息");
    }
  • publisher
 @Test
    public void testDelayMessage(){
        Message delayMessage = MessageBuilder.withBody("hello delay message".getBytes()).
                setHeader("x-delay", 5000).
                setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();

        rabbitTemplate.convertAndSend("delay.exchange", "delay", delayMessage);

        log.info("发送延迟消息成功");
    }
  • 忽略由延迟队列带来的异常
@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) -> {
            //判断是否为延迟消息
            if(message.getMessageProperties().getReceivedDelay() > 0){
                //是一个延迟消息,忽略这个错误提示
                return;
            }
            
            //记录日志
            log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},路由key:{},消息:{}",
                    replyCode,replyText,exchange,routingKey,message.toString());
        });

    }
}

3.消息堆积

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

解决消息堆积有三种思路:

  • 增加更多消费者,提高消费速率

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

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

从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列.

惰性队列的特征如下 :

  • 接收消息后直接存入磁盘而非内存

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

  • 支持数百万条的消息存储

而要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可.可以通过命令将一个运行中的队列修改为惰性队列

  • 修改队列为惰性队列

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

 "^lazy-queue$" : 正则表达式 

声明惰性队列

  • 方式一

  @Bean
    public Queue lazyQueue(){
        return QueueBuilder.durable("lazy.queue").lazy().build();
    }
  • 方式二
@RabbitListener(queuesToDeclare = @Queue(
	name = "lazy.queue",
    durable = "true",
    arguments = @Argument(name = "x-queue-mode",value = "lazy") //创建惰性队列
))
public void listenLazyQueue(String msg){
    log.info("接收到 lazy.queue的消息:{}",msg);
}

4.MQ集群

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

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

镜像集群虽然支持主从,但主从同步并不是强一致性的,某些情况下可能有数据丢失的风险.因此在RabbitMQ的3.8版本以后,退出了新的功能:仲裁队列来代替镜像集群,底层采用Raft协议确保主从的数据一致性.

4.1 普通集群

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

  • 当访问集群某个节点的时候,如果队列不在该节点,会从数据所在节点传递到当前节点并且返回

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

4.1.1 搭建

  • 参考文档

4.2 镜像(队列)

与es的备份模式类似

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

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

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

  • 所有操作都是由主节点完成的,然后同步给镜像节点.

  • 主宕机后,镜像节点会替代称为新的主节点

4.2.1 搭建

  • 参考文档

4.3 仲裁队列

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

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

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

  • 默认会创建4个镜像,如果集群数量小于5个,则会采用all模式.

4.3.1 搭建

  • 参考文档

  • AMQP创建仲裁队列
 @Bean
    public Queue quorumQueue(){
        return QueueBuilder.durable("quorum.queue").quorum().build();
    }
  • 修改配置文件
spring:
  rabbitmq:
#    host: 116.62.71.234 # rabbitMQ的ip地址
#    port: 5672 # 端口
    username: itcast
    password: 123321
    virtual-host: /
    addresses: 192.168.150.101:8071,192.168.150.101:8072,192.168.150.101:8073

5.rabbitMQ补

5.1 mq削峰填谷

对集中的大量请求,经过mq做流量削峰,保护服务系统.

 5.2 什么合适情况下使用MQ

  • 生产者不需要从消费者处获取请求的反馈.引入消息队列之前的直接调用,其接口的返回值应该为空.
  • 允许短暂的不一致性
  • 确实是用了有效果.即解耦,提速,削峰这些方面的收益,超过加入MQ,管理MQ这些成本.
     

5.3 消息追踪-firehose

firehose的机制是将生产者投递给rabbitmq的消息,rabbitmq投递给消费者的消息按照指定的格式
发送到默认的exchange上。这个默认的exchange的名称为amq.rabbitmq.trace,它是一个topic类
型的exchange。发送到这个exchange上的消息的routing key为 publish.exchangename 和
deliver.queuename。其中exchangename和queuename为实际exchange和queue的名称,分别
对应生产者投递到exchange的消息,和消费者从queue上获取的消息。
注意:打开 trace 会影响消息写入功能,适当打开后请关闭。
rabbitmqctl trace_on :开启Firehose命令
rabbitmqctl trace_off :关闭Firehose命令

amq.rabbitmq.trace 会监听发送消息,会发送一个带有详细信息的消息给你所发消息的队列中去.

rabbitmq_tracing和Firehose在实现上如出一辙,只不过rabbitmq_tracing的方式比Firehose多了一
层GUI的包装,更容易使用和管理。
查看所有插件 : rabbitmq-plugins list
启用插件: rabbitmq-plugins enable rabbitmq_tracing

 

6. RabbitMQ-haproxy

HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。

 6.1 安装

自行安装

6.2 配置

#logging options
global
	log 127.0.0.1 local0 info
	maxconn 5120
	chroot /usr/local/haproxy
	uid 99
	gid 99
	daemon
	quiet
	nbproc 20
	pidfile /var/run/haproxy.pid

defaults
	log global
	
	mode tcp

	option tcplog
	option dontlognull
	retries 3
	option redispatch
	maxconn 2000
	contimeout 5s
   
     clitimeout 60s

     srvtimeout 15s	
#front-end IP for consumers and producters

listen rabbitmq_cluster
	#haproxy端口,供publisher访问做负载均衡的入口
	bind 0.0.0.0:5672
	
	mode tcp
	#balance url_param userid
	#balance url_param session_id check_post 64
	#balance hdr(User-Agent)
	#balance hdr(host)
	#balance hdr(Host) use_domain_only
	#balance rdp-cookie
	#balance leastconn
	#balance source //ip
	
	balance roundrobin
	   #绑定负载均衡的rabbitmq实例	
        server node1 127.0.0.1:5673 check inter 5000 rise 2 fall 2
        server node2 127.0.0.1:5674 check inter 5000 rise 2 fall 2

listen stats
	#haproxy控制台
	bind 172.16.98.133:8100
	mode http
	option httplog
	stats enable
	stats uri /rabbitmq-stats
	stats refresh 5s

启动HAproxy负载

/usr/local/haproxy/sbin/haproxy -f /etc/haproxy/haproxy.cfg
//查看haproxy进程状态
ps -ef | grep haproxy

访问如下地址对mq节点进行监控
http://172.16.98.133:8100/rabbitmq-stats

代码中访问mq集群地址,则变为访问haproxy地址:5672

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值