Sping Boot 整个RabbitMQ,保证消息100%消费和结合死信队列

463 阅读7分钟

目录

  • Spring Boot整合RabbitMQ
  • RabbitMQ保证消息100%消费
  • 死信队列
  • 最后

Spring Boot整合RabbitMQ

目录结构

配置

spring:
  rabbitmq:
    host: 192.168.81.132
    port: 5671
    username: admin
    password: admin
    ##开启生产端->>>Exchange的Confirm
    publisher-confirms: true
    ##开启Exchange->>Queue的Confirm
    publisher-returns: true
    listener:
      simple:
        ##设置手动ACK Queue ->消息端
        acknowledge-mode: manual
        ## unack message的数量。在Channel里面的数量不能超过100
        prefetch: 100

初始化

@Configuration
public class CreateOrderRabbitConfig {



    /**
     * 发货交换器
     * @return
     */
    @Bean
    public DirectExchange sendOrderExchange(){
        return new DirectExchange(RabbitKeyEnums.ORDER_SEND_EXCHANGE.getKey());
    }

    /**
     * 订单超时交换器
     * @return
     */
    @Bean
    public DirectExchange orderOverTimeExchange(){
        return (DirectExchange) ExchangeBuilder.directExchange(RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey()).durable(true).build();
    }


    /**
     * 发货队列
     * @return
     */
    @Bean
    public Queue sendOrderQueue(){
        return new Queue(RabbitKeyEnums.ORDER_SEND_QUEUE.getKey());
    }

    /**
     * 归还库存死信队列
     * @return
     */
    @Bean
    public Queue returnInventoryQueue(){
        return new Queue(RabbitKeyEnums.ORDER_RETURN_INVENTORY_QUEUE.getKey());
    }


    /**
     * 订单超时队列
     * @return
     */
    @Bean
    public Queue orderOverTimeQueue(){
        Map<String,Object> args = new HashMap<>(2);
        //x-dead-letter-exchange   声明死信队列Exchange
        args.put("x-dead-letter-exchange",
                RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey());
        //x-dead-letter-routing-key 声明超时订单未支付队列中过期的消息将转发到归还库存队列中
        args.put("x-dead-letter-routing-key",
                RabbitKeyEnums.ORDER_RETURN_INVENTORY_ROUTING_KEY.getKey());
        return QueueBuilder.durable(RabbitKeyEnums.ORDER_PAY_OVERTIME_QUEUE.getKey()).
                withArguments(args).
                build();

    }

    /**
     * 发货队列交换器绑定
     * @return
     */
    @Bean
    public Binding createOrderBinding(){
        return BindingBuilder.
                bind(sendOrderQueue()).
                to(sendOrderExchange()).
                with(RabbitKeyEnums.ORDER_SEND_ROUTING_KEY.getKey());
    }





    /**
     *  订单超时-绑定
     * @return
     */
    @Bean
    public Binding orderOverTimeBinding(){
        return BindingBuilder.
                bind(orderOverTimeQueue()).
                to(orderOverTimeExchange()).
                with(RabbitKeyEnums.ORDER_PAY_OVERTIME_ROUTING_KEY.getKey());
    }



    /**
     *  归还库存-绑定
     * @return
     */
    @Bean
    public Binding returnInventoryBinding(){
        return BindingBuilder.
                bind(returnInventoryQueue()).
                to(orderOverTimeExchange()).
                with(RabbitKeyEnums.ORDER_RETURN_INVENTORY_ROUTING_KEY.getKey());
    }


}

配置RabbitTemplate

@Slf4j
@Configuration
public class RabbitConfig {

    @Autowired
    private CachingConnectionFactory connectionFactory;

    @Autowired
    private MsgLogMapper msgLogMapper;

    @Bean
    public RabbitTemplate rabbitTemplate(){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(converter());

        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){

            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack){
                    String msgId = correlationData.getId();
                    MsgLog msgLog = new MsgLog();
                    msgLog.setMsgId(msgId);
                    msgLog.setStatus(MsgLogEnum.DELIVERY_SUC);
                    msgLogMapper.updateById(msgLog);
                    log.info("消息发送成功到Exchange");
                }else{
                    //需要进行重发消息
                    log.info("消息发送到Exchange失败,{},cause:{}",correlationData,cause);
                }
            }
        });
        // 触发setReturnCallback回调必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setUseDirectReplyToContainer(true);
        // 消息是否从Exchange路由到Queue, 注意: 这是一个失败回调, 只有消息从Exchange路由到Queue失败才会回调这个方法
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            log.info("消息从Exchange路由到Queue失败: exchange: {}, route: {}, replyCode: {}, replyText: {}, message: {}", exchange, routingKey, replyCode, replyText, message);
        });
        return rabbitTemplate;
    }

    @Bean
    public Jackson2JsonMessageConverter converter() {
        return new Jackson2JsonMessageConverter();
    }
}

RabbitMQ 交換器、队列,绑定Key

@Getter
public enum RabbitKeyEnums {



    /**
     * 订单发货交换器
     */
    ORDER_SEND_EXCHANGE("order.send.exchange"),

    /**
     * 订单超时exchange
     */
    ORDER_PAY_OVERTIME_EXCHANGE("order.pay.overtime.exchange"),

    /**
     * 订单发货队列
     */
    ORDER_SEND_QUEUE("order.send.queue"),

    /**
     * 订单超时queue
     */
    ORDER_PAY_OVERTIME_QUEUE("order.pay.overtime.queue"),

    /**
     * 归还库存_死信队列
     */
    ORDER_RETURN_INVENTORY_QUEUE("order.return.inventory.queue"),

    /**
     * 订单发货队列交换器绑定Key
     */
    ORDER_SEND_ROUTING_KEY("order.send.routing"),

    /**
     * 订单超时routing_key
     */
    ORDER_PAY_OVERTIME_ROUTING_KEY("order.pay.overtime.routing"),


    /**
     * 归还库存routing_key
     */
    ORDER_RETURN_INVENTORY_ROUTING_KEY("order.return.inventory.routing"),



    ;

    private String key;

    RabbitKeyEnums(String key) {
        this.key = key;
    }

    public String getKey() {
        return key;
    }
}

简单模拟支付成功发货场景使用

  • 具体流程:收到支付回调之后,发送一条通知到仓库发货的消息,仓库服务收到监听发货队列并消费消息发货
  • 发送端代码:
@GetMapping("/payCallBack")
    public String payCallBack(String orderNo){
        CorrelationData correlationData = new CorrelationData(orderNo);
        if(isPay==1){
            log.info("{}订单支付成功,进行发货处理",orderNo);
            msgLogFun.saveMsgLog(orderNo,
                    orderNo,
                    RabbitKeyEnums.ORDER_SEND_EXCHANGE.getKey(),
                    RabbitKeyEnums.ORDER_SEND_ROUTING_KEY.getKey());
            log.info("{}订单发货消息落库成功",orderNo);
            //已支付
            rabbitTemplate.convertAndSend(RabbitKeyEnums.ORDER_SEND_EXCHANGE.getKey(),
                    RabbitKeyEnums.ORDER_SEND_ROUTING_KEY.getKey(),
                    MessageHelper.objToMsg(orderNo),
                    correlationData);
        }else{
            log.info("{}订单异常,支付回调失败",orderNo);
        }
        return "成功";
    }
  • 消费端:
@RabbitListener(queues = "order.send.queue")
    public void consume(Message message, Channel channel) {
        String orderNo = MessageHelper.msgToObj(message, String.class);
        try {
            log.info("收到发货的消息:orderId:{}", orderNo);
            MsgLog msgLog = msgLogMapper.selectById(orderNo);
            if (msgLog == null || MsgLogEnum.SPENT.equals(msgLog.getStatus())) {
                log.info("重复发货,orderId:{}", orderNo);
                return;
            }
            MessageProperties messageProperties = message.getMessageProperties();
            long deliveryTag = messageProperties.getDeliveryTag();
            MsgLog newMsg = new MsgLog();
            newMsg.setMsgId(msgLog.getMsgId());
            newMsg.setStatus(MsgLogEnum.SPENT);
            newMsg.setUpdateTime(LocalDateTime.now());
            UpdateWrapper<MsgLog> updateWrapper = new UpdateWrapper<>();
            updateWrapper.eq("status",MsgLogEnum.DELIVERY_SUC);
            int count = msgLogMapper.update(newMsg,updateWrapper);
            if (count > 0) {
                log.info("发货成功,msgId:{}", orderNo);
                channel.basicAck(deliveryTag, false);
            } else {
                log.info("发货,重新发送,msgId:{}", orderNo);
                channel.basicNack(deliveryTag, false, true);
            }
        }catch (Exception e){
            log.info("程序异常,发货", orderNo);
            e.printStackTrace();
        }
    }

保证百分百消费

  • 在一些比较重要的消息,需要保证消息百分百消费,上面这个例子也提现了这一场景
  • 流程:发送消息前,我们提前对消息进行落库,收到Exchange消息到达之后,并且成功路由到队列,将消息状态改为投递成功,消费端接受到消息之后并成功消费后将消息状态改为消费成功。
  • 最后:设置定时任务从数据库中查询出消费次数大于三次,且是失败的消息,进行重新投递。

数据库结构

@TableName(value="msg_log")
@Data
public class MsgLog{


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

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

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

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

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

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

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

    /** 创建时间 */
    private LocalDateTime createTime ;

    /** 更新时间 */
    private LocalDateTime updateTime ;

}

消息状态

@Getter
public enum MsgLogEnum implements IEnum<Integer>{

    /** 状态: 0投递中 1投递成功 2投递失败 3已消费 */
    /**
     * 投递中
     */
    DELIVERING(0,"投递中"),

    /**
     * 投递成功
     */
    DELIVERY_SUC(1,"投递成功"),

    /**
     * 投递失败
     */
    DELIVERY_FAILED(2,"投递失败"),

    /**
     * 已消费
     */
    SPENT(3,"已消费"),

    ;

    MsgLogEnum(Integer state, String desc) {
        this.state = state;
        this.desc = desc;
    }

    private int state;

    private String desc;

    @Override
    public String toString() {
        return this.desc;
    }

    @Override
    public Integer getValue() {
        return this.state;
    }}

发送端配置和代码

rabbitmq:
    ##开启生产端->>>Exchange的Confirm
    publisher-confirms: true
    ##开启Exchange->>Queue的Confirm
    publisher-returns: true
    listener:
      simple:
        ##设置手动ACK Queue ->消息端
        acknowledge-mode: manual

说明:开启pubisher确认机制,开启Exchange->>Queue的Confirm,设置手动ACK Queue进行手动确认

@Bean
    public RabbitTemplate rabbitTemplate(){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(converter());

        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){

            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack){
                    String msgId = correlationData.getId();
                    MsgLog msgLog = new MsgLog();
                    msgLog.setMsgId(msgId);
                    msgLog.setStatus(MsgLogEnum.DELIVERY_SUC);
                    msgLogMapper.updateById(msgLog);
                    log.info("消息发送成功到Exchange");
                }else{
                    //需要进行重发消息
                    log.info("消息发送到Exchange失败,{},cause:{}",correlationData,cause);
                }
            }
        });
        // 触发setReturnCallback回调必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setUseDirectReplyToContainer(true);
        // 消息是否从Exchange路由到Queue, 注意: 这是一个失败回调, 只有消息从Exchange路由到Queue失败才会回调这个方法
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            String msgId = MessageHelper.msgToObj(message, String.class);
            MsgLog msgLog = new MsgLog();
            msgLog.setMsgId(msgId);
            msgLog.setStatus(MsgLogEnum.DELIVERY_FAILED);
            msgLogMapper.updateById(msgLog);
            log.info("消息从Exchange路由到Queue失败: exchange: {}, route: {}, replyCode: {}, replyText: {}, message: {}", exchange, routingKey, replyCode, replyText, message);
        });
        return rabbitTemplate;
    }

说明:必须设置ConfirmCallback,和ReturnCallback进行相对应的业务处理,这里处理是改变消息的状态。 消费端代码

@RabbitListener(queues = "order.send.queue")
    public void consume(Message message, Channel channel) {
        String orderNo = MessageHelper.msgToObj(message, String.class);
        try {
            log.info("收到发货的消息:orderId:{}", orderNo);
            MsgLog msgLog = msgLogMapper.selectById(orderNo);
            if (msgLog == null || MsgLogEnum.SPENT.equals(msgLog.getStatus())) {
                log.info("重复发货,orderId:{}", orderNo);
                return;
            }
            MessageProperties messageProperties = message.getMessageProperties();
            long deliveryTag = messageProperties.getDeliveryTag();
            MsgLog newMsg = new MsgLog();
            newMsg.setMsgId(msgLog.getMsgId());
            newMsg.setStatus(MsgLogEnum.SPENT);
            newMsg.setUpdateTime(LocalDateTime.now());
            UpdateWrapper<MsgLog> updateWrapper = new UpdateWrapper<>();
            updateWrapper.eq("status",MsgLogEnum.DELIVERY_SUC);
            int count = msgLogMapper.update(newMsg,updateWrapper);
            if (count > 0) {
                log.info("发货成功,msgId:{}", orderNo);
                channel.basicAck(deliveryTag, false);
            } else {
                log.info("发货,重新发送,msgId:{}", orderNo);
                channel.basicNack(deliveryTag, false, true);
            }
        }catch (Exception e){
            log.info("程序异常,发货", orderNo);
            e.printStackTrace();
        }
    }

说明:消费成功,进行手动ack,消费失败进行重发。

定时任务

@Slf4j
@Configuration
@EnableScheduling
public class ResendMsg {


    @Autowired
    private MsgLogMapper msgLogMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 每30秒拉去投递失败的消息,重新投递
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void resend() {
        log.info("开始执行定时任务:重新投递消费失败的消息");
        QueryWrapper<MsgLog> wrapper = new QueryWrapper<>();
        wrapper.eq("status", MsgLogEnum.DELIVERY_FAILED);
        List<MsgLog> msgLogs = msgLogMapper.selectList(wrapper);
        if (msgLogs != null && msgLogs.size() > 0) {
            msgLogs.forEach((msgLog -> {
                String msgId = msgLog.getMsgId();
                if (msgLog.getTryCount() >= 3) {
                    log.info("超过最大投递次数,消息投递失败,msgId:{}", msgId);
                } else {
                    int num = msgLog.getTryCount() + 1;
                    msgLog.setTryCount(num);
                    msgLog.setStatus(MsgLogEnum.SPENT);
                    msgLogMapper.updateById(msgLog);
                    CorrelationData correlationData = new CorrelationData(msgId);
                    rabbitTemplate.convertAndSend(msgLog.getExchange(),
                            msgLog.getRoutingKey(),
                            MessageHelper.objToMsg(msgLog.getMsg()),
                            correlationData);
                    log.info("msgId:{},第{}次重新投递消息", msgId, num);
                }
            }));
        }
        log.info("结束执行定时任务:重新投递失败消息");
    }
}
说明:这里使用SpringBoot自带的定时任务,并每30秒拉去投递失败的消息,重新投递,可以根据业务进行调整。

死信队列

使用场景

  • 发红包过期退款业务
  • 订单到期未支付恢复库存业务
  • 等等

变成死信队列的情况

  • 消息被拒绝(basic.reject/basic.nack)并且requeue = false
  • 消息TTL过期
  • 队列达到最大长度

配置

/**
     * 订单超时交换器
     * @return
     */
    @Bean
    public DirectExchange orderOverTimeExchange(){
        return (DirectExchange) ExchangeBuilder.directExchange(RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey()).durable(true).build();
    }
    /**
     * 归还库存死信队列
     * @return
     */
    @Bean
    public Queue returnInventoryQueue(){
        return new Queue(RabbitKeyEnums.ORDER_RETURN_INVENTORY_QUEUE.getKey());
    }
    /**
     * 订单超时队列
     * @return
     */
    @Bean
    public Queue orderOverTimeQueue(){
        Map<String,Object> args = new HashMap<>(2);
        //x-dead-letter-exchange   声明死信队列Exchange
        args.put("x-dead-letter-exchange",
                RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey());
        //x-dead-letter-routing-key 声明超时订单未支付队列中过期的消息将转发到归还库存队列中
        args.put("x-dead-letter-routing-key",
                RabbitKeyEnums.ORDER_RETURN_INVENTORY_ROUTING_KEY.getKey());
        return QueueBuilder.durable(RabbitKeyEnums.ORDER_PAY_OVERTIME_QUEUE.getKey()).
                withArguments(args).
                build();

    }
    /**
     *  订单超时-绑定
     * @return
     */
    @Bean
    public Binding orderOverTimeBinding(){
        return BindingBuilder.
                bind(orderOverTimeQueue()).
                to(orderOverTimeExchange()).
                with(RabbitKeyEnums.ORDER_PAY_OVERTIME_ROUTING_KEY.getKey());
    }



    /**
     *  归还库存-绑定
     * @return
     */
    @Bean
    public Binding returnInventoryBinding(){
        return BindingBuilder.
                bind(returnInventoryQueue()).
                to(orderOverTimeExchange()).
                with(RabbitKeyEnums.ORDER_RETURN_INVENTORY_ROUTING_KEY.getKey());
    }

发送死信队列消息

 //发送订单未支付恢复库存的死信队列
        CorrelationData correlationData = new CorrelationData(orderNo);
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                MessageProperties messageProperties = message.getMessageProperties();
                //订单未支付超时时间
                messageProperties.setExpiration(String.valueOf(timeOut));
                messageProperties.setContentEncoding("utf-8");
                return message;
            }
        };
        rabbitTemplate.convertAndSend(RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey(),
                RabbitKeyEnums.ORDER_PAY_OVERTIME_ROUTING_KEY.getKey(),
                MessageHelper.objToMsg(orderNo),messagePostProcessor,correlationData);
        log.info("{}订单超时未支付消息已发送",orderNo);

说明:这里发送指定的队列不进行消费,消息过期后,会转发到死信队列中(returnInventoryQueue)

最后

  • 如果对你有帮助,就点个赞吧。
  • 不懂可以在评论区留言,一起进步。
  • 如果哪里有不对的地方,或者有更好的方式,欢迎指出。