血的教训-踩了定时线程池的坑

news2024/9/20 13:46:42

ScheduledExecutorService

一、背景

大家好呀,上周我们公司由于定时线程池使用不当出了一个故障,几千万的单子可能没了

给兄弟们分享分享这个坑,希望兄弟们以后别踩!

业务中大量的使用定时线程池(ScheduledExecutorService)执行任务,有时候会忽略掉 Try/Catch 的异常判断

当任务执行报错时,会导致整个定时线程池挂掉,影响业务的正常需求

二、问题

我们来模仿一个生产的例子:

  • 合作方修改频率低且合作方允许最终一致性

  • 我们有一个定时任务每隔 60 秒去 MySQL 拉取全量的 合作方 数据放至 合作方缓存(本地缓存) 中

  • 当客户请求时,我们去缓存中拿取合作方即可

这样的生产例子应该存在于绝大数公司,代码如下:

public class Demo {

    // 创建定时线程池
    private static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

    private List<String> partnerCache = new ArrayList<>();

    @PostConstruct
    public void init() {
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                // 启动时每隔60秒执行一次数据库的刷新
                // 将数据缓存至本地
                loadPartner();
            }
        }, 3, 60, TimeUnit.SECONDS);
    }

    public void loadPartner() {
        // 查询数据库当前最新合作方数据
        List<String> partnerList = queryPartners();

        // 合作方数据放至缓存
        partnerCache.clear();
        partnerCache.addAll(partnerList);
    }


    public List<String> queryPartners() {
        // 数据库挂了!
        throw new RuntimeException();
    }

}

 

运行上述样例,我们会发现程序不停止,输出一遍 Load start!,一直在运行,但后续不输出 Load start!

这个时候我们可以确认:异常确实导致当前任务不再执行

1、为什么任务报错会影响定时线程池?

2、定时线程池是真的挂掉了嘛?

3、定时线程池内部是如何执行的?

跟着这三个问题,我们一起来看一看 ScheduledExecutorService 的原理介绍

三、原理剖析

对于 ScheduledExecutorService 来说,本质上是 延时队列 + 线程池

1、延时队列介绍

DelayQueue 是一个无界的 BlockingQueue,用于放置实现了Delayed接口的对象,只能在到期时才能从队列中取走。

这种队列是有序的,即队头对象的延迟到期时间最长。

我们看一下延时队列里对象的属性:

 
class MyDelayedTask implements Delayed{
    // 当前任务创建时间
    private long start = System.currentTimeMillis();
    // 延时时间
    private long time ;

    // 初始化
    public MyDelayedTask(long time) {
        this.time = time;
    }

    /**
     * 需要实现的接口,获得延迟时间(用过期时间-当前时间)
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert((start+time) - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
    }

    /**
     * 用于延迟队列内部比较排序(当前时间的延迟时间 - 比较对象的延迟时间)
     */
    @Override
    public int compareTo(Delayed o) {
        MyDelayedTask o1 = (MyDelayedTask) o;
        return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }
}

 

所以,延时队列的实现原理也很简单:

  • 生产端:投递消息时增加时间戳(当前时间+延时时间
  • 消费端:用当前时间与时间戳进行比较,若小于则消费,反之则循环等待

2、线程池的原理介绍

  • 当前的线程池个数低于核心线程数,直接添加核心线程即可
  • 当前的线程池个数大于核心线程数,将任务添加至阻塞队列中
  • 如果添加阻塞队列失败,则需要添加非核心线程数处理任务
  • 如果添加非核心线程数失败(满了),执行拒绝策略

3、定时线程的原理

我们从定时线程池的创建看:scheduledExecutorService.scheduleAtFixedRate(myTask, 3L, 1L, TimeUnit.SECONDS);

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
    // 初始化我们的任务
    // triggerTime:延时的实现
    ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command,null,triggerTime(initialDelay, unit),unit.toNanos(period));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}
private void delayedExecute(RunnableScheduledFuture<?> task) {
    // 将当前任务丢进延时队列
    super.getQueue().add(task);
    // 创建核心线程并启动 
    ensurePrestart();
}

// 时间轮算法
private long triggerTime(long delay, TimeUnit unit) {
    return now() + delay;
}

 

从这里我们可以得到结论:定时线程池通过延时队列来达到定时的目的

有一个问题:我们仅仅向 Queue 里面放了一个任务,他是怎么保证执行多次的呢?

带着这个问题,我们看一下他拉取任务启动的代码:

for (;;) {
    // 从延时队列中获取任务
    Runnable r = workQueue.take();
}
public RunnableScheduledFuture<?> take(){
    for (;;) {
        // 获取队列第一个任务
        RunnableScheduledFuture<?> first = queue[0];
        
        // 【重点】如果当前队列任务为空,则等待
        if (first == null){
            available.await();
        }
                        
        // 获取当前任务的时间
        long delay = first.getDelay(NANOSECONDS);
        
        if (delay <= 0){
            // 弹出当前任务
            return finishPoll(first);
        }
        
                            
    }
}
// 时间戳减去当前时间
public long getDelay(TimeUnit unit) {
    return unit.convert(time - now(), NANOSECONDS);
}

当拿到任务(ScheduledFutureTask)之后,会执行任务:task.run()

public void run() {
   // 执行当前的任务
   if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

protected boolean runAndReset() {
    if (state != NEW){
        return false;
    }
    int s = state;
    try {
        Callable<V> c = callable;
        if (c != null && s == NEW) {
            try {
                // 执行任务
                c.call(); 
                // 【重点!!!】如果任务正常执行成功的话,这里会将ran置为true
                // 如果你的任务有问题,会被下面直接捕捉到,不会将此处的ran置为true
                ran = true;
            } catch (Throwable ex) {
                // 出现异常会将state置为EXCEPTIONAL
                // 标记当前任务执行失败并将异常赋值到结果
                setException(ex);
            }finally {
                 s = state;
            }
        }
    }
    // ran:当前任务是否执行成功
    // s:当前任务状态
    // ran为false:当前任务执行失败
    // s == NEW = false:当前任务状态出现异常
    return ran && s == NEW;
}

如果我们的 runAndReset 返回 false 的话,那么进不去 setNextRunTime 该方法:

if (ScheduledFutureTask.super.runAndReset()) {
    // 修改当前任务的Time
    setNextRunTime();
    // 将任务重新丢进队列
    reExecutePeriodic(outerTask);
}

最终,任务没有办法被丢进队列,我们的线程无法拿到任务执行,一直在等待。

四、结论

通过上面的分析,我们回头看一下开篇的三个问题:

1、为什么任务报错会影响定时线程池?

  • 任务报错不会影响线程池,只是线程池将当前任务给丢失,没有继续放到队列中

2、定时线程池是真的挂掉了嘛?

  • 定时线程池没有挂,挂的只是报错的任务

3、定时线程池内部是如何执行的?

  • 线程池 + 延时队列

所以,通过上述的讲解,我们应该认识到:定时任务一定要加Try Catch,不然一旦发生异常

不然,你就会和作者一样,背故障让公司损失几千万,血的教训!

在阿里巴巴的开发文档中也曾说过:

前人总结的经验真的是血的教训,一定要听劝【哭】。 

需要阿里巴巴开发文档书籍版的小伙伴点击获取先关资料

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1153695.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSS3盒模型

CSS3盒模型规定了网页元素的显示方式&#xff0c;包括大小、边框、边界和补白等概念。2015年4月&#xff0c;W3C的CSS工作组发布了CSS3基本用户接口模块&#xff0c;该模块负责控制与用户接口界面相关效果的呈现方式。 1、盒模型基础 在网页设计中&#xff0c;经常会听到内容…

软件测试面试,一定要提前准备好的面试题

收集了2023年所有粉丝的面试题后&#xff0c;负责整理出了7个高频出现的面试题&#xff0c;一起来看看。 问题1&#xff1a;请自我介绍下&#xff1f; 核心要素&#xff1a;个人技能优势工作背景经验亮点 参考回答&#xff1a; 第一种&#xff1a;基本信息离职理由 面试官您好&…

吐血整理,Jmeter接口测试-项目案例场景,直接上高速...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 场景一&#xff1…

人工智能-softmax回归

回归可以用于预测多少的问题。 比如预测房屋被售出价格&#xff0c;或者棒球队可能获得的胜场数&#xff0c;又或者患者住院的天数。 事实上&#xff0c;我们也对分类问题感兴趣&#xff1a;不是问“多少”&#xff0c;而是问“哪一个”&#xff1a; 某个电子邮件是否属于垃圾…

groovy下载与安装

Groovy是一种基于JVM&#xff08;Java虚拟机&#xff09;的敏捷开发语言&#xff0c;它结合了Python、Ruby和Smalltalk的许多强大的特性&#xff0c;Groovy 代码能够与 Java 代码很好地结合&#xff0c;也能用于扩展现有代码。由于其运行在 JVM 上的特性&#xff0c;Groovy也可…

ElasticSearch深度解析入门篇:高效搜索解决方案的介绍与实战案例讲解,带你避坑

ElasticSearch深度解析入门篇&#xff1a;高效搜索解决方案的介绍与实战案例讲解&#xff0c;带你避坑 1.Elasticsearch 产生背景 大规模数据如何检索 如&#xff1a;当系统数据量上了 10 亿、100 亿条的时候&#xff0c;我们在做系统架构的时候通常会从以下角度去考虑问题&a…

【大数据基础平台】星环TDH社区开发版单机部署

&#x1f341; 博主 "开着拖拉机回家"带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——&#x1f390;开着拖拉机回家_大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#…

产品解读 | GreatADM如何高效实现数据库资源池化部署?

前段时间&#xff0c;介绍了万里数据库的GreatADM数据库管理平台如何图形化部署MGR&#xff08;详见文章&#xff1a;探索GreatADM&#xff1a;如何快速定义监控&#xff09;&#xff0c;有朋友想了解GreatADM是否支持资源池化管理、租户隔离、监控等功能。 今天&#xff0c;我…

MaxQuant的安装和使用(linux OR windows)

目录 1. 安装2. 用法2.1 命令行方式&#xff08;linux&#xff09;2.2 GUI方式&#xff08;windows&#xff09;1) completeAndromeda配置/Configuration&#xff08;2&#xff09;数据上传/Raw files (14)组特异参数/Group-specific parameters&#xff08;17&#xff09;全局…

如何选择安全又可靠的文件数据同步软件?

数据实时同步价值体现在它能够确保数据在多个设备或系统之间实时更新和保持一致。这种技术可以应用于许多领域&#xff0c;如电子商务、社交媒体、金融服务等。在这些领域中&#xff0c;数据实时同步可以带来很多好处&#xff0c;如提高工作效率、减少数据不一致、提高用户体验…

后台管理(一)

1、管理员登录 1.1、创建Md5加密工具类&#xff1a; public static String md5(String source) {//判断source是否生效if (source null || source.length() 0) {//不是有效的数据throw new RuntimeException(CrowdConstant.MESSAGE_STRING_INVALIDATE);}String algorithm &…

带你学习毫秒级的故障检测技术BFD

【微|信|公|众|号&#xff1a;厦门微思网络】 【微思网络http://www.xmws.cn&#xff0c;成立于2002年&#xff0c;专业培训21年&#xff0c;思科、华为、红帽、ORACLE、VMware等厂商认证及考试&#xff0c;以及其他认证PMP、CISP、ITIL等】 什么是BFD&#xff1f; BFD&#x…

【uniapp+vue3】scroll-view实现纵向自动滚动及swiper实现纵向自动滚动

scroll-view本身不支持自动滚动&#xff0c;通过scroll-top属性控制滚动&#xff0c;但是不可以循环滚动 <scroll-view class"notice-bar" scroll-y"true" ref"scrollViewRef" :scroll-top"data.scrollViewTop"scroll-with-animati…

Power BI 傻瓜入门 18. 让您的数据熠熠生辉

本章内容包括&#xff1a; 配置Power BI以使数据增量刷新发现使用Power BI Desktop and Services保护数据集的方法在不影响性能和完整性的情况下管理海量数据集 如果有更新的、更相关的数据可用&#xff0c;旧数据对组织没有好处。而且&#xff0c;老实说&#xff0c;如果数据…

一文搞懂“支付·清结算·账务”全局

《上帝视角看支付&#xff0c;总架构解析》 对支付的宏观层面做了分析&#xff0c;详解了整个支付体系每一层的架构和业务模型&#xff0c;而每一层的企业内部支付体系建设是什么样的&#xff1f;会涉及到哪些环节和系统&#xff1f;每个系统会涉及到哪些单据和逻辑&#xff0c…

工业级环网交换机的功效和用途

十年前&#xff0c;工业级环网交换机是一个被忽视的领域&#xff0c;在自动化中只占据了很小的一部分&#xff0c;并没有引起太多厂商的重视。随着自动化技术的不断成熟&#xff0c;工业以太网的广泛采用以及大型工业控制网络的建设&#xff0c;自动化厂商也不能忽视丰富产品线…

中文版goole浏览器支持小于12px的文字

1、说明&#xff1a; 中文版goole浏览器默认不支持小于12px的文字&#xff0c;英文版支持。 2、可浏览器设置&#xff1a; goole浏览器前往 chrome://settings/fonts&#xff0c;更改浏览器设置。 3、可代码设置 -webkit-transform:scale() 说明&#xff1a;transform:sca…

数字组合-第10届蓝桥杯省赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第1讲。 数字组合&#xff…

01-学成在线项目基础环境模块搭建

基础工程搭建 项目根目录 创建一个空工程xuecheng-plus即项目的根目录,进入Project Structure检查jdk是否配置正确 新增.gitignore文件用来设置工程中不需要向仓库提交的内容 HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/** !**/src/test/** #### STS …

天津重点大数据培训 大数据培训的三个重要内容

随着互联网的发展和技术的进步&#xff0c;大数据的应用范围越来越广泛&#xff0c;对于企业和个人来说&#xff0c;学习和掌握大数据技术已经成为了必不可少的一项能力。大数据技术是当前和未来的发展方向&#xff0c;对于想进入互联网行业或从事相关技术工作的人来说&#xf…