RabbitMQ的可靠性投递

文章目录

  • 前言
  • 一、什么是消息确认?
  • 二、如何进行消息可靠性投递?
    • 1. Producter->Exchange的可靠性投递
    • 2. Exchange->Queue的可靠性投递
    • 3. Queue->Consumer的可靠性投递
  • 三、可靠性投递的代码实现


前言

RabbitMQ的主要作用就是连接两个系统,进行消息传输的功能。而如何抱枕消息在两个系统之间传输不丢失,这是一个很重要的问题。因此需要我们的RabbitMQ消息确认机制。

一、什么是消息确认?

RabbitMQ的可靠性投递_第1张图片

RabbitMQ的消息确认有两种。
一种是消息发送确认。这种是用来确认生产者将消息发送给交换机,交换机传递给队列的过程中,消息是否投递成功。发送确认分为两步,一步是确认是否到达交换机,二步是确认是否由交换机到达队列
二种是消费接收确认。这种是确认消费者是否成功消费了队列中的消息

二、如何进行消息可靠性投递?

1. Producter->Exchange的可靠性投递

依赖:
开启Confirm确认机制后,生产者投递消息后,如果Broker收到消息,则会给生产者一个答复。生产者进行接收应答,用来确定这条消息是否正常发送到了Broker
可靠性投递:
开启Confirm确认机制,在生产者向交换机投递消息的同时,将消息持久化到本地数据库中,如果投递成功,则将数据库中的消息状态由投递中,该为投递成功状态。如果投递失败,则后续通过定时任务继续尝试向交换机中投递该消息,当同一个消息,被尝试投递多次时(可以定义为大于3次),将该消息改为投递失败状态,后续不再投递

2. Exchange->Queue的可靠性投递

依赖:
开启ReturnCallback失败回调后,当消息路由不到指定的队列时,会执行该失败回调方法,可以在该失败回调方法中,对消息进行重新路由
可靠性投递:
开启ReturnCallback失败回调之后,当消息路由到指定队列失败时会自动触发ReturnCallback失败回调方法,可以在该方法中,将该消息投递给另外一个备用交换机,交由该备用交换机,向指定队列路由

3. Queue->Consumer的可靠性投递

依赖:
开启了RabbitMQ消息确认机制,即指定队列中的应答方式为手动应答模式之后,RabbitMQ会等待消费者显示的发回ACK信号之后,才会从内存(或磁盘)中移除消息。
可靠性投递:
RabbitMQ中队列的默认的应答方式是自动的,即当队列中的消息成功的发送到消费者之后,会立即将消息从内存(或者其他持久化方式)删除。如果消费者端服务在消费消息的时候遇到宕机或者重启,将会造成消息的丢失。当开启了手动应答方式之后,可以带消费者端将消息全部处理完成之后,再给队列一个消费者端接收到消息的应答。
注意:
队列开启手动应答模式之后,RabbitMQ判断消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者是否已经断开。这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很久很久。

三、可靠性投递的代码实现

  1. 创建交换机和路由,并进行交换机和路由的绑定
@Configuration
public class DirectRabbitConfig {
     

    @Bean
    public Queue TestDirectQueue(){
     
        return new Queue("TestDirectQueue",true);
    }

    @Bean
    public DirectExchange TestDirectExchange(){
     
        return new DirectExchange("TestDirectExchange",true,false);
    }

    @Bean
    public Binding TestBinding(){
     
        return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectRouting");
    }

    @Bean
    public FanoutExchange TestFanoutExchange(){
     
        return new FanoutExchange("TestFanoutExchange",true,false);
    }

    @Bean
    public Binding TestFanoutBinding(){
     
        return BindingBuilder.bind(TestDirectQueue()).to(TestFanoutExchange());
    }
}

  1. 创建一张表存储消息的基本信息
    RabbitMQ的可靠性投递_第2张图片
  2. 创建消息实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("msglog")
public class MsgLog {
     

    private String msgId;

    private Integer tryCount;

    private Integer status;

    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
    private LocalDateTime createTime;

    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
    private LocalDateTime tryTime;

    private String content;

    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
    private LocalDateTime updateTime;
}
  1. 创建生产者(生产者发送消息到RabbitMQ的同时,将要发送的消息内容落地到数据库中保存)
@RestController
@RequestMapping("rabbit")
public class DirectRabbitController {
     

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Autowired
    MsgLogService msgLogService;

    @GetMapping("sendMessage")
    public String sendMessage(){
     
        String uuid = String.valueOf(UUID.randomUUID());
        String message = "这是发往消费者的消息";
        MsgLog msgLog = new MsgLog();
        msgLog.setMsgId(uuid);
        msgLog.setContent(message);
        msgLog.setCreateTime(LocalDateTime.now());
        msgLog.setTryTime(LocalDateTime.now());
        msgLog.setStatus(0);
        msgLog.setTryCount(0);
        if (msgLogService.save(msgLog)){
     
            // 消息落地到数据库成功
            rabbitTemplate.convertAndSend("TestDirectExchange","TestDirectRouting",msgLog.getContent(),new CorrelationData(uuid));
        }else {
     
            // 消息落地到数据库失败
            System.out.println("发送消息到RabbitMQ失败");
        }
        return "OK";
    }
}

  1. 实现生产者投递消息到交换机的确认机制,以及消息由交换机投递到队列的错误返回机制
spring:
# rabbitmq服务
  rabbitmq:
    host: 82.157.60.144
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    # 开启发送确认机制
    publisher-confirms: true
    publisher-returns: true
    # 消息由交换机向队列中发送失败时触发退回方法
    template:
      mandatory: true
@Configuration
public class RabbitConfig {
     

    @Autowired
    private MsgLogService msgLogService;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 表示当构造器RabbitConfig构造完成后 再调用此方法
    @PostConstruct
    public void rabbitTemplate() {
     

        // 消息由生产者向RabbitMQ中投递,不论成功与否都触发此方法
        rabbitTemplate.setConfirmCallback(((correlationData, ack, cause) -> {
     
            System.out.println("ACK:" + ack);
            if (ack) {
     
                System.out.println("------消息成功到达了Broker-------");
                // 将消息状态改为投递成功
                msgLogService.update(new UpdateWrapper<MsgLog>().eq("msgId", correlationData.getId()).set("status", 1));
            } else {
     
                // 消息无法到达Broker
                System.out.println("----------消息投递失败,无法到达Broker-------");
            }
        }));

        // 在RabbitMq中消息由交换机向队列中投递失败时触发此方法
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
     
            System.out.println("消息无法路由到指定队列"+ LocalDateTime.now());
            Map<String, Object> headers = message.getMessageProperties().getHeaders();
            String msgId = (String) headers.get("spring_returned_message_correlation");
            // 获取重发次数
            Integer tryCount = msgLogService.list(new QueryWrapper<MsgLog>().eq("msgId", msgId)).get(0).getTryCount();
            // 重发,重发次数+1,修改状态位0
            msgLogService.update(new UpdateWrapper<MsgLog>().eq("msgId",msgId).set("status",0).set("updateTime",LocalDateTime.now()).set("tryTime",LocalDateTime.now().plusMinutes(1)).set("tryCount",tryCount+1));
            System.out.println("消息已准备重新发送:"+LocalDateTime.now());
        });

    }

    @Bean
    public MessageConverter jsonMessageConverter(){
     
        return new Jackson2JsonMessageConverter();
    }
}

  1. 建立消息补偿机制(创建一个定时任务,每隔10s从数据库中查询未被成功消费并且重试次数小于等于3次的消息进行重新发送,对于不符合要求的消息进行投递失败标识)
/**
 * 添加消息队列的定时任务
 */
@Configuration
@EnableScheduling
public class MsgTask implements SchedulingConfigurer {
     

    @Autowired
    private MsgLogService msgLogService;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 每10s执行一次
    @Scheduled(cron = "0/10 * * * * ?")
    public void rabbitTask(){
     
        // 从数据库中筛选符合条件的消息重新发送到交换机中
        List<MsgLog> logList = msgLogService.list(new QueryWrapper<MsgLog>().eq("status", 0).lt("tryTime", LocalDateTime.now()));
        logList.stream().forEach(msgLog->{
     
            // 尝试次数小于三次的继续发送
            if (msgLog.getTryCount() <= 3){
     
                msgLogService.update(new UpdateWrapper<MsgLog>().eq("msgId",msgLog.getMsgId()).set("status",0).set("tryCount",msgLog.getTryCount()+1).set("tryTime",LocalDateTime.now().plusMinutes(1)));
                rabbitTemplate.convertAndSend("TestDirectExchange","不存在的队列",msgLog.getContent(),new CorrelationData(msgLog.getMsgId()));
            }else {
     
                // 尝试次数大于三次的,修改消息发送状态为发送失败
                msgLogService.update(new UpdateWrapper<MsgLog>().eq("msgId",msgLog.getMsgId()).set("status",2).set("tryTime",LocalDateTime.now()));
            }
        });
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
     
        Method[] methods = BatchProperties.Job.class.getMethods();
        int defaultPoolSize = 3;
        int corePoolSize = 0;
        if (methods != null && methods.length > 0) {
     
            for (Method method : methods) {
     
                Scheduled annotation = method.getAnnotation(Scheduled.class);
                if (annotation != null) {
     
                    corePoolSize++;
                }
            }
            if (defaultPoolSize > corePoolSize)
                corePoolSize = defaultPoolSize;
        }
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(corePoolSize));
    }
}
  1. 开启消费者手动应答模式
spring:
  rabbitmq:
    host: 82.157.60.144
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        # 手动应答
        acknowledge-mode: manual
        # 最少消费者数量
        concurrency: 1
        # 最多消费者数量
        max-concurrency: 10
        # 支持重试
        retry:
          enabled: true

@Component
public class DirectReceiver {
     

    /**
     * 消息正常消费,返回已消费应答
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    @RabbitListener(queues = "TestDirectQueue")
    public void receive(Message message, Channel channel) {
     
        // 消息正常消费
        System.out.println(message);

        // 手动应答Broker
        try {
     
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
        } catch (IOException e) {
     
            e.printStackTrace();
        }
    }
}

你可能感兴趣的:(消息中间件,rabbitmq,分布式)