延迟消息的软肋,竟被定时任务完美弥补

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

这几天我们遇到一个问题,我们需要做一个自动化的拍卖系统。车辆加入场次之后,拍卖时间开始自动拍卖,直到所有的拍品拍卖结束。

因为车辆加入的场次可以是当天的,也可以是未来某一天的,场次的拍卖开始时间使我们要解决的重中之重。关于这个问题,我们做了技术讨论,主要放在定时任务和延迟消息上,因为延迟消息和定时任务都是用来处理"未来某个时间点需要执行的操作"的技术方案。

有人想用定时任务做;有人想用延迟消息;也有人想用定时任务+延迟消息的。到底应该怎么抉择呢?

我们一起来看一下定时任务和延迟消息的利害关系!

02 延迟消息

延迟消息主要被动触发,等待一个时间点你被唤起触发。实现方案有很多,可以基于JDK的延迟队列、也可以使用第三方的中间件。

2.1 延迟队列

基于JDK的延迟队列是最简单的实现方式,无需任何第三方依赖。只需要实现java.util.concurrent.Delayed接口即可。

java 复制代码
public class DelayTask implements Delayed {

    // 延迟任务名称
    private String name;
    // 实际延迟时间的时间戳
    private long delayTimestamp;

    public DelayTask(int time, TimeUnit unit, String name) {
        this.delayTimestamp = Instant.now().toEpochMilli() +  (time > 0 ? unit.toMillis(time) : 0);
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.delayTimestamp - Instant.now().toEpochMilli(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        DelayTask o1 = (DelayTask) o;
        long diff = this.delayTimestamp - o1.getDelay(TimeUnit.MILLISECONDS);
        if (diff == 0) {
            return 0;
        }
        return  diff > 0 ? 1 : -1;
    }
}

getDelay()主要是为了获取延迟的时间,然后通过compareTo()实现比较。

2.2 Redis的Sorted Set

Redis中的一个数据结构是Sorted Set,它是一个有序集合,利用score 值排序。我们可以用时间戳作为score,数据结构会自动帮我们排序,我们只要依次轮询取出值,如果当前值的score小于等于当前时间,就取出即可。

sh 复制代码
# Redis 命令
zadd delay:task 1750078434263 task1 1750078435800 task2

结合业务代码:

java 复制代码
 while (true) {
    DelayTask task =  redis.zrange("delay:task" , 0 , 0)
    if (task.getTime().compare(Instant.now().toEpochMilli()) <= 0) {
        // 处理业务
    }    
 }

2.3 MQ消息

消息中间件在企业中应用尤为广泛,同时支持延迟消息,使用起来非常简单。我们这里以Active MQ为例。消息的搭建这里不再赘述。

java 复制代码
this.jmsTemplate.send(this.destination, new MessageCreator() {
    @Override
    public Message createMessage(Session session) throws JMSException {
        MapMessage mapMessage = session.createMapMessage();
        mapMessage.setInt("taskId", taskId);
        // 延迟消息的属性
        mapMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delayTime);
        LOGGER.info("延时消息发送:{}", JSONObject.toJSONString(mapMessage));
        return mapMessage;
    }
});

MQ的延迟消息应用相比其他的延迟消息应用更广,因为消息一般都会搭建高可用,消息的持久化又避免了消息的丢失。

传统的延迟消息,尤其JDK自带的延迟队列,对于多节点、分布式系统处理起来就会麻烦一些,需要考虑不同节点处理热任务以及任务丢失的问题,而Redis的虽然有持久化,但是应用又需要一个守护线程不停地轮询处理任务。个人认为MQ延迟消息的方式更加方便一下。

03 定时任务

定时任务主要以轮询调度为主,调度中心按计划触发任务执行。主要处理一些周期性、固定频率的任务。

3.1 Timer定时器

定时器只需要继承java.util.TimerTask即可。

java 复制代码
public class DelayTImerTask extends TimerTask {

    private String name;

    public DelayTImerTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + this.name + " 任务执行了");
    }
}

实现只需要调用schedule()即可完成。

这里的调用比较像延迟消息。但是timer可以实现周期的任务:

这种实现方式非常方便,是JDK自身提供的,定时任务是基于内存的。

3.2 基于@Scheduled注解

Springboot已经集成了定时任务的注解,通过简单的配置就可以实现。

java 复制代码
@Configuration
@EnableScheduling
public class ScheduleTask {

    @Scheduled(cron = "0/3 * * * * ?")
    private void configureTasks() {
        System.out.println("执行静态定时任务时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

执行结果:

基于项目的定时任务,需要考虑多节点的幂等性问题。

3.3 分布式调度任务

分布式是始终绕不开的话题,分布式调度任务可以解决多节点的问题,常用的框架xxl-job

xxl-job依赖

xml 复制代码
<!-- xxl-job-core -->
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.3.0</version>
</dependency>
java 复制代码
@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appName;

    @Value("${xxl.job.executor.ip}")
    private String ip;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;


    @Bean(initMethod = "start", destroyMethod = "destroy")
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.warn(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppName(appName);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        System.out.println(logRetentionDays);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
        logger.warn(">>>>>>>>>>> [adminAddresses]{}|[appName]{}|[ip]{}|[port]{}|[accessToken]{}|[logPath]{}|[logRetentionDays]{}",
        		adminAddresses,appName,ip,port,accessToken,logPath,logRetentionDays);

        return xxlJobSpringExecutor;
    }
}

任务编写

java 复制代码
@JobHandler(value="dealyHandler")
@Component
public class DealyHandler  extends IJobHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(DealyHandler.class);

    @Override
    public ReturnT<String> execute(String param) throws Exception {
   		// 业务代码
        return SUCCESS;
    }
}

剩下的只需要在xxl-job管理界面配置即可:

04 区别与场景

延迟消息处理的时间比较灵活,可以自定义时间,适用于差别性较大的数据,可以逐一订制处理;而定时任务更适合处理时间固定、频率固定的批量的数据。

延迟消息基于事件的延迟处理如订单超时、支付交易异常的结果处理、优惠券的提醒过期等等;还可以削峰填谷,平滑流量,将非紧急的处理请求(如非实时的日志分析、图片处理)放入延迟队列,稍后处理,避免瞬时高峰压垮系统;工作流中的等待步骤,如在一个多步骤的工作流中,某个步骤完成后需要等待一段时间(如等待人工审核、等待外部系统响应超时)才能进行下一步,可以用延迟消息实现等待;还可以用在分布式事务的最终一致性补偿,如在发起一个可能失败的操作后,发送一条延迟检查状态的消息。如果延迟时间后检查到状态不一致,触发补偿操作。

定时任务适用于数据的清理、报表的生成、缓存的刷新、状态机的维护、数据同步等,以及特定的活动的开关,在合同到期日当天零点发送合同续签提醒邮件(如果系统能精确知道所有合同的到期日并提前配置好任务)

05 小结

正所谓尺有所短寸有所长,延迟消息和定时任务也有自己的不足的地方。延迟消息的时间过长会导致浪资源消耗特别严重,例如阿里开源的RocketMQ就不支持自定义的延迟消息,只支持18个等级的延迟消息,最长的也就2的小时。而定时任务频率太高也会产生资源的浪费。

个人觉得,当一个延迟消息超过24小时,就是不可靠的。而超过24小时的延迟消息,就可以使用定时任务来节省资源。如每天凌晨00:00以后,定时拉取当前需要的处理的延迟任务,然后投递到延迟消息里面。这时候延迟消息最长也不会超过24小时。这样即便消息丢失,也可以通过定时任务手动的方式重新完成消息投递。

大家觉得呢?

相关推荐
盛夏绽放12 分钟前
Python 目录操作详解
java·服务器·python
贰拾wan12 分钟前
ArrayList源码分析
java·数据结构
Code季风14 分钟前
跨语言RPC:使用Java客户端调用Go服务端的JSON-RPC服务
java·网络协议·rpc·golang·json
林太白22 分钟前
也许看了Electron你会理解Tauri,扩宽你的技术栈
前端·后端·electron
松果集28 分钟前
【Python3】练习一
后端
anganing29 分钟前
Web 浏览器预览 Excel 及打印
前端·后端
肯定慧32 分钟前
B1-基于大模型的智能办公应用软件
后端
豆沙沙包?38 分钟前
2025年- H82-Lc190--322.零钱兑换(动态规划)--Java版
java·算法·动态规划
TinyKing41 分钟前
一、getByRole 的作用
后端