引言

最近工作中开始接触支付、订单等相关业务内容,想要良好的解决订单超时未支付自动关闭的问题。传统解决方法有两种:

  1. 被动触发。只有当用户或商户查询订单信息时,再判断该订单是否超时,如果超时再进行超时逻辑的处理。这种做法实现简单,但是这种做法会导致用户体验极差,打开订单时需要极多的处理判断,甚至会对库存、订单量的统计带来误差。
  2. 写同步定时任务,定时扫描数据库表中的数据。这种处理方式只是适用比较小而简单的项目,当业务规模扩展时,依旧会带来很多问题,比如:效率极低;订单关闭不及时,订单自动关闭的及时与否取决于设置的扫描时间窗口,如果你设置每分钟定时轮询一次,那么订单取消时间的最大误差就有一分钟。当然也可能更大,比如一分钟之内有大量数据,但是一分钟没处理完,那么下一分钟的就会顺延;增加数据库的压力等。

在查阅资料之后,我们的解决方案寄望于延时队列的处理方式上。

延时队列的这种实现方式,包含两个重要的数据结构:环形队列,例如可以创建一个包含 2400 个 slot 的环形队列(本质是个数组);任务集合,环上每一个 slot 是一个 Set。本质上,就是一个时间轮算法的一个实现。

订单到期关闭如何使用redisson_订单到期关闭如何使用redisson

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的 tick 数),tickDuration(一个 tick 的持续时间)以及 timeUnit(时间单位),例如当 ticksPerWheel=60,tickDuration=1,timeUnit =秒,这就和现实中的始终的秒针走动完全类似了。

如果当前指针指在 1 上面,我有一个任务需要 4 秒以后执行,那么这个执行的线程回调或者消息将会被放在 5 上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到 7,如果要 20 秒,指针需要多转2圈。位置是在2圈之后的 5 上面(20 % 7 + 1)。

目前有很多消息队列都支持,比如 RocketMQ,RabbitMQ 等,本文以RabbitMQ为例,介绍如何使用RabbitMQ进行延时队列的创建。

1. Docker 安装Rabbit MQ
docker pull rabbitmq:management
docker run -dit --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:management
2. Rabbit MQ安装延迟队列插件

下载与安装的Rabbitmq匹配的插件

rabbitmq-delayed-message-exchange

订单到期关闭如何使用redisson_java_02

上传到服务器的/root文件夹下,然后进行如下操作

docker cp /root/rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq:/plugins

docker exec -it rabbitmq /bin/bash

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

rabbitmq-plugins list

看到如下图所示即为安装成功

订单到期关闭如何使用redisson_java_03

3. pom引入Rabbit MQ
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
4. 创建RabbitConfig配置类
@Configuration
@RequiredArgsConstructor
public class RabbitConfig {

    private final CachingConnectionFactory connectionFactory;

    private final SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;
    
    /**
     * 多个消费者
     *
     * @return SimpleRabbitListenerContainerFactory
     */
    @Bean(name = "multiListenerContainer")
    public SimpleRabbitListenerContainerFactory multiListenerContainer() {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factoryConfigurer.configure(factory, connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.NONE);
        factory.setConcurrentConsumers(10);
        factory.setMaxConcurrentConsumers(15);
        factory.setPrefetchCount(10);
        return factory;
    }

    @Bean
    public Queue reserveDelayQueue() {
        return new Queue("order.delay.queue.name", true);
    }

    @Bean
    public CustomExchange reserveDelayExchange() {
        Map<String, Object> pros = new HashMap<>(2);
        pros.put("x-delayed-type", "topic");
        return new CustomExchange("order.delay.exchange.name", "x-delayed-message", true, false, pros);
    }

    @Bean
    public Binding reserveDelayBindingNotify(@Qualifier("reserveDelayQueue") Queue queue,
                                 @Qualifier("reserveDelayExchange") CustomExchange customExchange) {
        return BindingBuilder.bind(queue).to(customExchange).with("order.delay.routing.key.name").noargs();
    }


}
3. 创建生产者
@Slf4j
@Component
@RequiredArgsConstructor
public class CancelOrderProducer {

    private final RabbitTemplate rabbitTemplate;

    public void sendMsg(Long orderId, Integer delayTime){
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        // 设置基本交换机
        rabbitTemplate.setExchange("order.delay.exchange.name");
        // 设置基本路由
        rabbitTemplate.setRoutingKey("order.delay.routing.key.name");

        rabbitTemplate.convertAndSend(orderId, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                MessageProperties messageProperties = message.getMessageProperties();
                messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                message.getMessageProperties().setDelay(delayTime);
                return message;
            }
        });

        log.info("用户下单支付超时-发送用户下单记录id的消息到延迟队列:orderId={}", orderId);
    }


}
4. 业务中引用
@Slf4j
@Service
@RequiredArgsConstructor
public class ReserveServiceImpl extends ServiceImpl<ReserveMapper, HaReserve> implements ReserveService {
    // 引入生产者
    private final CancelOrderProducer cancelOrderProducer;
    
    @Value("${reserve.release-time}")
    private int autoCloseTime;

    @Override
    public JSONObject doReserve(ReserveDTO reserveDTO) {
    	... ...
    	// 省略业务代码
    	cancelOrderProducer.sendMsg(reserve.getId(), autoCloseTime);
    	return null;
    }
}
5. 创建消费者接收超时消息

具体的业务处理写在此处,一定注意要消费后要手动提交ack!!!

@Slf4j
@Component
@RequiredArgsConstructor
public class CancelOrderConsumer {

    private final RemoteBasicService remoteBasicService;

    @RabbitHandler
    @RabbitListener(queues = "order.delay.queue.name", containerFactory = "multiListenerContainer")
    public void handle(@Payload String orderId, Channel channel, Message message) {
        try {
            //业务处理
            log.info("用户下单支付超时取消订单-取消订单id:orderId={}",orderId);
            handleService(orderId, channel, message);

        } catch (Exception e) {
            log.error("{}", e.getMessage(), e);
        }
    }

    public void handleService(String orderId, Channel channel, Message message) throws IOException, InterruptedException {

        // channel.basicAck(long,boolean); 确认收到消息,消息将被队列移除,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息。
        // channel.basicNack(long,boolean,boolean); 确认否定消息,第一个boolean表示一个consumer还是所有,第二个boolean表示requeue是否重新回到队列,true重新入队。
        // channel.basicReject(long,boolean); 拒绝消息,requeue=false 表示不再重新入队,如果配置了死信队列则进入死信队列。

        // 具体处理的业务代码省略
        ... ...
}
参考文章
  1. 订单超时未支付自动关闭的几种实现方案