面试官:生成订单30分钟未支付,则自动取消,该怎么实现

大家好,我是一安,之前有介绍使用RabbitMQ延时队列实现,一种是基于死信队列,但使用死信会存在两个弊端,所以又提到一种基于rabbitmq_delayed_message_exchange插件的方式,这两种是比较常见的和使用的,面试能答出来这两种,应该没什么问题,但我们不能局限这两种,下面就介绍一下几种不同的实现方式

数据库轮询

这种方式一般适合小型项目,通过启一个定时任务,周期性的扫描数据库,获取订单信息,然后执行状态更新或删除操作

常见的基本都是基于quartz实现定时轮询

伪代码:

1.引入依赖

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-quartz</artifactId>
  </dependency>

2.配置定时任务

注意在启动类加上@EnableScheduling注解,否则无法开启定时任务

这里只是简单写个quartz伪代码,实际开发中定时任务可能会比较复杂一些,详细可参看《Quartz基于配置实现动态定时任务执行

@Component
@Slf4j
public class CancelTheOrderTask {


    @Scheduled(cron = "0/10 * * * * ?}")
    public void run(){
        log.info("开始扫描数据库订单信息......");

        log.info("修改或删除超时未支付定时信息......");

        log.info("结束扫描数据库订单信息......");
    }
}

4.输出信息

2022-09-03 13:33:00.013  INFO 23656 --- [   scheduling-1] c.capitek.controller.CancelTheOrderTask  : 开始扫描数据库订单信息......
2022-09-03 13:33:00.014  INFO 23656 --- [   scheduling-1] c.capitek.controller.CancelTheOrderTask  : 修改或删除超时未支付定时信息......
2022-09-03 13:33:00.014  INFO 23656 --- [   scheduling-1] c.capitek.controller.CancelTheOrderTask  : 结束扫描数据库订单信息......
2022-09-03 13:33:10.008  INFO 23656 --- [   scheduling-1] c.capitek.controller.CancelTheOrderTask  : 开始扫描数据库订单信息......
2022-09-03 13:33:10.009  INFO 23656 --- [   scheduling-1] c.capitek.controller.CancelTheOrderTask  : 修改或删除超时未支付定时信息......
2022-09-03 13:33:10.010  INFO 23656 --- [   scheduling-1] c.capitek.controller.CancelTheOrderTask  : 结束扫描数据库订单信息......

优点:

  • 简单易行,quartz本身也支持集群操作

缺点:

  • 存在延迟,试想你如果每隔30分钟轮询一次,一个订单刚好过29分钟,那你就要下个30分钟才可以处理这个订单信息
  • 如果每秒轮询一次,对服务器消耗太大
  • 而且随着你订单业务量的加大,也会增加数据库的压力

所以这种方式不常见,也不常用,但它也是方案之一

JDK自带的延迟队列

之前有篇文章专门介绍了一下JDK中几种常见的队列,里面就有介绍DelayQueue,详细可参考《如何优雅的使用Java队列》,今天以代码的方式给大家做个简单实现

注意:队列中的元素必须实现Delayed接口,才可以使用DelayQueue

伪代码:

public class OrderDelay implements Delayed {
        //订单号
        private String orderId;
        //订单超时时间
        private long timeout;

        public OrderDelay(String orderId, long timeout) {
            this.orderId = orderId;
            this.timeout = timeout + System.nanoTime();
        }

        public int compareTo(Delayed other) {

            if (other == this) return 0;

            OrderDelay t = (OrderDelay) other;

            long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));

            return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
        }

        // 返回距离你自定义的超时时间还剩多少
        public long getDelay(TimeUnit unit) {
            return unit.convert(timeout - System.nanoTime(),TimeUnit.NANOSECONDS);
        }

        public void print() {
            System.out.println(orderId+"编号的订单修改或删除......");
        }
    
       public static void main(String[] args) {
        DelayQueue<OrderDelay> queue = new DelayQueue<OrderDelay>();
        long start = System.currentTimeMillis();
        
        //这里做个简单模拟,实际可以提交订单时放入延迟队列中
        for(int i = 0;i<5;i++){
            //延迟10秒取出
            queue.put(new OrderDelay("0000-"+i, TimeUnit.NANOSECONDS.convert(10,TimeUnit.SECONDS)));

            try {
                queue.take().print();
                System.out.println((System.currentTimeMillis()-start) + " ms");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出信息

0000-0编号的订单修改或删除......10013 ms
0000-1编号的订单修改或删除......20022 ms
0000-2编号的订单修改或删除......30036 ms
0000-3编号的订单修改或删除......40048 ms
0000-4编号的订单修改或删除......50063 ms

优点:

  • 效率高,任务触发时间延迟低

缺点:

  • 集群化扩展比较麻烦
  • 而且随着你订单业务量的加大,会增加服务器压力,有可能出现缓存溢出

时间轮算法

网上看到一篇基于NettyHashedWheelTimer的实现,这里也介绍一下

JAVA本身提供了java.util.Timer和java.util.concurrent.ScheduledThreadPoolExecutor等多种Timer工具,但是这些工具在执行效率上面还是有些缺陷,比如Timer,想要提交任务的话需要创建一个TimerTask类,于是netty提供了HashedWheelTimer,一个优化的Timer类

HashedWheelTimer本质是一种类似延迟任务队列的实现,适用于对时效性不高的,可快速执行的,大量这样的“小”任务,能够做到高性能,低消耗

伪代码:

1.引入依赖

<dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.24.Final</version>
</dependency>

2.配置定时任务

HashedWheelTimer

public class MyTimerTask implements TimerTask {

    @Override
    public void run(Timeout timeout) throws Exception {
        System.out.println("修改或删除超时未支付定时信息......");
    }

    public static void main(String[] args) {
        MyTimerTask myTimerTask = new MyTimerTask();
        Timer timer = new HashedWheelTimer();
        timer.newTimeout(myTimerTask, 5, TimeUnit.SECONDS);
        timer.newTimeout(myTimerTask, 10, TimeUnit.SECONDS);
    }
    
    修改或删除超时未支付定时信息......2022-09-03T14:54:22.440
    修改或删除超时未支付定时信息......2022-09-03T14:54:27.380
}

java.util.Timer

public class MyTimerTask2 extends TimerTask {
    @Override
    public void run() {
        System.out.println("修改或删除超时未支付定时信息......"+ LocalDateTime.now());
    }

    public static void main(String[] args) {
        MyTimerTask2 timerTask = new MyTimerTask2();
        Timer timer = new Timer();
        timer.schedule(timerTask, 1000,5000);
        timer.schedule(timerTask, 1000,10000); //会报Exception in thread "main" java.lang.IllegalStateException: Task already scheduled or cancelled 异常
    }
}


Exception in thread "main" java.lang.IllegalStateException: Task already scheduled or cancelled
 at java.util.Timer.sched(Unknown Source)
 at java.util.Timer.schedule(Unknown Source)
 at com.capitek.controller.MyTimerTask2.main(MyTimerTask2.java:17)
修改或删除超时未支付定时信息......2022-09-03T15:09:44.947

优点:

  • 效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低

缺点:

  • 集群化扩展比较麻烦
  • 而且随着你订单业务量的加大,会增加服务器压力,有可能出现缓存溢出

Redis

redis的zset

zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值

伪代码:

 @GetMapping("/test")
    public void test() throws Exception {
        RedisTemplate redisTemplate = redisService.getRedisTemplate();
        for (int i = 0; i < 5; i++) {
            redisTemplate.opsForZSet().add("OrderId","00000"+i,LocalDateTime.now().plusSeconds(5+i).toInstant(ZoneOffset.of("+8")).toEpochMilli());
        }

        listenDelayLoop();
    }

    /**
     * 监听延迟消息
     */
    public void listenDelayLoop() {
        RedisTemplate redisTemplate = redisService.getRedisTemplate();
        while (true) {
            // 获取一个到点的消息
            Set<String> set = redisTemplate.opsForZSet().rangeByScore("OrderId", 0, LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli(),0,1);

            // 如果没有,就等等
            if (set.isEmpty()) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 继续执行
                continue;
            }
            // 获取具体消息的key
            String it = set.iterator().next();
            // 删除成功
            if (redisTemplate.opsForZSet().remove("OrderId", it) > 0) {
                // 拿到任务
                System.out.println("消息到期"+it+",时间为"+ LocalDateTime.now());
            }
        }
    }

输出:

消息到期000000,时间为2022-09-03T15:46:18.165
消息到期000001,时间为2022-09-03T15:46:19.172
消息到期000002,时间为2022-09-03T15:46:20.197
消息到期000003,时间为2022-09-03T15:46:21.206
消息到期000004,时间为2022-09-03T15:46:22.225

过期监听

Redis 自动过期的实现方式是:定时任务离线扫描并删除部分过期键;在访问键时惰性检查是否过期并删除过期键。

Redis 从未保证会在设定的过期时间立即删除并发送过期通知。实际上,过期通知晚于设定的过期时间数分钟的情况也比较常见。

此外键空间通知采用的是发送即忘(fire and forget)策略,并不像消息队列一样保证送达。当订阅事件的客户端会丢失所有在断线期间所有分发给它的事件。

这是一种比定时扫描数据库更 “LOW” 的解决方案

伪代码:

1.检查是否开启redis过期监听,修改redis.conf 文件

notify-keyspace-events Ex #修改为EX

2.创建RedisKeyExpirationListener

package com.test.springbootredislisten.listen;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.io.IOException;


@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    /**
     * Redis失效事件 key
     *
     * @param message
     * @param pattern
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiraKey = message.toString();
        System.out.println(expiraKey);
        //这里写业务逻辑 

    }
}

3.创建RedisListenerConfig

package com.test.springbootredislisten.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;


@Configuration
public class RedisListenerConfig {
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}

4.创建RedisConfig

package com.test.springbootredislisten.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

消息队列

这里不再详细介绍,可以参考《一文让你了解rabbitmq》,文中附有代码

号外!号外!

如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!

面试官:生成订单30分钟未支付,则自动取消,该怎么实现

如何优雅的使用Java队列


一文让你了解rabbitmq


Quartz基于配置实现动态定时任务执行


面试官:生成订单30分钟未支付,则自动取消,该怎么实现


原文始发于微信公众号(一安未来):面试官:生成订单30分钟未支付,则自动取消,该怎么实现

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44868.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!