当前位置:首页 > 科技  > 软件

线程池异常黑洞及其防范策略

来源: 责编: 时间:2024-01-08 09:13:51 297观看
导读1. 问题&分析迭代刚上线,小艾同学又接到了一线业务的投诉,一起看下本次遇到的又是什么问题。1.1. 案例上周接到一个需求,需要对系统中的核心操作增加操作日志,也就是在操作完成后对操作人、操作时间等信息进行详细记录。

1. 问题&分析

迭代刚上线,小艾同学又接到了一线业务的投诉,一起看下本次遇到的又是什么问题。Bpi28资讯网——每日最新资讯28at.com

1.1. 案例

上周接到一个需求,需要对系统中的核心操作增加操作日志,也就是在操作完成后对操作人、操作时间等信息进行详细记录。核心包括:创建订单、取消订单、删除订单、修改价格等。Bpi28资讯网——每日最新资讯28at.com

在需求分析时,小艾做了深度思考:记录操作日志不能影响正常的业务操作,比如创建订单,哪怕是操作日志记录失败,也不能导致下单失败。Bpi28资讯网——每日最新资讯28at.com

当然,方案1便是,记录日志的逻辑使用 try-catch 进行处理,哪怕是抛出异常也不能影响原来的主流程。Bpi28资讯网——每日最新资讯28at.com

图片imageBpi28资讯网——每日最新资讯28at.com

这样做确实能控制住异常,但由于是在主线程中运行,这样会导致整个流程的处理时间加长。这时,小艾想起了线程池的异常操作,整体如下:Bpi28资讯网——每日最新资讯28at.com

图片imageBpi28资讯网——每日最新资讯28at.com

这个方案好处多多:Bpi28资讯网——每日最新资讯28at.com

  1. 可以非常好对异常进行隔离,任务执行发生异常也不会影响主流程
  2. 操作日志在线程池中异步处理,不会占用主线程的时间,整体操作时间变化不大

核心代码如下:Bpi28资讯网——每日最新资讯28at.com

@GetMapping("createOrder")public RestResult<String> createOrder(Integer taskId){    log.info("begin to create Order {}", taskId);    // 创建订单    doCreateOrder(taskId);    log.info("end to create Order {}", taskId);    // 异步保存操作日志    log.info("Begin to Submit Task {}", taskId);    this.executorService.execute(new SaveOperationLogTask(taskId));    log.info("Success to Submit Task {}", taskId);    return RestResult.success("提交成功");}// 保存操作日志 Task@Slf4jpublic class SaveOperationLogTask implements Runnable {    // 省略部分代码    @Override    public void run() {        log.info("Begin to save operation");        // 保存日志        saveLog();        log.info("Success to Run task {}", this.taskId);    }    private void saveLog() {        // 实际执行业务逻辑,保存到数据库    }}

在收到业务反馈时,小艾第一时间查看日志,居然没有找到任何异常信息。难道是业务反馈信息有问题?根据业务提供的订单号,在数据库中确实没有找到操作记录,好奇怪呀。Bpi28资讯网——每日最新资讯28at.com

从日志中提取出 正常 和 异常 信息,分别如下:Bpi28资讯网——每日最新资讯28at.com

正常日志:Bpi28资讯网——每日最新资讯28at.com

图片imageBpi28资讯网——每日最新资讯28at.com

可以看出:Bpi28资讯网——每日最新资讯28at.com

  1. 在主线程中完成创建订单和提交任务操作
  2. 在线程池线程中完成操作日志保存操作
  3. 在数据库中能看到详细的操作信息

异常日志:Bpi28资讯网——每日最新资讯28at.com

图片imageBpi28资讯网——每日最新资讯28at.com

可以看出少了些信息:Bpi28资讯网——每日最新资讯28at.com

  1. 主线程操作没有变化,完成了创建订单和提交任务操作
  2. 线程池线程只打印了开始执行的日志,未见到执行成功日志
  3. 数据中也没有操作信息

对比日志可见,==保存操作日志的任务执行失败,同时系统没有抛出任何异常!!!==Bpi28资讯网——每日最新资讯28at.com

1.2. 问题分析

核心还是对 线程池的核心 API 不熟悉,当使用 `execute()` 方法提交任务时,异常信息不会直接抛出给调用者。这是因为线程池处理任务的方式是,将这些任务封装到一个 `Runnable` 中去执行。`Runnable.run()` 方法没有任何抛出异常的声明,所以在运行 `Runnable` 时产生的异常只会被内部捕获,不会抛出。Bpi28资讯网——每日最新资讯28at.com

线程池中提供两者函数:Bpi28资讯网——每日最新资讯28at.com

  1. `execute()` 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池成功执行。
  2. `submit()` 方法用于提交需要返回值的任务。线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功。

2. 解决方案

最大困扰原因是:出问题后没有任何信息。所以对应的解决方案便是:让系统能够打印异常栈暴露异常原因。Bpi28资讯网——每日最新资讯28at.com

2.1. 手工抛出异常

最简单方式便是,在Task代码中通 try-catch 手工捕获并打印异常日志。Bpi28资讯网——每日最新资讯28at.com

详细代码如下:Bpi28资讯网——每日最新资讯28at.com

@Slf4jpublic class SaveOperationLogTask1 implements Runnable {    // 省略非核心代码    @Override    public void run() {        try {            int result = RandomUtils.nextInt() / this.taskId;            log.info("Success to Run task {}", this.taskId);        }catch (Exception e) {            log.error("failed to run task {}", taskId, e);        }    }}

当出现异常数据时,日志如下:Bpi28资讯网——每日最新资讯28at.com

图片imageBpi28资讯网——每日最新资讯28at.com

可以看出,从 SaveOperationLogTask1 类中清楚的打印异常信息。Bpi28资讯网——每日最新资讯28at.com

2.2. 封装 Runnable 统一管理异常

每个 Task 都手工添加 try-catch 逻辑,不仅工作量大也非常容易出现遗漏场景,我们需要一个更好的方案。Bpi28资讯网——每日最新资讯28at.com

可以构建一个 Runnable 的封装类来对异常进行统一处理,详细代码如下:Bpi28资讯网——每日最新资讯28at.com

@Slf4jpublic class LogBasedTaskWrapper implements Runnable {    private final Runnable runnable;    public LogBasedTaskWrapper(Runnable runnable) {        this.runnable = runnable;    }    @Override    public void run() {        try {            this.runnable.run();        }catch (Exception e) {            log.error("Filed to run task {}", runnable, e);        }    }}// 在提交任务时,使用 LogBasedTaskWrapper 对 Task 进行封装即可log.info("Begin to Submit Task {}", taskId);Runnable task = new SaveOperationLogTask(taskId);this.executorService.execute(new LogBasedTaskWrapper(task));log.info("Success to Submit Task {}", taskId);

当出现异常数据时,日志如下:Bpi28资讯网——每日最新资讯28at.com

图片imageBpi28资讯网——每日最新资讯28at.com

可以看出,从 LogBasedTaskWrapper 类中清楚的打印异常信息。Bpi28资讯网——每日最新资讯28at.com

2.3. 定制化线程工厂

Wrapper 机制不错,但需要对 Task 进行封装操作,还是容易被遗漏,我们还需要更简单的方式。Bpi28资讯网——每日最新资讯28at.com

可以对线程池的线程工厂进行定制,对为捕获异常进行特殊处理,详细代码如下:Bpi28资讯网——每日最新资讯28at.com

executorServiceV2 = new ThreadPoolExecutor(4, 4,        0L, TimeUnit.MILLISECONDS,        new LinkedBlockingQueue<Runnable>(20),        new BasicThreadFactory.Builder()                .namingPattern("BlackHole_thread-%d")                // 设置为捕获异常处理器                .uncaughtExceptionHandler((t, e) -> log.error("Failed to run task", e))                .build(),        new ThreadPoolExecutor.AbortPolicy());// 然后使用 executorServiceV2 即可// 异步保存操作日志log.info("Begin to Submit Task {}", taskId);this.executorServiceV2.execute(new SaveOperationLogTask(taskId));log.info("Success to Submit Task {}", taskId);

核心代码就一句:.uncaughtExceptionHandler((t, e) -> log.error("Failed to run task", e))。当出现未捕获异常时,会统一被 UncaughtExceptionHandler 处理。详细日志如下:Bpi28资讯网——每日最新资讯28at.com

图片imageBpi28资讯网——每日最新资讯28at.com

从日志中看到,从 ExceptionBlackHoleFixController 类中对异常进行处理。Bpi28资讯网——每日最新资讯28at.com

这是一劳永逸的方法,也是最鼓励的方法。Bpi28资讯网——每日最新资讯28at.com

2.4. 使用 CompletableFuture

当使用 submit 提交任务时,会返回 Futrue 对象,通过 Future 的 get 方法便可以获取任务运行的异常信息,但这样会阻塞主线程导致接口响应时间过长。Bpi28资讯网——每日最新资讯28at.com

这种情况下,可以使用更高级的 CompletableFuture,向 CompletableFuture 设置异常处理器后,出现异常时会自动调用处理器,核心代码如下:Bpi28资讯网——每日最新资讯28at.com

// 异步保存操作日志log.info("Begin to Submit Task {}", taskId);CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(new SaveOperationLogTask(taskId), this.executorService);completableFuture.exceptionally(e -> {        log.error("Failed to Submit Task", e);        return null;    });log.info("Success to Submit Task {}", taskId);

当出现异常数据时,日志如下:Bpi28资讯网——每日最新资讯28at.com

图片imageBpi28资讯网——每日最新资讯28at.com

从日志中看到,从 ExceptionBlackHoleFixController 类中对异常进行处理。Bpi28资讯网——每日最新资讯28at.com

3. 示例&源码

代码仓库:https://gitee.com/litao851025/learnFromBugBpi28资讯网——每日最新资讯28at.com

代码地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/exceptionblackholeBpi28资讯网——每日最新资讯28at.com


Bpi28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-57841-0.html线程池异常黑洞及其防范策略

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 一加 Ace 3 Pop-up 快闪活动来袭,二十城掀起抢购热潮

下一篇: 如何使用Kotlin开发DSL?

标签:
  • 热门焦点
  • 小米官宣:2023年上半年出货量中国第一!

    今日早间,小米电视官方微博带来消息,称2023年小米电视上半年出货量达到了中国第一,同时还表示小米电视的巨屏风暴即将开始。“公布一个好消息2023年#小米电视上半年出货量中国
  • Redmi Pad评测:红米充满野心的一次尝试

    从Note系列到K系列,从蓝牙耳机到笔记本电脑,红米不知不觉之间也已经形成了自己颇有竞争力的产品体系,在中端和次旗舰市场上甚至要比小米新机的表现来得更好,正所谓“大丈夫生居
  • 如何正确使用:Has和:Nth-Last-Child

    我们可以用CSS检查,以了解一组元素的数量是否小于或等于一个数字。例如,一个拥有三个或更多子项的grid。你可能会想,为什么需要这样做呢?在某些情况下,一个组件或一个布局可能会
  • 谷歌KDD'23工作:如何提升推荐系统Ranking模型训练稳定性

    谷歌在KDD 2023发表了一篇工作,探索了推荐系统ranking模型的训练稳定性问题,分析了造成训练稳定性存在问题的潜在原因,以及现有的一些提升模型稳定性方法的不足,并提出了一种新
  • 一篇文章带你了解 CSS 属性选择器

    属性选择器对带有指定属性的 HTML 元素设置样式。可以为拥有指定属性的 HTML 元素设置样式,而不仅限于 class 和 id 属性。一、了解属性选择器CSS属性选择器提供了一种简单而
  • 2纳米决战2025

    集微网报道 从三强争霸到四雄逐鹿,2nm的厮杀声已然隐约传来。无论是老牌劲旅台积电、三星,还是誓言重回先进制程领先地位的英特尔,甚至初成立不久的新
  • OPPO Reno10 Pro英雄联盟定制礼盒公布:萨勒芬妮同款配色梦幻十足

    5月24日,OPPO推出了全新的OPPO Reno 10系列,包含OPPO Reno10、OPPO Reno10 Pro和OPPO Reno10 Pro+三款新机,全系标配了超光影长焦镜头,是迄今为止拍照
  • 苹果140W USB-C充电器:采用氮化镓技术

    据10 月 30 日 9to5 Mac 消息报道,当苹果推出新的 MacBook Pro 2021 时,该公司还推出了新的 140W USB-C 充电器,附赠在 MacBook Pro 16 英寸机型的盒子里,也支
  • onebot M24巧系列一体机采用轻薄机身设计,现已在各平台开售

    onebot M24 巧系列一体机目前已在线上线下各平台同步开售。onebot M24 巧系列采用一体化轻薄机身设计,最薄处为 10.15mm,拥有宝石红、午夜蓝、石墨绿、雅致
Top