为什么大家都在抵制用定时任务实现「关闭超时订单」功能?

2023-11-02

e355cd44a9ee7073c8a5ef63b0ffd75e.gif

作者 | 阿Q

来源 | 阿Q说代码

前几天领导突然宣布几年前停用的电商项目又重新启动了,让我把代码重构下进行升级。

让我最深恶痛觉的就是里边竟然用定时任务实现了“关闭超时订单”的功能,现在想来,哭笑不得。我们先分析一波为什么大家都在抵制用定时任务来实现该功能。

定时任务

关闭超时订单是在创建订单之后的一段时间内未完成支付而关闭订单的操作,该功能一般要求每笔订单的超时时间是一致的。

如果我们使用定时任务来进行该操作,很难把握定时任务轮询的时间间隔:

  • 时间间隔足够小,在误差允许的范围内可以达到我们说的时间一致性问题,但是频繁扫描数据库,执行定时任务,会造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击;

  • 时间间隔比较大,由于每个订单创建的时间不一致,所以上边的一致性要求很难达到,举例如下:

add46df543a672fef403cb49a6438409.png

假设30分钟订单超时自动关闭,定时任务的执行间隔时间为30分钟:

  1. 我们在第5分钟进行下单操作;

  2. 当时间来到第30分钟时,定时任务执行一次,但是我们的订单未满足条件,不执行;

  3. 当时间来到第35分钟时,订单达到关闭条件,但是定时任务未执行,所以不执行;

  4. 当时间来到第60分钟时,开始执行我们的订单关闭操作,而此时,误差达到25分钟。

经此种种,我们需要舍弃该方式。

延时队列

为了满足领导的需求,我便将手伸向了消息队列:RabbitMQ。尽管它本身并没有提供延时队列的功能,但是我们可以利用它的存活时间和死信交换机的特性来间接实现。

首先我们先来简单介绍下什么是存活时间?什么是死信交换机?

存活时间

存活时间的全拼是Time To Live,简称 TTL。它既支持对消息本身进行设置(延迟队列的关键),又支持对队列进行设置(该队列中所有消息存在相同的过期时间)。

  • 对消息本身进行设置:即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判定的;

  • 对队列进行设置:一旦消息过期,就会从队列中抹去;

如果同时使用这两种方法,那么以过期时间的那个数值为准。当消息达到过期时间还没有被消费,那么该消息就“死了”,我们把它称为 死信 消息。

消息变为死信的条件:

  • 消息被拒绝(basic.reject/basic.nack),并且requeue=false;

  • 消息的过期时间到期了;

  • 队列达到最大长度;

队列设置注意事项

  1. 队列中该属性的设置要在第一次声明队列的时候设置才有效,如果队列一开始已存在且没有这个属性,则要删掉队列再重新声明才可以;

  2. 队列的 ttl 只能被设置为某个固定的值,一旦设置后则不能更改,否则会抛出异常;

死信交换机

死信交换机全拼Dead-Letter-Exchange,简称DLX

当消息在一个队列中变成死信之后,如果这个消息所在的队列设置了x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换机上,这个交换机就称之为死信交换机,与这个死信交换器绑定的队列就是死信队列。

  • x-dead-letter-exchange:出现死信之后将死信重新发送到指定交换机;

  • x-dead-letter-routing-key:出现死信之后将死信重新按照指定的routing-key发送,如果不设置默认使用消息本身的routing-key

死信队列与普通队列的区别就是它的RoutingKeyExchange需要作为参数,绑定到正常的队列上。

实战教学

先来张图感受下我们的整体思路

f724baa2819b6b441b9dfaf827394a91.png

  1. 生产者发送带有 ttl 的消息放入交换机路由到延时队列中;

  2. 在延时队列中绑定死信交换机与死信转发的routing-key

  3. 等延时队列中的消息达到延时时间之后变成死信转发到死信交换机并路由到死信队列中;

  4. 最后供消费者消费。

代码实现:

配置类
@Configuration
public class DelayQueueRabbitConfig {

    public static final String DLX_QUEUE = "queue.dlx";//死信队列
    public static final String DLX_EXCHANGE = "exchange.dlx";//死信交换机
    public static final String DLX_ROUTING_KEY = "routingkey.dlx";//死信队列与死信交换机绑定的routing-key

    public static final String ORDER_QUEUE = "queue.order";//订单的延时队列
    public static final String ORDER_EXCHANGE = "exchange.order";//订单交换机
    public static final String ORDER_ROUTING_KEY = "routingkey.order";//延时队列与订单交换机绑定的routing-key

 /**
     * 定义死信队列
     **/
    @Bean
    public Queue dlxQueue(){
        return new Queue(DLX_QUEUE,true);
    }

    /**
     * 定义死信交换机
     **/
    @Bean
    public DirectExchange dlxExchange(){
        return new DirectExchange(DLX_EXCHANGE, true, false);
    }


    /**
     * 死信队列和死信交换机绑定
     * 设置路由键:routingkey.dlx
     **/
    @Bean
    Binding bindingDLX(){
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(DLX_ROUTING_KEY);
    }


    /**
     * 订单延时队列
     * 设置队列里的死信转发到的DLX名称
     * 设置死信在转发时携带的 routing-key 名称
     **/
    @Bean
    public Queue orderQueue() {
        Map<String, Object> params = new HashMap<>();
        params.put("x-dead-letter-exchange", DLX_EXCHANGE);
        params.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
        return new Queue(ORDER_QUEUE, true, false, false, params);
    }

    /**
     * 订单交换机
     **/
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange(ORDER_EXCHANGE, true, false);
    }

    /**
     * 把订单队列和订单交换机绑定在一起
     **/
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(ORDER_ROUTING_KEY);
    }
}
发送消息
@RequestMapping("/order")
public class OrderSendMessageController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMessage")
    public String sendMessage(){

        String delayTime = "10000";
        //将消息携带路由键值
        rabbitTemplate.convertAndSend(DelayQueueRabbitConfig.ORDER_EXCHANGE, DelayQueueRabbitConfig.ORDER_ROUTING_KEY,
                "发送消息!",message->{
            message.getMessageProperties().setExpiration(delayTime);
            return message;
        });
        return "ok";
    }

}
消费消息
@Component
@RabbitListener(queues = DelayQueueRabbitConfig.DLX_QUEUE)//监听队列名称
public class OrderMQReciever {

    @RabbitHandler
    public void process(String message){
        System.out.println("OrderMQReciever接收到的消息是:"+ message);
    }
}
测试

通过调用接口,发现10秒之后才会消费消息

3ff197d048f3f415b53ab5bc2c5d2aab.png

问题升级

由于开发环境和测试环境使用的是同一个交换机和队列,所以发送的延时时间都是30分钟。但是为了在测试环境让测试同学方便测试,故手动将测试环境的时间改为了1分钟。

问题复现

接着问题就来了:延时时间为1分钟的消息并没有立即被消费,而是等30分钟的消息被消费完之后才被消费了。至于原因,我们下边再分析,先用代码来给大家复现下该问题。

@GetMapping("/sendManyMessage")
public String sendManyMessage(){
    send("延迟消息睡10秒",10000+"");
    send("延迟消息睡2秒",2000+"");
    send("延迟消息睡5秒",5000+"");
    return "ok";
}

private void send(String msg, String delayTime){
 rabbitTemplate.convertAndSend(DelayQueueRabbitConfig.ORDER_EXCHANGE, 
                                  DelayQueueRabbitConfig.ORDER_ROUTING_KEY,
                                  msg,message->{
                                      message.getMessageProperties().setExpiration(delayTime);
                                      return message;
                                  });
}

执行结果如下:

OrderMQReciever接收到的消息是:延迟消息睡10秒
OrderMQReciever接收到的消息是:延迟消息睡2秒
OrderMQReciever接收到的消息是:延迟消息睡5秒

原因就是延时队列也满足队列先进先出的特征,当10秒的消息未出队列时,后边的消息不能顺利出队,造成后边的消息阻塞了,未能达到精准延时。

问题解决

我们可以利用x-delay-message插件来解决该问题

消息的延迟范围是 Delay > 0, Delay =< ?ERL_MAX_T(在 Erlang 中可以被设置的范围为 (2^32)-1 毫秒)

5978fcceb85863e16e289fa860f871b7.png

  1. 生产者发送消息到交换机时,并不会立即进入,而是先将消息持久化到 Mnesia(一个分布式数据库管理系统);

  2. 插件将会尝试确认消息是否过期;

  3. 如果消息过期,消息会通过 x-delayed-type 类型标记的交换机投递至目标队列,供消费者消费;

实践

我这边使用的是v3.8.0.ez,将文件下载下来放到服务器的/usr/local/soft/rabbitmq_server-3.7.14/plugins 路径下,执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange命令即可。

6574261345ee0914c6cc779659177819.pnge28a5c5f39e4b2f46c09315e59b79793.png

出现如图所示,代表安装成功。

配置类

@Configuration
public class XDelayedMessageConfig {

    public static final String DIRECT_QUEUE = "queue.direct";//队列
    public static final String DELAYED_EXCHANGE = "exchange.delayed";//延迟交换机
    public static final String ROUTING_KEY = "routingkey.bind";//绑定的routing-key

    /**
     * 定义队列
     **/
    @Bean
    public Queue directQueue(){
        return new Queue(DIRECT_QUEUE,true);
    }

    /**
     * 定义延迟交换机
     * args:根据该参数进行灵活路由,设置为“direct”,意味着该插件具有与直连交换机具有相同的路由行为,
     * 如果想要不同的路由行为,可以更换现有的交换类型如:“topic”
     * 交换机类型为 x-delayed-message
     **/
    @Bean
    public CustomExchange delayedExchange(){
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
    }

    /**
     * 队列和延迟交换机绑定
     **/
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(directQueue()).to(delayedExchange()).with(ROUTING_KEY).noargs();
    }

}

发送消息

@RestController
@RequestMapping("/delayed")
public class DelayedSendMessageController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendManyMessage")
    public String sendManyMessage(){

        send("延迟消息睡10秒",10000);
        send("延迟消息睡2秒",2000);
        send("延迟消息睡5秒",5000);
        return "ok";
    }

    private void send(String msg, Integer delayTime){
        //将消息携带路由键值
        rabbitTemplate.convertAndSend(
                XDelayedMessageConfig.DELAYED_EXCHANGE,
                XDelayedMessageConfig.ROUTING_KEY,
                msg,
                message->{
                    message.getMessageProperties().setDelay(delayTime);
                    return message;
                });
    }
}

消费消息

@Component
@RabbitListener(queues = XDelayedMessageConfig.DIRECT_QUEUE)//监听队列名称
public class DelayedMQReciever {


    @RabbitHandler
    public void process(String message){
        System.out.println("DelayedMQReciever接收到的消息是:"+ message);
    }
}

测试

DelayedMQReciever接收到的消息是:延迟消息睡2秒
DelayedMQReciever接收到的消息是:延迟消息睡5秒
DelayedMQReciever接收到的消息是:延迟消息睡10秒

这样我们的问题就顺利解决了。

局限性

延迟的消息存储在一个Mnesia表中,当前节点上只有一个磁盘副本,它们将在节点重启后存活。

虽然触发计划交付的计时器不会持久化,但它将在节点启动时的插件激活期间重新初始化。显然,集群中只有一个预定消息的副本意味着丢失该节点或禁用其上的插件将丢失驻留在该节点上的消息。

该插件的当前设计并不适合延迟消息数量较多的场景(如数万条或数百万条),另外该插件的一个可变性来源是依赖于 Erlang 计时器,在系统中使用了一定数量的长时间计时器之后,它们开始争用调度程序资源,并且时间漂移不断累积。

9e85b00feead7789c11095610a36b9b3.gif

往期推荐

从 40% 跌至 4%,“糊”了的 Firefox 还能重回巅峰吗?

Gartner 发布 2022 年汽车行业五大技术趋势

别再用 Redis List 实现消息队列了,Stream 专为队列而生

漫画:什么是“低代码”开发平台?

8f2a60e30dd1055f2b650baf486e03f0.gif

点分享

8974d72a0904cf4319aefc2d29133b02.gif

点收藏

b45a9b6451ed5d11f84ceda8a4e67179.gif

点点赞

7fdb3612a3be4a579c6ef3aa6c13b1c4.gif

点在看

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么大家都在抵制用定时任务实现「关闭超时订单」功能? 的相关文章

  • java.lang.NoClassDefFoundError: javax/ws/rs/core/Configuration

    我正在实现轻松的网络服务 并且正在使用 jboss 4 0 但我遇到以下异常 java lang NoClassDefFoundError javax ws rs core Configuration 我的 web xml 是
  • H2 - 多个应用程序访问同一个 H2 数据库

    我正在使用嵌入式数据库H2在 2 个网络应用程序中说WebApp1 and WebApp2 我运行 WebApp1 并执行一些查询来访问 H2 数据库 同时我运行 WebApp2 但它抛出异常H2 当前已被另一个进程使用 我的需求是 我应该
  • 哪个 new 首先执行——在构造函数中还是在构造函数外?

    如果我定义一个类如下 public class myClass private x new anotherClass private y public myClass y new anotherClass 哪个变量会更早获得实例 x 或 y
  • java.lang.unsatisfiedlinkerror 无法加载 amd 64 位 .dll ia 32 位

    当我尝试在 Eclipse 上运行我的项目时 出现以下错误 它在我开发它的计算机上运行良好 但当我将其导入我的笔记本电脑时 它不起作用 这个问题已经在本网站的其他地方提出过 这个问题的主要原因似乎是环境变量设置不正确 但我检查过 它们似乎是
  • 通过 JDBC 调用 Sybase 存储过程时结果集为空

    我正在调用一个通过 JDBC 返回多个结果集的 Sybase 存储过程 我需要获取一个特定的结果集 其中有一列名为 结果 这是我的代码 CallableStatement cs conn prepareCall sqlCall cs reg
  • 在 Java 和 C 中在运行时调用名为“string”的方法

    我们如何调用名称为的方法string在运行时 谁能告诉我如何在 Java 和 C 中做到这一点 在java中可以通过反射api来完成 看一下Class getMethod String methodName Class parameterT
  • Hibernate HQL 查询:如何将集合设置为查询的命名参数?

    给定以下 HQL 查询 FROM Foo WHERE Id id AND Bar IN barList I set id使用查询对象的setInteger 方法 我想设置 barList用一个List对象 但查看 Hibernate 文档和
  • Maven:缺少工件 org.springframework:spring:jar:4.2.6

    我在 SpringToolSuite 中有一个动态 Web 项目 它被转换为 Maven 项目 我遇到问题 缺少工件 org springframework spring jar 4 2 6 我已经尝试清理 重建和运行该项目 它给 读取文件
  • 如何更改tomcat jmx密码的文件权限

    我正在尝试保护 Windows 平台上托管的本地 tomcat 实例上的 JMX 访问 我已经创建了访问权限和密码文件 并使用以下 VM 参数插入这些文件 Dcom sun management jmxremote password fil
  • 如何在Redis中正确存储图片?

    决定将图像存储在Redis中 如何正确执行 现在我这样做 redis gt set image path here is the base64 image code 我不确定这是否正常 将图片存储在Redis中是完全可以的 Redis 键和
  • 在java中将jpeg/png转换为像素数组

    如何将包含 jpeg 或 png 的字符串转换为像素数组 最好是一维 理想情况下使用java内置的类 原来你需要公共文件上传 http commons apache org fileupload 看着那 这用户指南 http commons
  • 摆动刷新周期

    我试图了解何时使用重新验证 重绘 打包 令人惊讶的是 我没有找到详细的底层文档 请随意链接 到目前为止我已经明白这都是 RepaintManager 的责任 油漆 重新油漆指的是脏 干净的东西 pack validate revalidat
  • 获取运行时生成的类的字节

    我正在使用一个 Java 框架 该框架使用自定义类加载器在运行时生成一些 代理 类 我想为任何这样的类获取自定义 ClassLoader 从 loadClass 返回的与该类对应的原始字节数组 这可能吗 我知道 如果一个类作为资源存在 那么
  • Java字符串中的字符数[重复]

    这个问题在这里已经有答案了 可能的重复 Java 使用unicode上划线显示平方根时字符串的长度 https stackoverflow com questions 7704426 java length of string when u
  • Java 通用问题

    下面的代码可以编译 但如果我取消注释行 它不会编译 我很困惑为什么 HashMap 确实扩展了 AbstractMap 并且声明映射的第一行可以正常编译 import java util AbstractMap import java ut
  • Java可以进行进程监控吗?

    是否可以用Java编写一个在托盘中运行的应用程序 并且当启动某个应用程序时 它可以检测到它 我想对某些程序执行此操作 以了解我每周使用它们多长时间 我是 Java 新手 所以我不知道 Java 是否是最适合此操作的语言 或者它是否具有对操作
  • 在 Java Jersey 2 JAX-RS 中初始化单例

    我是泽西岛 2 22 2 的新手 请耐心等待 我正在创建一个与 LDAP 服务器交互的 REST 服务 用于存储 删除和检索用户数据 该服务通过执行加密 解密充当安全中介 在使用 REST 服务之前必须进行相当多的初始化 并且我只想执行此初
  • Jersey bean 验证 ParameterNameProvider

    我正在阅读关于泽西岛的文档Bean验证 https jersey java net documentation latest bean validation html The ParameterNameProvider示例显示如何定义方法的
  • 警告:无法加载 sqljdbc_auth.dll 原因:java.library.path 中没有 sqljdbc_auth

    我正在使用 Ubuntu 12 05 并尝试连接到 Windows Server 2012 来获取数据库 我的数据库名称是 jobs 电脑的IP地址是192 160 1 33 托管在1433 但是当我尝试连接时出现以下错误 WARNING
  • @JsonCreator '无法找到具有名称的创建者属性',即使使用ignoreUnknown = true

    我有以下课程 JsonIgnoreProperties ignoreUnknown true public class Topic private List

随机推荐

  • 【笔记】计算机网络-应用层

    文章目录 网络应用模型 概述 模型 域名解析系统DNS 域名 域名服务器 域名解析过程 文件传输协议FTP FTP工作原理 电子邮件 格式 组成结构 简单邮件传送协议SMTP MIME 邮局协议POP3 网际报文存取协议IMAP 小结 万维
  • 微信小程序获取接口返回数据

    import java io BufferedReader import java io InputStreamReader import java net URL import java net URLConnection import
  • 37- 输入和显示-文本浏览器QTextBrowser

    文本浏览器QTextBrowser 扩展了QTextEdit 只读模式 添加了一些导航功能 以便用户可以跟踪超文本文档中的链接 如果要为用户提供可编辑的富文本编辑器 请使用QTextEdit 如果想要一个没有超文本导航的文本浏览器 使用QT
  • 免费IP类api接口:含ip查询、ip应用场景查询、ip代理识别、IP行业查询...

    免费IP类api接口 含ip查询 ip应用场景查询 ip代理识别 IP归属地 IPv6区县级 根据IP地址 IPv6版本 查询归属地信息 包含国家 省 市 区县和运营商等信息 IP归属地 IPv6城市级 根据IP地址 IPv6版本 查询归属
  • android平台LCD驱动分析

    目前手机芯片厂家提供的源码里包含整个LCD驱动框架 一般厂家会定义一个xxx fb c的源文件 注册一个平台设备和平台驱动 在驱动的probe函数中来调用register framebuffer 从而生成 dev fbx的设备节点 这里最重
  • fabirc的get或者put抛出的paramiko.ssh_exception.SSHException: Channel closed.

    内容抛出错误如下 Traceback most recent call last File Library Python 2 7 site packages fabric main py line 743 in main args kwar
  • 【详解】位运算符--正数及负数的位运算

    位运算符的正负数计算 按位与 按位或 按位异或 按位非 左移 lt lt 右移 gt gt 以及涉及的码制相关知识 文章目录 一 码制 二 位运算符 1 二元位运算符的运算 按位与 按位或 按位异或 左移 lt lt 右移 gt gt 2
  • 35黑马QT笔记之QFile写文件

    35黑马QT笔记之QFile写文件 1 如何在文本编辑区写内容保存到一个本地文件呢 1 利用文件对话框函数getSaveFileName获取要创建的文件路径 实际上还没真正在电脑创建 只是意味着你要创建的路径 2 将要创建的文件路径与QFi
  • python判断字符为空_大神教你如何判断Python中字符串是否为空和null

    导读 这篇文章主要介绍了Python判断字符串是否为空和null 文中通过示例代码介绍的非常详细 对大家的学习或者工作具有一定的参考学习价值 需要的朋友可以参考下 判断python中的一个字符串是否为空 可以使用如下方法 1 使用字符串长度
  • msvcp120.dll丢失的解决方法?哪种方法更推荐

    msvcp120 dll是一个Windows操作系统的动态链接库文件 它属于Microsoft Visual C Redistributable软件包的一部分 这个文件包含了一些用于C 程序编译和运行的函数和类 当某个程序需要使用这些函数和
  • [错误解决]centos中使用kubeadm方式搭建多master的高可用K8S集群

    安装步骤 部署Kubernetes Master时的错误 部署Kubernetes Master时 创建了一个kubeadm config yaml文件 将相关配置信息放到这个地方 该文件如下 apiServer certSANs mast
  • Spring学习笔记:Bean的装配方式

    学习内容 Bean的装配方式 文章目录 学习内容 Bean的装配方式 1 装配Bean的概述 2 基于XML的装配 3 基于注解的装配 4 自动装配 5 使用注解实现自动装配 6 使用Java的方式配置Spring 1 装配Bean的概述
  • 【SLAM】——DynaSLAM项目环境配置(超多坑)

    DynaSLAM 坑多 慢慢来 不要急 先整体说一下 项目是在ORB SLAM2项目的基础上 加上maskrcnn的融合 主流程采用还是采用ORB SLAM2的流程 maskrcnn部分采用c 调用python的实现 其中又穿插opencv
  • 计算机网络的体系结构

    1 OSI 七层模型 提出者 ISO 国际标准化组织 一种网络分层的设计方法论 比较复杂且不实用 落地时几乎都是TCP IP五层模型 层数 功能 数据传输单元 7 应用层 面向用户 应用程序 6 表示层 处理在两个通信系统中交换信息的表示方
  • 医学图像处理综述

    本文作者 张伟 公众号 计算机视觉life 编辑成员 0 引言 医学图像处理的对象是各种不同成像机理的医学影像 临床广泛使用的医学成像种类主要有X 射线成像 X CT 核磁共振成像 MRI 核医学成像 NMI 和超声波成像 UI 四类 在目
  • 2020 蓝桥杯省赛 B 组模拟赛:寻找重复项

    include
  • 期货和股票平仓时成本计价的区别

    期货和股票平仓时成本计价的区别 期货交易采用的是当天无负债结算 在掌握持仓盈亏之前 你先要掌握一个概念 结算价 结算价是指对当天未平仓合约进行交易保证金结算和盈亏结算的基准价 它是把期货合约当天的各个成交价格按照成交量进行加权平均得来的 当
  • codeforces 1217b B - Zmei Gorynich

    题意 有头龙有m个头 有n种砍法 第i种 砍去ai个 再长bi个 某一时刻头为0胜利 要砍几刀 先找伤害最大 记为d 的刀和效率 maxv max ai bi 最高的刀 如果d
  • 利用Eclipse进行重构

    来源 http my opera com jojomclntosh blog 重构和单元测试是程序员的两大法宝 他们的作用就像空气和水对于人一样 平凡 不起眼 但是意义深重 预善事 必先利器 本文就介绍怎样在Eclipse中进行重构 本文介
  • 为什么大家都在抵制用定时任务实现「关闭超时订单」功能?

    作者 阿Q 来源 阿Q说代码 前几天领导突然宣布几年前停用的电商项目又重新启动了 让我把代码重构下进行升级 让我最深恶痛觉的就是里边竟然用定时任务实现了 关闭超时订单 的功能 现在想来 哭笑不得 我们先分析一波为什么大家都在抵制用定时任务来