RabbitMQ消息丢失等异常解决方式&RabbitMQ集群搭建

924 阅读5分钟

先来个拓展命令吧

  • docker 删除rabbitmq队列命令
#进入docker容器
docker exce -it rabbitmq bash
#停止rabbitmq运行
rabbitmqctl stop_app    # 可以双击table键查看命令
#执行删除队列命令
rabbitmqctl reset 
#启动rabbitmq
rabbitctl start_app

rabbitmq在什么情况下可能会丢失消息呢?

  • 图解:

    • 消息可能丢失的情况

    1: 消息在发送的时候没有到达交换机

    2: 消息从交换机路由到队列失败

  • 生产者

  • 解决方案:

    1: 添加confirm确认 ---> 可以解决: 消息在发送的过程中没有到达交换机 2: 添加return退回模式 ---> 可以解决: 消息从交换机路由到队列失败

    • 代码 :
配置文件: 
spring:
  application:
    name: rabbitmq-day02
  rabbitmq:
    host:  192.168.200.130
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirms: true
    publisher-returns: true
server:
  port: 8083
config包: 
@Configuration
public class QueueConfig {

/**
 * @program: day10
 *
 * @description:
 *
 * @author: Mr.Li
 *
 * @create: 2020-07-27 16:24
 **/
// 创建队列
    @Bean
    public Queue createdDirectQueue(){
        return new Queue("directQueue", true, false, false);
    }
//    创建路由
    @Bean
    public DirectExchange createdDirectExchange(){
        return new DirectExchange("directExchange", true, false);
    }

    //    绑定
    @Bean
    Binding createdDirectQueueBingcreatedDirectExchange(Queue createdDirectQueue, DirectExchange createdDirectExchange) {
        return BindingBuilder.bind(createdDirectQueue).to(createdDirectExchange).with("itheima");
    }
}
-------------------------------------------------
@Slf4j
@Component
@RabbitListener(queues = "directQueue")
public class RabbitConfirmCallbackAndReturnCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    /**
     * @program: day10
     * @description:
     * @author: Mr.Li
     * @create: 2020-07-27 17:00
     **/
    @Autowired
    private RabbitTemplate template;  // 这里为什么从ioc容器 获取RabbitTemplate 对象呢    因为他需要使用的RabbitTemplate对象必须和生产者使用的是同一个对象

    @PostConstruct  // 这个注解的意思就是   在  Autowired  这个注解之后执行
    public void init() {
        template.setConfirmCallback(this::confirm);    // 这里的this::confirm  也可以写成 this  会自动匹配
        template.setReturnCallback(this::returnedMessage);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("消息到达交换机: {} ", ack);
        } else {
            log.info("消息未到达交换机 -  原因 : {} ", cause);
        }
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("退回信息 : message {} , replyCode :  {} ,  replyText: {} , exchange :  {} , routingKey :  {}  ",
                message, replyCode, replyText, exchange, routingKey);
    }
}

消费者: 
@Component
@Slf4j
@RabbitListener(queues = "directQueue")
public class QueueConsumer {

    /**
     * @program: day10
     * @description:
     * @author: Mr.Li
     * @create: 2020-07-27 16:47
     **/
    @RabbitHandler
    public void monitorDirectQueue(String argument) {
        log.info("监听到信息: {} ", argument);
    }
}

生产者: 
@RestController
@Slf4j
public class ProducerController {

    /**
     * @program: day10
     * @description:
     * @author: Mr.Li
     * @create: 2020-07-27 16:40
     **/
    @Autowired
    private RabbitTemplate template;

    @GetMapping("/directQueue/{directExchange}/{itheima}/{argument}")
    public void sendDirectExchange(@PathVariable("directExchange") String directExchange,
                                   @PathVariable("itheima") String itheima,
                                   @PathVariable("argument") String argument) {
        template.convertAndSend(directExchange, itheima, argument);
    }
}

  • 小结:

    • 在配置文件中开启confirm确认模式以及return退回模式

    • 实现RabbitTemplate.ConfirmCallBack接口

      • 重写confirm方法: 不论消息是否到达该交换机都会被调用
    • 实现RabbitTemplate.ReturnCallBack

      • 重写returnedMessage方法 只有消息从交换机路由到队列失败才会执行
    • 将confirm和returnedMessage设置到RabbitmqTemplate中,其中使用了@PostConstruct注解表示在 @Autowired后执行

    • 这里 为什么要把confirm和returnedMessage放到RabbitTemplate中

        原因: 因为只有把confirm和returnedMessage放到RabbitTemplate中confirm和returnedMessage的方法才会生效
      
  • 消费者消息确认

    • 消息确认的三种方式:

1: 自动确认(默认z设置): 消息消费成功,消息会从队列中删除,消费失败会重新回到队列再次进行消费 最终会出现 --- > 死循环现象

2: 手动确认: 消息需要消费者自己通过业务编码实现,根据自己的实际情况解决是否从队列删除

3: none: 不做任何操作 消费只会被消费一次,成功就成功,失败就从队列删除

* 自动配置的配置文件
spring:
  application:
    name: rabbitmq-day02
  rabbitmq:
    host:  192.168.200.130
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirms: true
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual   # 开启手动确认模式
server:
  port: 8083

消费者代码:

@Component
@Slf4j
@RabbitListener(queues = "directQueue")
public class QueueConsumer {

    /**
     * @program: day10
     * @description:
     * @author: Mr.Li
     * @create: 2020-07-27 16:47
     **/
    @RabbitHandler
    public void monitorDirectQueue(String argument, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, @Header(AmqpHeaders.REDELIVERED) boolean redelivered) {
        try {
            log.info("消息消费成功");
            if (argument.contains("联想")) {
                throw new RuntimeException("不能购买联想品牌");
            }

            log.info("下单消息; {} ", argument);
//       删除消息
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
//            判断消息是否回滚过
            if (redelivered){
//                删除
                log.info("消息执行删除操作");
                try {
                    channel.basicNack(deliveryTag, false, false);
                } catch (IOException e1) {
                    e1.printStackTrace();
                }

            }else {
//                回滚
                log.info("消息回滚");
                try {
                    channel.basicNack(deliveryTag, false, true);
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }

}

 配置文件中需要手动修改为手动确认
  • 限流

    • 限流只需要修改配置文件
  • 延时队列

    1. 过期队列--TTL

      官方文档: https://www.rabbitmq.com/ttl.html#per-queue-message-ttl

    2. 死信

      注意: 队列一旦创建不能进行修改,如要进行修改请删除重新创建

      创建死信交换机的目的: 没有交换机的时候删除消息是直接删除,绑定了交换机他会将删除的消息保存到交换机对应的队列中

    死信交换机创建必须在普通交换机之前否则会报错

    • 过期队列 + 死信 = 延时队列
  • 消息幂等问题

  • rabbitmq - docker集群的搭建

    • 创建第一台命令
docker run -id --name=rabbitmq1 --hostname rabbitmq1 -e RABBITMQ_ERLANG_COOKIE='rabbitmq' -p 5672:5672 -p 15672:15672 rabbitmq:management
  • 创建第二台命令
docker run -id --name=rabbitmq2 --hostname rabbitmq2 -e RABBITMQ_ERLANG_COOKIE='rabbitmq' -p 5673:5672 -p 15673:15672 --link rabbitmq1:rabbitmq1 rabbitmq:management
  • 创建第三台代码
docker run -id --name=rabbitmq3 --hostname rabbitmq3 -e RABBITMQ_ERLANG_COOKIE='rabbitmq' -p 5674:5672 -p 15674:15672 --link rabbitmq1:rabbitmq1  --link rabbitmq2:rabbitmq2 rabbitmq:management
#3.进入容器内部设置
#myrabbitmq1操作
#进入容器内部
docker exec -it myrabbitmq1 bash
#停止rabbitmq服务
rabbitmqctl stop_app
#清空rabbitmq交换机和队列---》恢复出厂设置
rabbitmqctl reset
#启动rabbitmq服务
rabbitmqctl start_app
#退出
exit

#myrabbitmq2操作
#进入容器内部
docker exec -it myrabbitmq2 bash
#停止rabbitmq服务
rabbitmqctl stop_app
#清空rabbitmq交换机和队列---》恢复出厂设置
rabbitmqctl reset
#连接集群,将rabbitmq2设置为内存节点
rabbitmqctl join_cluster --ram rabbit@rabbitmq1
#启动rabbitmq服务
rabbitmqctl start_app
#退出
exit

#myrabbitmq3操作
#进入容器内部
docker exec -it myrabbitmq3 bash
#停止rabbitmq服务
rabbitmqctl stop_app
#清空rabbitmq交换机和队列---》恢复出厂设置
rabbitmqctl reset
#连接集群,将rabbitmq3设置为磁盘
rabbitmqctl join_cluster  rabbit@rabbitmq1
#启动rabbitmq服务
rabbitmqctl start_app
#退出
exit


#在任意一个容器中可以查看集群状态
rabbitmqctl cluster_status

#为了解决集群中单节点故障后队列依然可以正常使用,这里给所有的队列设置为镜像队列
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
#注意事项
mq集群中至少需要一台磁盘节点

修改配置文件

  • 搭建集群的主要目的:

    • 1: 防止单点故障
    • 2: 实现高可用 , 高并发
    • 3: 实现单台容量的扩充
  • 消息幂等性问题

    • 解决方案: 加锁即可