前言:

我们在抢购商品的时候总有这样的一种场景,就是我们已经抢购到我们的商品,但是由于我们某种原因没有及时的支付导致订单失效的情况。

那么我们今天就用rabbitmq来实现这么的一个场景。

“死信队列”,顾明思议,是可以延时、延迟一定的时间再处理消息的一种特殊队列,它相对于“普通的队列”而言,可以实现“进入死信队列的消息不立即处理,而是可以等待一定的时间再进行处理”的功能!而普通的队列则不行,即进入队列后的消息会立即被对应的消费者监听消费,如下图所示为普通队列的基本消息模型:

订单超时未支付怎么实现使用kafka 超时未支付的订单_订单超时未支付怎么实现使用kafka

 而对于“死信队列”,它的构成以及使用相对而言比较复杂一点,在正常情况,死信队列由三大核心组件组成:死信交换机+死信路由+TTL(消息存活时间~非必需的),而死信队列又可以由“面向生产者的基本交换机+基本路由”绑定而成,故而生产者首先是将消息发送至“基本交换机+基本路由”所绑定而成的消息模型中,即间接性地进入到死信队列中,当过了TTL,消息将“挂掉”,从而进入下一个中转站,即“面下那个消费者的死信交换机+死信路由”所绑定而成的消息模型中。如下图所示:

订单超时未支付怎么实现使用kafka 超时未支付的订单_java_02

 

引入maven依赖:

<!--rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <version>2.1.5.RELEASE</version>
        </dependency>

填写yml文件的配置信息:application.yml

# Spring配置
spring:
  # rabbitmq
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /

    listener:
      simple:
        concurrency: 20
        max-concurrency: 30
        prefetch: 15


RabbitmqConfig.class 自定义配置信息


import com.ruoyi.common.constant.RabbitMQConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author Lxq
 * @Date 2020/12/17 9:52
 * @Version 1.0
 */
@Slf4j
@Configuration
public class RabbitmqConfig {

    @Autowired
    private Environment env;

    @Autowired
    private CachingConnectionFactory connectionFactory;

    @Autowired
    private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;


    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
        return new RabbitAdmin(connectionFactory);
    }


    /**
     * 单一消费
     *
     * @return
     */
    @Bean(name = "singleListenerContainer")
    public SimpleRabbitListenerContainerFactory listenerContainerFactory() {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setConcurrentConsumers(1);
        factory.setMaxConcurrentConsumers(1);
        factory.setPrefetchCount(1);
        factory.setTxSize(1);
        return factory;
    }


    /**
     * 为了保证数据不被丢失,RabbitMQ支持消息确认机制,为了保证数据能被正确处理而不仅仅是被Consumer收到,那么我们不能采用no-ack,而应该是在处理完数据之后发送ack.
     * 在处理完数据之后发送ack,就是告诉RabbitMQ数据已经被接收,处理完成,RabbitMQ可以安全的删除它了.
     * 如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer,这样就保证在Consumer异常退出情况下数据也不会丢失.
     * RabbitMQ它没有用到超时机制.RabbitMQ仅仅通过Consumer的连接中断来确认该Message并没有正确处理,也就是说RabbitMQ给了Consumer足够长的时间做数据处理。
     * 如果忘记ack,那么当Consumer退出时,Mesage会重新分发,然后RabbitMQ会占用越来越多的内存.
     * <p>
     * 无ack模式(AcknowledgeMode.NONE)
     * 有ack模式(AcknowledgeMode.AUTO,AcknowledgeMode.MANUAL)
     * <p>
     * AcknowledgeMode.MANUAL模式需要人为地获取到channel之后调用方法向server发送ack(或消费失败时的nack)信息。
     * <p>
     * AcknowledgeMode.AUTO模式下,由spring-rabbit依据消息处理逻辑是否抛出异常自动发送ack(无异常)或nack(异常)到server端。
     */

    /**
     * 消费者的数量 默认 20
     */
    private static final Integer spring_rabbitmq_listener_simple_concurrency = 20;
    /**
     * 最大的消费者数量
     */
    private static final Integer spring_rabbitmq_listener_simple_max_concurrency = 30;
    /**
     * 每个消费者获取最大投递数量
     */
    private static final Integer spring_rabbitmq_listener_simple_prefetch = 15;

    /**
     * 多个消费值
     *
     * @return
     */
    @Bean(name = "multiListenerContainer")
    public SimpleRabbitListenerContainerFactory multiListenerContainer() {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factoryConfigurer.configure(factory, connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        // 确认消费模式(无ack)
        factory.setAcknowledgeMode(AcknowledgeMode.NONE);
        factory.setConcurrentConsumers(spring_rabbitmq_listener_simple_concurrency);
        factory.setMaxConcurrentConsumers(spring_rabbitmq_listener_simple_max_concurrency);
        factory.setPrefetchCount(spring_rabbitmq_listener_simple_prefetch);
        return factory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate() {
        // 发布者确认
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        /**
         * mandatory:交换器无法根据自身类型和路由键找到一个符合条件的队列时的处理方式
         * true:RabbitMQ会调用Basic.Return命令将消息返回给生产者
         * false:RabbitMQ会把消息直接丢弃
         */
        rabbitTemplate.setMandatory(true);
        /**
         * ConfirmCallback:每一条发到rabbitmq server的消息都会调一次confirm方法。
         * 如果消息成功到达exchange,则ack参数为true,反之为false;
         * cause参数是错误信息;
         * CorrelationData可以理解为context,在发送消息时传入的这个参数,此时会拿到。
         */
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                log.info("消息发送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause);
            }
        });

        /**
         * ReturnCallback:成功到达exchange,但routing不到任何queue时会调用。
         * 可以看到这里能直接拿到message,exchange,routingKey信息。
         */
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                log.warn("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message);
            }
        });
        return rabbitTemplate;
    }


    //构建异步发送邮箱通知的消息模型

    @Bean
    public Queue successEmailQueue() {
        return new Queue(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_EMAIL_QUEUE, true);
    }

    @Bean
    public TopicExchange successEmailExchange() {
        return new TopicExchange(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_EMAIL_EXCHANGE, true, false);
    }

    @Bean
    public Binding successEmailBinding() {
        return BindingBuilder.bind(successEmailQueue()).to(successEmailExchange()).with(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_EMAIL_ROUTING_KEY);
    }


    /**
     * 死信队列
     */
    @Bean
    public Queue successKillDeadQueue() {
        Map<String, Object> argsMap = new HashMap(2);
        // 死信交换机
        argsMap.put("x-dead-letter-exchange", RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_EXCHANGE);
        // 死信路由
        argsMap.put("x-dead-letter-routing-key", RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_ROUTING_KEY);
        // 死信队列
        return new Queue(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_QUEUE, true, false, false, argsMap);
    }


    /**
     * 基本交换机
     */
    @Bean
    public TopicExchange successKillDeadProdExchange() {
        return new TopicExchange(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_PROD_EXCHANGE, true, false);
    }

    /**
     * 创建基本交换机+基本路由 -> 死信队列 的绑定
     */
    @Bean
    public Binding successKillDeadProdBinding() {
        return BindingBuilder.bind(successKillDeadQueue()).to(successKillDeadProdExchange()).with(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_PROD_ROUTING_KEY);
    }

    /**
     * 真正的队列
     */
    @Bean
    public Queue successKillRealQueue() {
        return new Queue(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_REAL_QUEUE, true);
    }

    /**
     * 死信交换机
     */

    @Bean
    public TopicExchange successKillDeadExchange() {
        return new TopicExchange(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_EXCHANGE, true, false);
    }


    /**
     * 死信交换机+死信路由->真正队列 的绑定
     */

    @Bean
    public Binding successKillDeadBinding() {
        return BindingBuilder.bind(successKillRealQueue()).to(successKillDeadExchange()).with(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_ROUTING_KEY);
    }


}

从上面的配置信息可以看出,首先是定义个死信队列 = 死信交换机 + 死信路由 ,然后用 基本交换机 + 基本路由 - > 绑定到我们的死信队列、 接着按照上面的模型图,我们还需要的是将 死信交换机 + 死信路由 -> 绑定到基本的队列

附上常量类:RabbitMQConstants 

/**
 * @Author Lxq
 * @Date 2020/12/17 14:53
 * @Version 1_0
 */
public class RabbitMQConstants {


    /**
     * 秒杀成功异步发送邮件的消息模型
     */
    public static final String MQ_KILL_ITEM_SUCCESS_EMAIL_QUEUE = "kill_item_success_email_queue";

    public static final String MQ_KILL_ITEM_SUCCESS_EMAIL_EXCHANGE = "kill_item_success_email_exchange";

    public static final String MQ_KILL_ITEM_SUCCESS_EMAIL_ROUTING_KEY = "kill_item_success_email_routing_key";

    /**
     * 死信队列
     */
    public static final String MQ_KILL_ITEM_SUCCESS_KILL_DEAD_QUEUE = "kill_item_success_kill_dead_queue";
    /**
     * 死信交换机
     */
    public static final String MQ_KILL_ITEM_SUCCESS_KILL_DEAD_EXCHANGE = "kill_item_success_kill_dead_exchange";
    /**
     * 死信路由key
     */
    public static final String MQ_KILL_ITEM_SUCCESS_KILL_DEAD_ROUTING_KEY = "kill_item_success_kill_dead_routing_key";

    /**
     * 正常队列
     */
    public static final String MQ_KILL_ITEM_SUCCESS_KILL_DEAD_REAL_QUEUE = "kill_item_success_kill_dead_real_queue";
    /**
     * 正常交换机
     */
    public static final String MQ_KILL_ITEM_SUCCESS_KILL_DEAD_PROD_EXCHANGE = "kill_item_success_kill_dead_prod_exchange";
    /**
     * 正常路由key
     */
    public static final String MQ_KILL_ITEM_SUCCESS_KILL_DEAD_PROD_ROUTING_KEY = "kill_item_success_kill_dead_prod_routing_key";
}

接下来就是如何去发送消息到MQ中,那么我定义一个通用的service


RabbitSenderService


/**
 * @Author Lxq
 * @Date 2020/12/17 9:50
 * @Version 1.0
 * RabbitMQ通用的消息发送服务
 */
@Slf4j
@Service
public class RabbitSenderService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ItemKillSuccessMapper itemKillSuccessMapper;

    /**
     * 秒杀成功后生成抢购订单-发送信息入死信队列,等待着一定时间失效超时未支付的订单
     *
     * @param orderCode
     */
    public void sendKillSuccessOrderExpireMsg(String orderCode) {
        try {
            if (StringUtils.isNotEmpty(orderCode)) {
                KillSuccessUserInfo info = itemKillSuccessMapper.selectByCode(orderCode);
                if (info != null) {
                    rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
                    rabbitTemplate.setExchange(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_PROD_EXCHANGE);
                    rabbitTemplate.setRoutingKey(RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_PROD_ROUTING_KEY);
                    rabbitTemplate.convertAndSend(info, new MessagePostProcessor() {
                        @Override
                        public Message postProcessMessage(Message message) throws AmqpException {
                            MessageProperties mp = message.getMessageProperties();
                            mp.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                            mp.setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME, KillSuccessUserInfo.class);
                            //TODO:动态设置TTL(为了测试方便,暂且设置20s)
                            // 消息的失效时间(下单并且没有支付限定的时间)
                            mp.setExpiration("20000");
                            return message;
                        }
                    });
                }
            }
        } catch (Exception e) {
            log.error("秒杀成功后生成抢购订单-发送信息入死信队列,等待着一定时间失效超时未支付的订单-发生异常,消息为:{}", orderCode, e.fillInStackTrace());
        }
    }
}

这里其实就是模拟秒杀 成功之后,我就会调用这个方法将订单的编号传来获取订单信息,然后将订单信息通过MQ TTL 发送

这样子设置就是默认订单下单之后10秒钟没有支付,待会下面写的监听器就会监听这个队列,然后处理这里超时没有支付的信息,将状态进行修改。

这里要注意的是发送消息设置的交换机和路由key(基本交换机 + 基本的路由key)

订单的监听service


RabbitReceiverService


/**
 * @Author Lxq
 * @Date 2020/12/17 15:29
 * @Version 1.0
 * RabbitMQ通用的消息接收服务
 */
@Slf4j
@Service
public class RabbitReceiverService {


    @Autowired
    private ItemKillSuccessMapper itemKillSuccessMapper;

    @RabbitListener(queues = {RabbitMQConstants.MQ_KILL_ITEM_SUCCESS_KILL_DEAD_REAL_QUEUE}, containerFactory = "singleListenerContainer")
    public void consumeExpireOrder(KillSuccessUserInfo info) {
        try {
            log.info("用户秒杀成功后超时未支付-监听者-接收消息:{}", info);

            if (info != null) {
                ItemKillSuccess entity = itemKillSuccessMapper.selectByPrimaryKey(info.getCode());
                if (entity != null && entity.getStatus().intValue() == 0) {
                    itemKillSuccessMapper.expireOrder(info.getCode());
                }
            }
        } catch (Exception e) {
            log.error("用户秒杀成功后超时未支付-监听者-发生异常:", e.fillInStackTrace());
        }
    }
}

这里我们监听基本的队列。

然后附上测试图:

订单超时未支付怎么实现使用kafka 超时未支付的订单_java_03