订单自动取消的11种实现方式(下)

这篇具有很好参考价值的文章主要介绍了订单自动取消的11种实现方式(下)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、监听Redis过期key

在Redis中,有个发布订阅的机制
订单自动取消的11种实现方式(下),架构篇,java
生产者在消息发送时需要到指定发送到哪个channel上,消费者订阅这个channel就能获取到消息。图中channel理解成MQ中的topic。

并且在Redis中,有很多默认的channel,只不过向这些channel发送消息的生产者不是我们写的代码,而是Redis本身。这里面就有这么一个channel叫做__keyevent@__:expired,db是指Redis数据库的序号。

当某个Redis的key过期之后,Redis内部会发布一个事件到__keyevent@__:expired这个channel上,只要监听这个事件,那么就可以获取到过期的key。

所以基于监听Redis过期key实现延迟任务的原理如下:

  • 将延迟任务作为key,过期时间设置为延迟时间
  • 监听__keyevent@__:expired这个channel,那么一旦延迟任务到了过期时间(延迟时间),那么就可以获取到这个任务

1、来个demo

Spring已经实现了监听__keyevent@*__:expired这个channel这个功能,__keyevent@*__:expired中的*代表通配符的意思,监听所有的数据库。

所以demo写起来就很简单了,只需4步即可

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>

配置文件

spring:
  redis:
    host: 192.168.200.144
    port: 6379

配置类

@Configuration
public class RedisConfiguration {

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(connectionFactory);
        return redisMessageListenerContainer;
    }

    @Bean
    public KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer redisMessageListenerContainer) {
        return new KeyExpirationEventMessageListener(redisMessageListenerContainer);
    }
}

KeyExpirationEventMessageListener实现了对__keyevent@*__:expiredchannel的监听
订单自动取消的11种实现方式(下),架构篇,java
当KeyExpirationEventMessageListener收到Redis发布的过期Key的消息的时候,会发布RedisKeyExpiredEvent事件
订单自动取消的11种实现方式(下),架构篇,java
所以我们只需要监听RedisKeyExpiredEvent事件就可以拿到过期消息的Key,也就是延迟消息。

对RedisKeyExpiredEvent事件的监听实现MyRedisKeyExpiredEventListener

@Component
public class MyRedisKeyExpiredEventListener implements ApplicationListener<RedisKeyExpiredEvent> {

    @Override
    public void onApplicationEvent(RedisKeyExpiredEvent event) {
        byte[] body = event.getSource();
        System.out.println("获取到延迟消息:" + new String(body));
    }

}

代码写好,启动应用

之后我直接通过Redis命令设置消息,就没通过代码发送消息了,消息的key为sanyou,值为task,值不重要,过期时间为5s

set sanyou task 
expire sanyou 5

成功获取到延迟任务
订单自动取消的11种实现方式(下),架构篇,java
虽然这种方式可以实现延迟任务,但是这种方式坑 比较多

任务存在延迟

Redis过期事件的发布不是指key到了过期时间就发布,而是key到了过期时间被清除之后才会发布事件。

而Redis过期key的两种清除策略,就是面试八股文常背的两种:
惰性清除。当这个key过期之后,访问时,这个Key才会被清除
定时清除。后台会定期检查一部分key,如果有key过期了,就会被清除

所以即使key到了过期时间,Redis也不一定会发送key过期事件,这就到导致虽然延迟任务到了延迟时间也可能获取不到延迟任务。

丢消息太频繁

Redis实现的发布订阅模式,消息是没有持久化机制,当消息发布到某个channel之后,如果没有客户端订阅这个channel,那么这个消息就丢了,并不会像MQ一样进行持久化,等有消费者订阅的时候再给消费者消费。

所以说,假设服务重启期间,某个生产者或者是Redis本身发布了一条消息到某个channel,由于服务重启,没有监听这个channel,那么这个消息自然就丢了。

消息消费只有广播模式

Redis的发布订阅模式消息消费只有广播模式一种。

所谓的广播模式就是多个消费者订阅同一个channel,那么每个消费者都能消费到发布到这个channel的所有消息。
订单自动取消的11种实现方式(下),架构篇,java
如图,生产者发布了一条消息,内容为sanyou,那么两个消费者都可以同时收到sanyou这条消息。

所以,如果通过监听channel来获取延迟任务,那么一旦服务实例有多个的话,还得保证消息不能重复处理,额外地增加了代码开发量。

接收到所有key的某个事件

这个不属于Redis发布订阅模式的问题,而是Redis本身事件通知的问题。

当监听了__keyevent@__:expired的channel,那么所有的Redis的key只要发生了过期事件都会被通知给消费者,不管这个key是不是消费者想接收到的。

所以如果你只想消费某一类消息的key,那么还得自行加一些标记,比如消息的key加个前缀,消费的时候判断一下带前缀的key就是需要消费的任务。

二、Redisson的RDelayedQueue

Redisson他是Redis的儿子(Redis son),基于Redis实现了非常多的功能,其中最常使用的就是Redis分布式锁的实现,但是除了实现Redis分布式锁之外,它还实现了延迟队列的功能。

1、先来个demo

引入pom

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.1</version>
</dependency>

封装了一个RedissonDelayQueue类

@Component
@Slf4j
public class RedissonDelayQueue {

    private RedissonClient redissonClient;

    private RDelayedQueue<String> delayQueue;
    private RBlockingQueue<String> blockingQueue;

    @PostConstruct
    public void init() {
        initDelayQueue();
        startDelayQueueConsumer();
    }

    private void initDelayQueue() {
        Config config = new Config();
        SingleServerConfig serverConfig = config.useSingleServer();
        serverConfig.setAddress("redis://localhost:6379");
        redissonClient = Redisson.create(config);

        blockingQueue = redissonClient.getBlockingQueue("SANYOU");
        delayQueue = redissonClient.getDelayedQueue(blockingQueue);
    }

    private void startDelayQueueConsumer() {
        new Thread(() -> {
            while (true) {
                try {
                    String task = blockingQueue.take();
                    log.info("接收到延迟任务:{}", task);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "SANYOU-Consumer").start();
    }

    public void offerTask(String task, long seconds) {
        log.info("添加延迟任务:{} 延迟时间:{}s", task, seconds);
        delayQueue.offer(task, seconds, TimeUnit.SECONDS);
    }

}

这个类在创建的时候会去初始化延迟队列,创建一个RedissonClient对象,之后通过RedissonClient对象获取到RDelayedQueue和RBlockingQueue对象,传入的队列名字叫SANYOU,这个名字无所谓。

当延迟队列创建之后,会开启一个延迟任务的消费线程,这个线程会一直从RBlockingQueue中通过take方法阻塞获取延迟任务。

添加任务的时候是通过RDelayedQueue的offer方法添加的。

controller类,通过接口添加任务,延迟时间为5s

@RestController
public class RedissonDelayQueueController {

    @Resource
    private RedissonDelayQueue redissonDelayQueue;

    @GetMapping("/add")
    public void addTask(@RequestParam("task") String task) {
        redissonDelayQueue.offerTask(task, 5);
    }

}

启动项目,在浏览器输入如下连接,添加任务:http://localhost:8080/add?task=sanyou
静静等待5s,成功获取到任务。
订单自动取消的11种实现方式(下),架构篇,java

2、实现原理

如下是Redisson延迟队列的实现原理
订单自动取消的11种实现方式(下),架构篇,java
SANYOU前面的前缀都是固定的,Redisson创建的时候会拼上前缀。

  • redisson_delay_queue_timeout:SANYOU,sorted set数据类型,存放所有延迟任务,按照延迟任务的到期时间戳(提交任务时的时间戳 + 延迟时间)来排序的,所以列表的最前面的第一个元素就是整个延迟队列中最早要被执行的任务,这个概念很重要
  • redisson_delay_queue:SANYOU,list数据类型,也是存放所有的任务,但是研究下来发现好像没什么用。。
  • SANYOU,list数据类型,被称为目标队列,这个里面存放的任务都是已经到了延迟时间的,可以被消费者获取的任务,所以上面demo中的RBlockingQueue的take方法是从这个目标队列中获取到任务的
  • redisson_delay_queue_channel:SANYOU,是一个channel,用来通知客户端开启一个延迟任务

任务提交的时候,Redisson会将任务放到redisson_delay_queue_timeout:SANYOU中,分数就是提交任务的时间戳+延迟时间,就是延迟任务的到期时间戳

Redisson客户端内部通过监听redisson_delay_queue_channel:SANYOU这个channel来提交一个延迟任务,这个延迟任务能够保证将redisson_delay_queue_timeout:SANYOU中到了延迟时间的任务从redisson_delay_queue_timeout:SANYOU中移除,存到SANYOU这个目标队列中。

于是消费者就可以从SANYOU这个目标队列获取到延迟任务了。

所以从这可以看出,Redisson的延迟任务的实现跟前面说的MQ的实现都是殊途同归,最开始任务放到中间的一个地方,叫做redisson_delay_queue_timeout:SANYOU,然后会开启一个类似于定时任务的一个东西,去判断这个中间地方的消息是否到了延迟时间,到了再放到最终的目标的队列供消费者消费。

Redisson的这种实现方式比监听Redis过期key的实现方式更加可靠,因为消息都存在list和sorted set数据类型中,所以消息很少丢。

三、Netty的HashedWheelTimer

1、先来个demo

@Slf4j
public class NettyHashedWheelTimerDemo {

    public static void main(String[] args) {
        HashedWheelTimer timer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 8);
        timer.start();

        log.info("提交延迟任务");
        timer.newTimeout(timeout -> log.info("执行延迟任务"), 5, TimeUnit.SECONDS);
    }

}

测试结果
订单自动取消的11种实现方式(下),架构篇,java

2、实现原理

订单自动取消的11种实现方式(下),架构篇,java
如图,时间轮会被分成很多格子(上述demo中的8就代表了8个格子),一个格子代表一段时间(上述demo中的100就代表一个格子是100ms),所以上述demo中,每800ms会走一圈。

当任务提交的之后,会根据任务的到期时间进行hash取模,计算出这个任务的执行时间所在具体的格子,然后添加到这个格子中,通过如果这个格子有多个任务,会用链表来保存。所以这个任务的添加有点像HashMap储存元素的原理。

HashedWheelTimer内部会开启一个线程,轮询每个格子,找到到了延迟时间的任务,然后执行。

由于HashedWheelTimer也是单线程来处理任务,所以跟Timer一样,长时间运行的任务会导致其他任务的延时处理。

前面Redisson中提到的客户端延迟任务就是基于Netty的HashedWheelTimer实现的。

四、Hutool的SystemTimer

Hutool工具类也提供了延迟任务的实现SystemTimer

1、先来个Demo

@Slf4j
public class SystemTimerDemo {

    public static void main(String[] args) {
        SystemTimer systemTimer = new SystemTimer();
        systemTimer.start();

        log.info("提交延迟任务");
        systemTimer.addTask(new TimerTask(() -> log.info("执行延迟任务"), 5000));
    }

}

执行结果
订单自动取消的11种实现方式(下),架构篇,java
Hutool底层其实也用到了时间轮。

五、Qurtaz

Qurtaz是一款开源作业调度框架,基于Qurtaz提供的api也可以实现延迟任务的功能。

1、来个Demo

依赖

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

SanYouJob实现Job接口,当任务到达执行时间的时候会调用execute的实现,从context可以获取到任务的内容

@Slf4j
public class SanYouJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail jobDetail = context.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        log.info("获取到延迟任务:{}", jobDataMap.get("delayTask"));
    }
}

测试类

public class QuartzDemo {

    public static void main(String[] args) throws SchedulerException, InterruptedException {
        // 1.创建Scheduler的工厂
        SchedulerFactory sf = new StdSchedulerFactory();
        // 2.从工厂中获取调度器实例
        Scheduler scheduler = sf.getScheduler();

        // 6.启动 调度器
        scheduler.start();

        // 3.创建JobDetail,Job类型就是上面说的SanYouJob
        JobDetail jb = JobBuilder.newJob(SanYouJob.class)
                .usingJobData("delayTask", "这是一个延迟任务")
                .build();

        // 4.创建Trigger
        Trigger t = TriggerBuilder.newTrigger()
                //任务的触发时间就是延迟任务到的延迟时间
                .startAt(DateUtil.offsetSecond(new Date(), 5))
                .build();

        // 5.注册任务和定时器
        log.info("提交延迟任务");
        scheduler.scheduleJob(jb, t);
    }
}

执行结果:
订单自动取消的11种实现方式(下),架构篇,java

2、实现原理

核心组件

  • Job:表示一个任务,execute方法的实现是对任务的执行逻辑
  • JobDetail:任务的详情,可以设置任务需要的参数等信息
  • Trigger:触发器,是用来触发业务的执行,比如说指定5s后触发任务,那么任务就会在5s后触发
  • Scheduler:调度器,内部可以注册多个任务和对应任务的触发器,之后会调度任务的执行
    订单自动取消的11种实现方式(下),架构篇,java
    启动的时候会开启一个QuartzSchedulerThread调度线程,这个线程会去判断任务是否到了执行时间,到的话就将任务交给任务线程池去执行。

六、无限轮询延迟任务

无限轮询的意思就是开启一个线程不停的去轮询任务,当这些任务到达了延迟时间,那么就执行任务。

1、来个demo

@Slf4j
public class PollingTaskDemo {

    private static final List<DelayTask> DELAY_TASK_LIST = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                try {
                    for (DelayTask delayTask : DELAY_TASK_LIST) {
                        if (delayTask.triggerTime <= System.currentTimeMillis()) {
                            log.info("处理延迟任务:{}", delayTask.taskContent);
                            DELAY_TASK_LIST.remove(delayTask);
                        }
                    }
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (Exception e) {
                }
            }
        }).start();

        log.info("提交延迟任务");
        DELAY_TASK_LIST.add(new DelayTask("芋道源码", 5L));
    }

    @Getter
    @Setter
    public static class DelayTask {

        private final String taskContent;

        private final Long triggerTime;

        public DelayTask(String taskContent, Long delayTime) {
            this.taskContent = taskContent;
            this.triggerTime = System.currentTimeMillis() + delayTime * 1000;
        }
    }

}

任务可以存在数据库又或者是内存,看具体的需求,这里我为了简单就放在内存里了。

执行结果:
订单自动取消的11种实现方式(下),架构篇,java
这种操作简单,但是就是效率低下,每次都得遍历所有的任务。

七、最后

最后,本文所有示例代码地址:文章来源地址https://www.toymoban.com/news/detail-535180.html

https://github.com/sanyou3/delay-task-demo.git

到了这里,关于订单自动取消的11种实现方式(下)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringBoot+RabbitMQ实现超时未支付订单自动取消,localhost:15672没有登录页面。

    简介 安装RabbitMQ需要安装Erlang/OTP,并保持版本匹配。 RabbitMQ官网:RabbitMQ: One broker to queue them all | RabbitMQ RabbitMQ与Erlang/OTP版本对照表:Erlang Version Requirements | RabbitMQ Erlang官网下载:Downloads - Erlang/OTP 1.Windows上安装RabbitMQ前需要安装Erlang。(下载安装不做叙述,除了需要自定义安

    2024年04月15日
    浏览(55)
  • 商城系统中30分钟未付款自动取消订单怎么实现(简单几种方法)

    实现以上功能 写一个定时任务,每隔 30分钟执行一次,列出所有超出时间范围得订单id的列表 批量执行取消订单操作 实现一个简单的队列,每隔一定时间执行队列。 这里使用队列的优势可以跟前端时间匹配上,前端读秒几秒这里就什么时候取消  使用mq队列,消费消息。如

    2024年01月24日
    浏览(36)
  • Spring Boot中实现订单30分钟自动取消的策略

    🎉欢迎来到架构设计专栏~Spring Boot中实现订单30分钟自动取消的策略 ☆* o(≧▽≦)o *☆嗨~我是IT·陈寒🍹 ✨博客主页:IT·陈寒的博客 🎈该系列文章专栏:架构设计 📜其他专栏:Java学习路线 Java面试技巧 Java实战项目 AIGC人工智能 数据结构学习 🍹文章作者技术和水平有限,

    2024年02月08日
    浏览(58)
  • 支付超时取消订单实现方案 - 定时任务、延迟队列、消息队列等

    在实际业务场景中,我们经常会碰到类似一下场景: 淘宝等购物平台在订单支付时,如果30分钟内未支付自动取消。 腾讯会议预约会议后,在会议开始前15分钟提醒。 未使用的优惠券有效期结束后,自动将优惠券状态更新为已过期。 等等。。。 像这种支付超时取消的场景需

    2024年04月22日
    浏览(42)
  • JAVA生成唯一订单编号方案(两种方式)

    参考博客: ​​​​​​fhttp://t.csdn.cn/lCQ47​​​​​​​

    2024年02月07日
    浏览(39)
  • Web3 solidity编写cancelorder取消订单函数 并梳理讲述逻辑

    上文 Web3 solidity订单池操作 中 我们讲述了订单池的基本概念 并手动编写了创建订单的操作 最近的 我们还是先将 ganache 环境起起来 然后 我们打开项目 上文中 我们写了makeOrder创建订单的函数 但是 也带出一个问题 我们创建之后 如果不要了 怎么干掉呀? js中我们有 splice 但

    2024年02月09日
    浏览(47)
  • 纯java的方式实现自定义自动化部署java项目

    关于自动化部署java项目的方案有很多,就比如说比较知名的使用 Jenkins 实现自动化部署,还有比如说使用 IDEA 中的插件 Alibaba Cloud Toolkit 实现自动化部署;其他的方式我也没太去了解,我现在要做的是使用java自定义部署项目 关于 Jenkins 我学习也使用了一会, Alibaba Cloud Tool

    2023年04月11日
    浏览(29)
  • windows10/11系统实现本地账户密码登录微软账户 微软账户取消pin登录

    Windows 10支持两种账户登录模式,一种是使用了多年的通过本地用户账户来登录系统,另一种则是使用Microsoft账户来登录系统。使用Microsoft账户登录Windows系统是从Windows 8开始支持的登录模式,这种登录模式会自动连接到微软,然后对账户信息与系统设置进行自动同步。 简单来

    2024年02月13日
    浏览(54)
  • Ubuntu18.04 取消开机密码 实现自动登录

    因为要把Ubuntu设备作为服务器,实现开机自动运行服务程序,所以需要取消开机密码 实现自动登录。 1、点击桌面右上角向下的箭头,点击设置图标 2、点击右上角的 Unlock 3、在弹出的窗口中输入系统登录密码,点击右下角 Authenticate  4、把自动登录设置为ON,关闭窗口,再开

    2024年02月13日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包