RabbitMQ相关--消息高投递率Demo

155 阅读3分钟

项目地址:gitee.com/qyh24/rabbi…

image.png 图片来源:
作者:不喝奶茶的Programmer
链接:juejin.cn/post/699797… 来源:稀土掘金\

Demo

创建数据表msg_log

create table msg_log
(
    msg_id        varchar(255) default '' not null comment '消息唯一标识',
    msg           text                    null comment '消息体',
    exchange      varchar(255) default '' not null comment '交换机',
    routing_key   varchar(255) default '' not null comment '路由键',
    status        int          default 0  not null comment '状态: 0投递中 1投递成功 2投递失败 3已消费',
    try_count     int          default 0  not null comment '重试次数',
    next_try_time datetime                null comment '下一次重试时间',
    create_time   datetime                null comment '创建时间',
    update_time   datetime                null comment '更新时间',
    constraint unq_msg_id
        unique (msg_id)
)
    comment '消息投递日志' charset = utf8mb4;

alter table msg_log
    add primary key (msg_id);

POJO

@Getter
@Setter
@TableName("msg_log")
public class MsgLog implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 消息唯一标识
     */
     @TableId
    private String msgId;

    /**
     * 消息体, json格式化
     */
    private String msg;

    /**
     * 交换机
     */
    private String exchange;

    /**
     * 路由键
     */
    private String routingKey;

    /**
     * 状态: 0投递中 1投递成功 2投递失败 3已消费
     */
    private Integer status;

    /**
     * 重试次数
     */
    private Integer tryCount;

    /**
     * 下一次重试时间
     */
    private LocalDateTime nextTryTime;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


}

消费者

@Component
@Slf4j
public class Consumer {
    @Autowired
    private IMsgLogService msgLogService;

    @RabbitListener(queues = RabbitmqConfig.DIRECT_QUEUE)
    public void consume(Message message, Channel channel) throws IOException {
        log.info("收到消息: {}", message.toString());
        MessageProperties properties = message.getMessageProperties();
        long tag = properties.getDeliveryTag();
        //业务逻辑是否成功
        boolean success = true;
        if (success) {
            //更新消息状态为"消费成功"
            log.info("收到消息ID: {}", getCorrelationId(message));
            MsgLog entity = MsgLog.builder().status(MsgConstant.CONSUMED_SUCCESS)
                    .msgId(getCorrelationId(message))
                    .build();
            msgLogService.saveOrUpdate(entity);
            // 消费确认
            channel.basicAck(tag, false);
        } else {
            //true 表示重试投递
            channel.basicNack(tag, false, true);
        }
    }

    private String getCorrelationId(Message message) {
        String correlationId = null;

        MessageProperties properties = message.getMessageProperties();
        Map<String, Object> headers = properties.getHeaders();
        for (String key : headers.keySet()) {
            if (key.equals("spring_returned_message_correlation")) {
                correlationId = (String) headers.get(key);
            }
        }
        return correlationId;
    }
}

发送消息

@GetMapping("/test")
@SysLog
@ApiOperation("测试")
public ServerResponse testPost(String msg){
    String msgId = RandomUtil.randomUUID();
    MsgLog msgLog = MsgLog.builder().msgId(msgId)
            .msg(msg)
            .exchange(RabbitmqConfig.DIRECT_EXCHANGE)
            .routingKey(RabbitmqConfig.DIRECT_ROUTING_KEY)
            .build();
    // 第一次消息入库,并发送消息
    msgLogService.save(msgLog);
    CorrelationData correlationData = new CorrelationData(msgId);
    rabbitTemplate.convertAndSend(RabbitmqConfig.DIRECT_EXCHANGE, RabbitmqConfig.DIRECT_ROUTING_KEY, msg, correlationData);// 发送消息
    return ServerResponse.success("成功");
}

定时任务补偿机制

@Configuration
@EnableScheduling
@Slf4j
public class resendTask {

    @Autowired
    private IMsgLogService msgLogService;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //每10S调用一次
    @Scheduled(cron = "0/10 * * * * ?")
    public void retryTask(){
        //从数据库中找到status为 0消息投递中 的消息,重试
        log.info("执行任务的时间------" + LocalDateTime.now());
        List<MsgLog> msgLogs = msgLogService.list(new QueryWrapper<MsgLog>()
                .eq("status",0)
                .lt("next_try_time", LocalDateTime.now()));
        msgLogs.forEach(e->{
            if (e.getTryCount() >= 3){
                log.info("投递失败的id号为:{}",e.getMsgId());
                //如果投递超过3次,则算作投递失败
                msgLogService.update(new UpdateWrapper<MsgLog>()
                        .eq("msg_id",e.getMsgId())
                        .set("status",2)
                        .set("next_try_time", LocalDateTime.now())
                        .set("update_time",LocalDateTime.now()));
            }else {
                //重新发送
                log.info("重新投递的id号为:{}",e.getMsgId());
                rabbitTemplate.convertAndSend(RabbitmqConfig.DIRECT_EXCHANGE,RabbitmqConfig.DIRECT_ROUTING_KEY,e.getMsg(),
                        new CorrelationData(e.getMsgId()));
                //更新数据库,重试+1,并更新重试时间 +1分钟
                msgLogService.update(new UpdateWrapper<MsgLog>()
                        .eq("msg_id",e.getMsgId())
                        .set("update_time",LocalDateTime.now())
                        .set("try_count",e.getTryCount() + 1)
                        .set("next_try_time",LocalDateTime.now().plusMinutes(1)));
            }
        });
    }
}

image.png 消费成功,并且数据库状态更新成“已消费”

image.png

验证消息确认机制

生产者 --> Broker失败

将交换机名称修改为"12",发送请求,因为交换机不存在,所以控制台输出如下,ACK为false。 image.png 当投递失败时,定时任务执行正确的发送流程,这时消息发送成功

image.png

Exchange --> Queue失败

把路由key修改成一个不存在key

image.png 通过定时任务重新发送

image.png 过程中发现一个问题,会先回调ReturnCallback再回调ConfirmCallback
目前想到的解决方法是confirmcallback中加入状态判断

//回调会先执行ReturnCallback,这时判断一下状态,如果是4路由失败的话,就不做更新了
if (msgLogService.getById(correlationData.getId()).getStatus() != 4){
    msgLogService.saveOrUpdate(msgLogEntity);
}