【项目实战】SpringBoot集成RabbitMQ实现延时队列、死信队列实现订单30分钟过期

  • 依赖引入
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • MQ枚举类
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum QueueEnum {

    /**
     * 死信队列
     */
    QUEUE_ORDER_CANCEL("order_dlx_exchange:", "order_dlx_queue:", "order_dlx_key"),

    /**
     * 延时队列
     */
    QUEUE_TTL_ORDER_CANCEL("order_ttl_exchange:", "order_ttl_queue:", "order_ttl_key"),

    /**
     * 退款队列
     */
    REFUND_QUEUE("refund_exchange", "refund_queue:", "refund_key"),
    
    ;

    /**
     * 交换名称
     */
    private final String exchange;
    /**
     * 队列名称
     */
    private final String queueName;
    /**
     * 路由键
     */
    private final String routeKey;
}
  • MQ配置类 - 声明Queue和Exchange与相互之间的Binding关系
import cn.hutool.core.map.MapUtil;
import com.demo.common.enums.QueueEnum;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;

/**
 * 消息队列 配置类
 */
@EnableRabbit
@Configuration
public class MqConfig {
    
    /* 订单退款相关 start */
    /**
     * 退款的消息队列
     */
    @Bean
    public Queue refundQueue() {
        return new Queue(QueueEnum.REFUND_QUEUE.getQueueName());
    }

    /**
     * 退款的exchange
     */
    @Bean
    public CustomExchange refundExchange() {

        Map<String, Object> param = MapUtil.newHashMap();
        param.put("x-delayed-type", "direct");
        return new CustomExchange(QueueEnum.REFUND_QUEUE.getExchange(), "x-delayed-message", true, false, param);
    }

    /**
     * 绑定退款队列、交换机的关系
     */
    @Bean
    public Binding binding(
            @Qualifier("refundQueue") Queue refundQueue,
            @Qualifier("refundExchange") CustomExchange exchange
    ) {
        return BindingBuilder.bind(refundQueue).to(exchange).with(QueueEnum.REFUND_QUEUE.getRouteKey()).noargs();
    }
    /* 订单退款相关 end */

    /* 延时队列 start */

    /**
     * 订单延迟队列
     */
    @Bean
    public Queue orderTtlQueue() {
        return QueueBuilder
                .durable(QueueEnum.QUEUE_TTL_ORDER_CANCEL.getQueueName())
                 //到期后转发的交换机
                .deadLetterExchange(QueueEnum.QUEUE_ORDER_CANCEL.getExchange())
                 //到期后转发的路由键
                .deadLetterRoutingKey(QueueEnum.QUEUE_ORDER_CANCEL.getRouteKey())
                .build();
    }

    /**
     * 订单延迟队列所绑定的交换机
     */
    @Bean
    public DirectExchange orderTtlDirect() {
        return ExchangeBuilder
                .directExchange(QueueEnum.QUEUE_TTL_ORDER_CANCEL.getExchange())
                .durable(true)
                .build();
    }

    /**
     * 将订单延迟队列绑定到交换机
     */
    @Bean
    Binding orderTtlBinding(
            @Qualifier("orderTtlDirect") DirectExchange orderTtlDirect,
            @Qualifier("orderTtlQueue") Queue orderTtlQueue
    ) {
        return BindingBuilder
                .bind(orderTtlQueue)
                .to(orderTtlDirect)
                .with(QueueEnum.QUEUE_TTL_ORDER_CANCEL.getRouteKey());
    }
    /* 延时队列 end */

    /* 死信队列 start */

    /**
     * 死信队列
     */
    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable(QueueEnum.QUEUE_ORDER_CANCEL.getQueueName()).build();
    }

    /**
     * 死信交换机
     */
    @Bean
    public DirectExchange orderDirect() {
        return ExchangeBuilder
                .directExchange(QueueEnum.QUEUE_ORDER_CANCEL.getExchange())
                .build();
    }

    /**
     * 绑定死信关系
     */
    @Bean
    Binding orderBinding(
            @Qualifier("orderDirect") DirectExchange orderDirect,
            @Qualifier("orderQueue") Queue orderQueue
    ) {
        return BindingBuilder
                .bind(orderQueue)
                .to(orderDirect)
                .with(QueueEnum.QUEUE_ORDER_CANCEL.getRouteKey());
    }

    /* 死信队列 end */


    /**
     * 自定义mq消息转换 , 确保监听端可以接收任何类型的消息
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        return rabbitTemplate;
    }
  • 消息生产者 - 生产消息发送至延时队列
import cn.hutool.core.map.MapUtil;
import com.demo.common.enums.QueueEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 订单消息的发送者
 */
@Slf4j
@Component
public class OrderSender {

    @Autowired
    private AmqpTemplate amqpTemplate;

    @Value("${mq.queue_ttl_order_cancel.exchange}")
    private String exchange;

    @Value("${mq.queue_ttl_order_cancel.routekey}")
    private String routeKey;

    /**
     * 发送订单生成消息至 MQ
     * @param orderId 订单id
     * @param delayTimes 存活时间
     */
    public void sendOrderMessage(Long orderId, final Long delayTimes) {
        //给延迟队列发送消息
        Map<String, String> map = MapUtil.newHashMap();
        map.put("orderId", orderId.toString());
        amqpTemplate.convertAndSend(QueueEnum.QUEUE_TTL_ORDER_CANCEL.getExchange(), QueueEnum.QUEUE_TTL_ORDER_CANCEL.getRouteKey(), map, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //给消息设置有效时间, 时间到达后会将此消息丢入死信队列进行处理
                message.getMessageProperties().setExpiration(delayTimes.toString());
                return message;
            }
        });
        log.info("-----------超时订单队列发送订单成功 orderId:{}-----------", orderId.toString());
    }
}
  • 消息消费者 - 监听死信队列进行消息消费
import com.alibaba.fastjson2.JSONObject;
import com.demo.api.client.service.OrderService;
import com.demo.common.constant.CustomConstants;
import com.demo.common.manager.OrderManager;
import com.demo.common.server.IMessageRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 订单消息的接收者
 */
@Slf4j
@Component
public class OrderReceiver {

    @Autowired
    private OrderService orderService;
    @Autowired
    private OrderManager orderManager;
    @Autowired
    private IMessageRecordService iMessageRecordService;

    // 监听死信队列
    @RabbitListener(queues = "${mq.queue_ttl_order_cancel.queue}")
    @Transactional(rollbackFor = Exception.class)
    public void handle(Message message, Channel channel) throws IOException {
        byte[] body = message.getBody();
        String s = new String(body, StandardCharsets.UTF_8);
        JSONObject parse = JSONObject.parse(s);
        Long orderId = parse.getLong("orderId");

        log.info("-----------超时订单队列开始消费 orderId = {}-----------",orderId.toString());
        try {
            // 走订单超时的逻辑
            orderService.cancelTimeOutOrderMq(orderId);
            // 记录超时订单被消费
            iMessageRecordService.saveInfo(CustomConstants.ONE_INT, orderId);
            // 消息确认成功消费
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            log.info("-----------超时订单队列消息接收成功 orderId:{}-----------",orderId.toString());
        } catch (Exception e) {
            e.printStackTrace();
            // 消息消费失败重新入队
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
            log.error("-----------超时订单队列消息接收失败,重新入队 orderId:{}-----------",orderId.toString());
        }
    }
}
  • 退款队列 - 消息产生者
import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 退款订单发送者
 */
@Slf4j
@Component
public class RefundOrderSender {

    @Autowired
    private AmqpTemplate amqpTemplate;

    @Value("${mq.refund_queue.exchange}")
    private String exchange;

    @Value("${mq.refund_queue.routekey}")
    private String routeKey;

    /**
     * 发送退款信息至mq
     * @param refundOrderId 退款单id
     * @param delayTimes 等待时长
     */
    public void sendRefundOrderMessage(Long refundOrderId, final long delayTimes) {
        //给延迟队列发送消息
        Map<String, String> map = MapUtil.newHashMap();
        map.put("refundOrderId", refundOrderId.toString());
        amqpTemplate.convertAndSend(exchange, routeKey, map, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //给消息设置延迟毫秒值
                message.getMessageProperties().setExpiration(String.valueOf(delayTimes));
                return message;
            }
        });
        log.info("-----------退款队列发送消息成功 refundOrderId:{}-----------", refundOrderId.toString());
    }
}
  • 退款队列 - 消息消费者
import com.alibaba.fastjson2.JSONObject;
import com.demo.api.admin.service.OrderService;
import com.demo.common.constant.CustomConstants;
import com.demo.common.server.IMessageRecordService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 退款订单接收者
 */
@Slf4j
@Component
public class RefundOrderReceiver {

    @Autowired
    private OrderService orderService;
    @Autowired
    private IMessageRecordService iMessageRecordService;

    @RabbitListener(queues = "${mq.refund_queue.queue}")
    @Transactional(rollbackFor = Exception.class)
    public void handle(Message message, Channel channel) throws IOException {
        byte[] body = message.getBody();
        String s = new String(body, StandardCharsets.UTF_8);
        JSONObject parse = JSONObject.parse(s);
        Long refundOrderId = parse.getLong("refundOrderId");

        log.info("-----------退款队列开始消费 refund_order_id = {} -----------", refundOrderId.toString());
        try {
            orderService.refundOrderAfter(refundOrderId);
            // 记录超时订单被消费
            iMessageRecordService.saveInfo(CustomConstants.TWO_INT, refundOrderId);

            // 消息确认成功消费
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            log.info("-----------退款队列消息接收成功 refundOrderId:{}-----------", refundOrderId.toString());

        } catch (Exception e) {
            // 消息消费失败重新入队
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            log.error("-----------退款队列消息接收失败,重新入队 refundOrderId:{}-----------", refundOrderId.toString());
            throw e;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值