RabbitMQ实战应用

一、确认机制

1 生产投递丢失问题

  • 如果生产者 P 投递消息到交换机 X 的过程中,出现了网络延迟,导致消息丢失,怎么保证消息安全?

RabbitMQ实战应用_第1张图片

1.1通过发布确认机制解决 

  • 生产者 P 投递消息到交换机 X 的过程中,交换机 X 会给生产者 P 一个 ACK 确认回调,生产者可以根据收到 ACK 值知道是否投递成功
RabbitMQ实战应用_第2张图片
@Configuration
@EnableRabbit
public class RabbitMqConfig {

    public static final String CONFIRM_EXCHANGE_NAME = "confirm-exchange";
    public static final String BACK_EXCHANGE_NAME = "back-confirm-exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm-queue";
    public static final String BACK_QUEUE_NAME = "back-confirm-queue";

    @Value("${spring.rabbitmq.host}")
    private String host;
    @Value("${spring.rabbitmq.port}")
    private Integer port;
    @Value("${spring.rabbitmq.username}")
    private String username;
    @Value("${spring.rabbitmq.password}")
    private String password;

    @Bean(name="myRabbitTemplate")
    //@Scope
    public RabbitTemplate rabbitTemplate(){
        RabbitTemplate template = new RabbitTemplate(cachingConnectionFactory());
        return template;
    }

    @Bean
    public CachingConnectionFactory cachingConnectionFactory(){
        //新建缓存连接工厂
        CachingConnectionFactory factory = new CachingConnectionFactory();
        factory.setHost(host);
        factory.setPassword(password);
        factory.setUsername(username);
        factory.setPort(port);
        return factory;
    }

    //新建确认交换机
    @Bean
    public DirectExchange confirmExchange(){
        return ExchangeBuilder.
                directExchange(CONFIRM_EXCHANGE_NAME).
                durable(true).
                build();
    }

    //确认队列
    @Bean
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    //绑定交换机和队列
    @Bean
    public Binding confirmBinding(){
        return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with("ack");
    }
}
@SpringBootTest
class SpringbootRabbitmqSend02ApplicationTests {

    @Autowired
    RabbitTemplate rabbitTemplate;


    @Test
    void contextLoads() {
         rabbitTemplate.convertAndSend("confirm-exchange","ack","你好");
    }

}

2 交换机无法路由问题

  • 当生产者 P 投递消息到交换机 X 的过程中,消息确定收到了,但是路由配置错误,或者没有绑定队列,此时又如何保证消息安全性?

RabbitMQ实战应用_第3张图片

 2.1回退机制让生产者自行处理

  • 仅仅开启确认机制无法保证消息安全性,可以通过回退机制,通知消费者此条消息无法处理,让消费者自行处理消息
  • 建立连接工厂、消息队列和交换机并绑定
@Configuration
@EnableRabbit
public class RabbitMqConfig {

    public static final String CONFIRM_EXCHANGE_NAME = "confirm-exchange";
    public static final String BACK_EXCHANGE_NAME = "back-confirm-exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm-queue";
    public static final String BACK_QUEUE_NAME = "back-confirm-queue";

    @Value("${spring.rabbitmq.host}")
    private String host;
    @Value("${spring.rabbitmq.port}")
    private Integer port;
    @Value("${spring.rabbitmq.username}")
    private String username;
    @Value("${spring.rabbitmq.password}")
    private String password;
    @Bean(name="myRabbitTemplate")
    //@Scope
    public RabbitTemplate rabbitTemplate(AckCallBack ackCallBack, ReturnCallBack returnCallBack){
        RabbitTemplate template = new RabbitTemplate(cachingConnectionFactory());
        template.setConfirmCallback(ackCallBack);
        template.setMandatory(true);//true交换机回退 false 直接丢弃
        template.setReturnsCallback(returnCallBack);
        return template;
    }

    @Bean
    public CachingConnectionFactory cachingConnectionFactory(){
        //新建缓存连接工厂
        CachingConnectionFactory factory = new CachingConnectionFactory();
        factory.setHost(host);
        factory.setPassword(password);
        factory.setUsername(username);
        factory.setPort(port);
        factory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
        return factory;
    }

    //新建确认交换机
    @Bean
    public DirectExchange confirmExchange(){
        return ExchangeBuilder.
                directExchange(CONFIRM_EXCHANGE_NAME).
                durable(true).
                build();
    }

    //确认队列
    @Bean
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    //绑定交换机和队列
    @Bean
    public Binding confirmBinding(){
        return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with("ack");
    }

}
  • 新建AckCallBack
@Component
@Slf4j
public class AckCallBack implements RabbitTemplate.ConfirmCallback {
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ?correlationData.getId():"";
        if(ack){
            log.info("交换机已经收到id:{}消息了",id);
        }else{
            log.info("交换机没有收到id:{}消息,原因是:{}",id,cause);
        }
    }
}
  •  新建ReturnCallBack
@Component
@Slf4j
public class ReturnCallBack implements RabbitTemplate.ReturnsCallback {
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        Message message = returned.getMessage();
        log.info("消息:{}被服务器退回,退回原因:{},退回码:{}",
                message.getBody(),
                returned.getReplyText(),
                returned.getReplyCode());
        /**
         * 处理方式
         *  1:尝试重新调用
         *  2:落库处理,存入 mysql 数据库中
         */
    }
}

 2.2 备份交换机解决

  • 可以通过给交换机设置备份机的方式,来处理交换机无法路由的消息,备份交换机设置 Fanout 类型,可以添加备份队列,还可以添加告警队列,还可以添加入库队列。

RabbitMQ实战应用_第4张图片

@Configuration
@EnableRabbit
public class RabbitMqConfig {

    public static final String CONFIRM_EXCHANGE_NAME = "confirm-exchange";
    public static final String BACK_EXCHANGE_NAME = "back-confirm-exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm-queue";
    public static final String BACK_QUEUE_NAME = "back-confirm-queue";

    @Value("${spring.rabbitmq.host}")
    private String host;
    @Value("${spring.rabbitmq.port}")
    private Integer port;
    @Value("${spring.rabbitmq.username}")
    private String username;
    @Value("${spring.rabbitmq.password}")
    private String password;

    @Bean(name="myRabbitTemplate")
    public RabbitTemplate rabbitTemplate(AckCallBack ackCallBack, ReturnCallBack returnCallBack){
        RabbitTemplate template = new RabbitTemplate(cachingConnectionFactory());
        template.setConfirmCallback(ackCallBack);
        template.setMandatory(true);//true交换机回退 false 直接丢弃
        template.setReturnsCallback(returnCallBack);
        return template;
    }


    @Bean
    public CachingConnectionFactory cachingConnectionFactory(){
        //新建缓存连接工厂
        CachingConnectionFactory factory = new CachingConnectionFactory();
        factory.setHost(host);
        factory.setPassword(password);
        factory.setUsername(username);
        factory.setPort(port);
        factory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
        return factory;
    }

    //建立确认交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        return ExchangeBuilder.
                directExchange(CONFIRM_EXCHANGE_NAME).
                durable(true).
                withArgument("alternate-exchange",BACK_EXCHANGE_NAME).
                build();
    }

    //确认队列
    @Bean
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    //绑定交换机和队列
    @Bean
    public Binding confirmBinding(){
        return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with("ack");
    }

    //声明备份交换机
    @Bean("backExchange")
    public FanoutExchange backExchange(){
        return new FanoutExchange(BACK_EXCHANGE_NAME);

    }

    //声明备份队列
    @Bean("backQueue")
    public Queue backQueue(){
        return QueueBuilder.durable(BACK_QUEUE_NAME).build();
    }

    //绑定备份交换机和队列
    @Bean
    public Binding backBinding(){
        return BindingBuilder.bind(backQueue()).to(backExchange());
    }
}

 3.消费者异常导致数据丢失

RabbitMQ实战应用_第5张图片

3.1消费者确认机制

  • 通过消费者确认机制避免消息丢失:
  • 有三种确认方式:①自动确认:acknowledge=none ②手动确认:acknowledge=manual ③根据异常情况确认:acknowledge=auto
  • 手动确认
    • 正常执行:调用channel.basicAck(deliveryTag,false);方法确认签收消息
    • 异常执行:在catch中调用 basicNack或basicReject,拒绝消息,让MQ重新发送消息
spring:
  rabbitmq:
    host: 192.168.245.129
    port: 5672
    username: wj
    password: 123456
    listener:
      simple:
        retry:
          enabled: true
        acknowledge-mode: manual #手动ack
@Service
@Slf4j
public class MessageReceive {

    @RabbitListener(queues = {"back-confirm-queue"})
    public void receive(String body, Channel channel, Message message) throws IOException {
        try {
            log.info("队列消息"+body);

            int ret = 1/0;

            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            if (message.getMessageProperties().getRedelivered()){
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            }else {
                channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
            }
        }
    }
}

二、死信队列

1.死信队列概述

  • 死信:在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。死信是RabbitMQ的一种消息机制,如果出现以下情况消息将变成死信:
    • 消息在队列的存活时间超过设置的生存时间(TTL)时间
    • 消息队列的消息数量已经超过最大队列长度
    • 消息被否定确认,使用basicNack或basicReject并且requeue=false
  • 死信队列:Dead Letter Queue用来存放死信消息的队列
  • 死信交换机:Dead Letter Exchange用来路由死信消息的交换机

2.死信队列架构图

消息满足刚提到的几个条件之后,消息进入死信队列

RabbitMQ实战应用_第6张图片

3.TTL

  • TTL概述:用来控制消息或者是队列的最大存活时间,单位是毫秒。
  • 设置TTL两种方式:
    • 给消息设置TTL
    • 给队列设置TTL
  • 注意:如果同时配置了队列的 TTL 和消息的 TTL,那么较小的那个值将会被使用。
  • 区别:
    • 消息设置TTL的方式是在消费时做消息过期判断,如果是顶端的,到期直接移除
    • 队列设置过期是直接丢弃或丢到死信队列

4. 死信队列实操

4.1 达到最大长度进入死信队列

  • 限制队列最大长度为10,发送11条消息给正常队列
    //确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
//        return QueueBuilder.durable(TEST_QUEUE_NAME).build();
        Map map = new HashMap<>();
        //设置队列过期时间
//        map.put("x-message-ttl",5000);
        map.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
        //设置队列最大长度
//        map.put("x-max-length",10);


        return QueueBuilder.durable(TEST_QUEUE_NAME).withArguments(map).build();
    }

4.2到达过期时间进入死信队列

  • 设置10秒过期时间
    //确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
//        return QueueBuilder.durable(TEST_QUEUE_NAME).build();
        Map map = new HashMap<>();
        //设置队列过期时间
//        map.put("x-message-ttl",5000);
        map.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
        //设置队列最大长度
//        map.put("x-max-length",10);


        return QueueBuilder.durable(TEST_QUEUE_NAME).withArguments(map).build();
    }

4.3拒收进入死信队列

  • 模拟异常拒收
    //拒收演示
    @RabbitListener(queues = {"test-queue"})
    public void receiveReject(String body, Channel channel, Message message) throws IOException {
        try {

            if ("3".equals(body)){
                log.info("收到异常队列消息"+body);
                int ret = 1/0;
            }
            log.info("队列消息"+body);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            if (message.getMessageProperties().getRedelivered()){
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            }else {
                channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
            }
        }
    }

5.总结

  • 死信交换机和死信队列和普通的没有区别
  • 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
  • 消息成为死信的三种情况:
    • 消息在队列的存活时间超过设置的生存时间(TTL)时间
    • 消息队列的消息数量已经超过最大队列长度
    • 消息被否定确认,使用basicNack或basicReject并且requeue=false

三、延时队列

1.延时队列概述

  • 延时队列:消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费,最重要的特征就是延迟上 

  • 应用场景
    • 12306购票30分钟内不支付自动取消订单,回滚库存
    • 会议设置,15分钟前通知参会人员
  • 解决方案
    • 通过轮询数据库查询处理
    • 通过延迟队列实现

RabbitMQ实战应用_第7张图片 

2.延时队列实现

  • RabbitMQ未提供延时队列功能,但是我们可以通过 TTL + 死信队列的方式设计出延时队列 

RabbitMQ实战应用_第8张图片 

  • RabbitMQ未提供延时队列功能,但是我们可以通过 TTL + 死信队列的方式设计出延时队列
  • 出现问题:发现消费不能按最近过期时间消费问题 

3.安装插件实现延时队列 

  • 下载地址: https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
  • 上传位置: /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.34/plugins
  • 启用插件: rabbitmq-plugins enablerabbitmq_delayed_message_exchange

4.小结

  • 延迟队列就是指消息进入队列后需要一定时间才消费
  • RabbitMQ本身是没有提供延迟队列功能,可以通过死信队列+过期时间来实现延迟队列
  • 自己实现的延迟队列不能按时消费,可能存在滞后问题,安装延时队列 插件使用

四、RabbitMQ实战场景

1.日志与可视化监控查看

  • 日志文件: /var/log/rabbitmq/[email protected]
  • 通过如下操作观察日志:
    • 停止服务:/sbin/service rabbitmq-serverstop
    • 重新启动:rabbitmqctl start_app
    • 开启服务:/bin/systemctl startrabbitmq-server.service

2.消息追踪

  • Firehose:生产者给交换机发送消息时,会按照指定的格式发送到amq.rabbitmq.trace(Topic)交换机上。
    • 开启Firehose命令:rabbitmqctl trace_on
    • 关闭Firehose命令:rabbitmqctl trace_off
  • 注意:开启Firehose 会影响性能,不建议一直打开,一般测试时打开
  • rabbitmq_tracing:启用插件来实现可视化查看
    • 查看插件:rabbitmq-plugins list
    • 启用插件:rabbitmq-plugins enable rabbitmq_tracing
    • 关闭插件:rabbitmq-plugins disable rabbitmq_tracing

3.幂等性保障

  • 幂等性:是分布式中比较重要的一个概念,是指在多作业操作时候避免造成重复影响,其实就是保证同一个消息不被 消费者重复消费两次。但是实际开发中可能存在网络波动等问题,生产者无法接受消费者发送ack信息,因此这条消 息将会被重复发送给其他消费者进行消费,实际上这条消息已经被消费过了,这就是重复消费的问题。
  • 如何去避免重复消费问题:
  • 数据库乐观锁机制
    • update items set count=count-1 where count= #{count} and id = #{id}
    • update items set count=count-1,version=version+1 where version=#{version} and id = #{id}
  • 生成全局唯一id+redis锁机制:操作之前先判断是否抢占了分布式锁 setNx 命名

 五、RabbitMQ集群

1.目前存在的问题

  • 单节点的RabbitMQ如果内存崩溃、机器掉电或者主板故障,会影响整个业务线正常使用
  • 单机吞吐性能会受内存、带宽大小限制

RabbitMQ实战应用_第9张图片

2.解决方案

2.1使用集群模式来解决 

RabbitMQ实战应用_第10张图片 

 

  •  搭建RabbitMQ集群

RabbitMQ实战应用_第11张图片

  • RabbitMQ集群存在问题 

RabbitMQ实战应用_第12张图片 

  •  RabbitMQ集群解决

RabbitMQ实战应用_第13张图片

 

 

 

 

 

 

 

 

你可能感兴趣的:(RabbitMQ,网络)