普通异步、守护定时和关键定时 线程池

10次阅读
没有评论

 

这段配置通过区分普通异步守护定时关键定时三种模式,很好地覆盖了后端开发中的典型并发场景。

以下是针对这三个 Bean 的具体使用场景示例:


1. taskExecutor (通用异步线程池)

场景特点:高并发、执行快、对实时性有一定要求,允许在系统关闭时有短暂的等待。

  • 发送通知:用户下单后,异步发送短信、邮件或推送通知。

  • 日志记录/审计:将非核心的操作日志异步写入数据库或 Elasticsearch。

  • 报表导出:用户点击“下载报表”,后台开始异步生成 Excel,完成后通过 WebSocket 或站内信通知。

  • 图片/文件处理:上传图片后的缩略图生成或水印添加。

代码示例:

Java

@Async("taskExecutor")
public void sendVerificationCode(String mobile){
    // 模拟耗时网络请求
    smsService.send(mobile, "Your code is 1234");
}

2. daemonScheduler (守护线程定时任务池)

场景特点:后台辅助任务。所谓“守护线程”,意味着如果 JVM 中只剩下这些线程在运行,JVM 会直接退出。适合那些“丢了也不心疼”或“下次启动能自动恢复”的任务。

  • 缓存预热/刷新:每隔 5 分钟从数据库更新一次 Redis 里的热门配置。

  • 系统监控指标收集:每分钟采集当前内存、CPU 使用率并上报。

  • 心跳检测:维护与外部 WebSocket 或中间件的活跃状态。

  • 清理临时文件:每天凌晨清理服务器上的临时上传目录。

代码示例:

Java

@Resource(name = "daemonScheduler")
private ScheduledExecutorService daemonScheduler;

public void startHeartbeat(){
    daemonScheduler.scheduleAtFixedRate(() -> {
        log.info("Sending heartbeat to monitoring server...");
    }, 0, 30, TimeUnit.SECONDS);
}

3. criticalScheduler (非守护线程定时任务池)

场景特点:核心业务逻辑。非守护线程会阻止 JVM 正常退出(直到任务完成或超时),配合 AbortPolicy 拒绝策略,确保任务的严肃性。

  • 金融对账/结算:每日凌晨进行的交易对账,必须确保完整运行,不能因为系统重启而被轻易中断。

  • 订单超时关闭:扫描数据库中超过 30 分钟未支付的订单并进行库存回退。

  • 自动平仓/触发止损:在交易系统中,根据价格触发预设的买卖指令(这种任务绝对不能无故消失)。

  • 多阶段状态轮询:例如调用第三方支付接口后,持续轮询最终支付结果。

代码示例:

Java

@Resource(name = "criticalScheduler")
private ScheduledExecutorService criticalScheduler;

public void scheduleOrderSettlement(){
    criticalScheduler.execute(() -> {
        log.info("Starting critical financial settlement...");
        // 执行核心财务逻辑
    });
}

总结建议

线程池 线程类型 拒绝策略 适合任务 优先级
taskExecutor 普通线程 CallerRuns (调用者运行) 业务异步化、高吞吐
daemonScheduler 守护线程 CallerRuns (调用者运行) 监控、缓存、辅助维护
criticalScheduler 非守护线程 Abort (抛出异常) 金融、结算、状态一致性

下一步建议:

由于 criticalScheduler 使用了 AbortPolicy,在高负载下会直接抛出 RejectedExecutionException。您是否需要我为您演示如何结合 Spring Retry 或自定义监听器来捕获并处理这些拒绝异常?

 

package com.coin.api.schedule;

import com.coin.business.service.MarketDataService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
* 交易数据1分钟清理任务
*/
@Component
@Slf4j
public class MarketDataCleanJob {
@Resource
MarketDataService marketDataService;

/**
* 交易数据清理任务,每8小时执行一次
*/
@Async("daemonScheduler")
@Scheduled(fixedRate = 8 * 60 * 60 * 1000)
public void execute() {
log.info("执行任务--TradeData1mCleanJob。。。。。。。。。 begin");
marketDataService.clean90Data();
marketDataService.cleanOldData();
log.info("执行任务--TradeData1mCleanJob。。。。。。。。。 end");
}
} 假设这种执行,如果上次没有执行完,或者就是线程数不够了,或者各种场景会怎么样

 

 

在 Spring 的 @Scheduled 配合自定义线程池的架构下,这种执行方式会根据具体的资源状况和任务耗时产生不同的行为。

我们需要先纠正一个代码层面的误区:@Async 注解默认是拦截方法调用的,但在同一个类中由 @Scheduled 直接触发时,如果配置不当,它的行为可能并不会如你预想的那样直接进入 daemonScheduler 假设配置已正确生效(即任务确实交由 ScheduledThreadPoolExecutor 异步执行),以下是针对不同异常场景的深度解析:


1. 上次任务没执行完,下次任务时间到了

这是最常见的场景。由于你使用的是 fixedRate = 8小时

  • 如果任务是单线程串行的:Spring 的 ScheduledThreadPoolExecutor 默认行为是阻塞。即使到了 8 小时,如果上一次 clean90Data() 还没跑完,下一次任务会进入队列等待,直到上一次结束后立即开始。

  • 由于你加了 @Async

    • 每次触发时,@Scheduled 只是负责“发起”调用。

    • @Async 会把任务封装成一个 Runnable 丢进 daemonScheduler 的队列。

    • 结果:会出现重叠执行。如果 8 小时到了,旧任务还在清理大批量数据,新任务会同时启动。这可能导致数据库死锁(两个任务同时删同一张表)或数据库 IO 瞬间飙升。

2. 线程数不够了(线程池满了)

在你的配置中,daemonScheduler 的核心线程数是 5,拒绝策略是 CallerRunsPolicy

  • 场景:假设有 5 个清理任务都在跑,且队列也塞满了。

  • 后果:第 6 个任务触发时,由于触发者是 Spring 内部的定时调度线程(通常是 scheduling-1),CallerRunsPolicy 会让调度线程自己去执行这个清理任务。

  • 连锁反应:调度线程被占用了执行耗时的数据库清理,会导致整个应用中所有的 @Scheduled 任务全部停摆,直到这个清理任务完成。

3. 任务执行过程中抛出异常

你在配置中重写了 afterExecute 并调用了 ThreadUtils.printException

  • 表现:如果 clean90Data() 抛出 RuntimeException(如数据库连接超时),线程不会死掉,它会被线程池回收。

  • 日志:你会看到 ThreadUtils 打印的错误堆栈,但任务会终止

  • 下次执行:8 小时后,定时器依然会尝试触发下一次执行,它不会因为上次失败就停止后续调度。


4. 关键场景对比表

场景 表现 潜在风险
执行耗时 > 8小时 新老任务并行运行 数据库压力倍增、逻辑重复、死锁
数据库压力过大 大量线程阻塞在 SQL 执行上 耗尽 daemonScheduler 线程池
JVM 突然重启/关闭 由于是 daemon(true) 任务会被立即强杀,可能导致数据清理只做了一半(事务未提交)
线程池队列满 触发 CallerRuns 全局调度阻塞,其他定时任务失效

优化建议

为了让这类清理任务更健壮,建议做以下改进:

  1. 防止重叠执行

    如果你不希望两个清理任务同时跑,去掉 @Async,或者在方法内部加一个“分布式锁”(如 Redis 锁)或简单的 AtomicBoolean 状态位。

    Java

    private final AtomicBoolean isRunning = new AtomicBoolean(false);
    
    public void execute() {
        if (!isRunning.compareAndSet(false, true)) {
            log.warn("上次任务还在跑,跳过本次执行");
            return;
        }
        try {
            // ... 清理逻辑
        } finally {
            isRunning.set(false);
        }
    }
    
  2. 改用 fixedDelay

    fixedRate 改为 fixedDelay

    • fixedRate: 8 小时准点触发(不管上次完没完)。

    • fixedDelay: 上次跑完之后,再等 8 小时触发。这在清理大数据量的场景下更安全。

  3. 考虑事务与分片

    清理 90 天前的数据建议分批删除(例如每批删除 1000 条,循环进行),避免长事务锁定数据库表太久,也能减少线程长时间占用的风险。

你想让我帮你写一个带自旋锁和分批处理的健壮清理逻辑模板吗?

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-03-13发表,共计4010字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)

摆渡资源站

文章搜索
一言一句话
-「
热门文章
某个货币持仓增长了一倍,但是 jing流入没有增加多少,为啥

某个货币持仓增长了一倍,但是 jing流入没有增加多少,为啥

  这通常是因为该货币的市值(价格)上涨抵消了持仓量的增加,或者存在某些“非交易性”的变动。 简单来...
2026比特币稳赚指南:顶级加密交易策略全揭秘!

2026比特币稳赚指南:顶级加密交易策略全揭秘!

    关键要点 定投(DCA):仍是比特币长期积累的低风险和纪律性策略。 趋势交易和波段...
使用Java类库ta4j计算基金的布林轨

使用Java类库ta4j计算基金的布林轨

ta4j简介 对于做金融分析的从业者而言,python的ta-lib是不可或缺的技术分析库,具有简单易用、功能...
全真早晚功课简介

全真早晚功课简介

         道教的斋醮仪式很多,主要的日常宗教活动是早晚功课经。凡是道教徒每天都要上殿唪诵,所...
吕祖朝科

吕祖朝科

  举步朝金阙    飞身谒玉京  天外琳琅响    齐举步虚声   步虚  宝座临金殿    霞光...
最新评论
333985 333985 每天都在战争,希望2026和平.
最新文章
普通异步、守护定时和关键定时 线程池

普通异步、守护定时和关键定时 线程池

  这段配置通过区分普通异步、守护定时和关键定时三种模式,很好地覆盖了后端开发中的典型并发场景。 以...
同步和非阻塞关系

同步和非阻塞关系

背景 同步(Synchronous):意味着“内核拷贝数据到用户空间”这个动作,必须由用户线程自己来完成。 非...
nio当数据来了,是由哪个线程读取的

nio当数据来了,是由哪个线程读取的

背景   多路复用:线程调用 selector.select()。这个方法会询问内核:我管辖的这一千...
nio的线程是什么?

nio的线程是什么?

背景 IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到...
女生的生理期和非生理期的对于男生的态度差别好大,连性兴趣都变了

女生的生理期和非生理期的对于男生的态度差别好大,连性兴趣都变了

  这是一个非常普遍且具有生物学依据的观察。女生的这种“判若两人”,其实背后有一套非常精密的激素驱动...