迭代剛上線,小艾同學又接到了一線業務的投訴,一起看下本次遇到的又是什么問題。
上周接到一個需求,需要對系統中的核心操作增加操作日志,也就是在操作完成后對操作人、操作時間等信息進行詳細記錄。核心包括:創建訂單、取消訂單、刪除訂單、修改價格等。
在需求分析時,小艾做了深度思考:記錄操作日志不能影響正常的業務操作,比如創建訂單,哪怕是操作日志記錄失敗,也不能導致下單失敗。
當然,方案1便是,記錄日志的邏輯使用 try-catch 進行處理,哪怕是拋出異常也不能影響原來的主流程。
image
這樣做確實能控制住異常,但由于是在主線程中運行,這樣會導致整個流程的處理時間加長。這時,小艾想起了線程池的異常操作,整體如下:
image
這個方案好處多多:
核心代碼如下:
@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() { // 實際執行業務邏輯,保存到數據庫 }}
在收到業務反饋時,小艾第一時間查看日志,居然沒有找到任何異常信息。難道是業務反饋信息有問題?根據業務提供的訂單號,在數據庫中確實沒有找到操作記錄,好奇怪呀。
從日志中提取出 正常 和 異常 信息,分別如下:
正常日志:
image
可以看出:
異常日志:
image
可以看出少了些信息:
對比日志可見,==保存操作日志的任務執行失敗,同時系統沒有拋出任何異常!!!==
核心還是對 線程池的核心 API 不熟悉,當使用 `execute()` 方法提交任務時,異常信息不會直接拋出給調用者。這是因為線程池處理任務的方式是,將這些任務封裝到一個 `Runnable` 中去執行。`Runnable.run()` 方法沒有任何拋出異常的聲明,所以在運行 `Runnable` 時產生的異常只會被內部捕獲,不會拋出。
線程池中提供兩者函數:
最大困擾原因是:出問題后沒有任何信息。所以對應的解決方案便是:讓系統能夠打印異常棧暴露異常原因。
最簡單方式便是,在Task代碼中通 try-catch 手工捕獲并打印異常日志。
詳細代碼如下:
@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); } }}
當出現異常數據時,日志如下:
image
可以看出,從 SaveOperationLogTask1 類中清楚的打印異常信息。
每個 Task 都手工添加 try-catch 邏輯,不僅工作量大也非常容易出現遺漏場景,我們需要一個更好的方案。
可以構建一個 Runnable 的封裝類來對異常進行統一處理,詳細代碼如下:
@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);
當出現異常數據時,日志如下:
image
可以看出,從 LogBasedTaskWrapper 類中清楚的打印異常信息。
Wrapper 機制不錯,但需要對 Task 進行封裝操作,還是容易被遺漏,我們還需要更簡單的方式。
可以對線程池的線程工廠進行定制,對為捕獲異常進行特殊處理,詳細代碼如下:
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 處理。詳細日志如下:
image
從日志中看到,從 ExceptionBlackHoleFixController 類中對異常進行處理。
這是一勞永逸的方法,也是最鼓勵的方法。
當使用 submit 提交任務時,會返回 Futrue 對象,通過 Future 的 get 方法便可以獲取任務運行的異常信息,但這樣會阻塞主線程導致接口響應時間過長。
這種情況下,可以使用更高級的 CompletableFuture,向 CompletableFuture 設置異常處理器后,出現異常時會自動調用處理器,核心代碼如下:
// 異步保存操作日志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);
當出現異常數據時,日志如下:
image
從日志中看到,從 ExceptionBlackHoleFixController 類中對異常進行處理。
代碼倉庫:https://gitee.com/litao851025/learnFromBug
代碼地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/exceptionblackhole
本文鏈接:http://www.www897cc.com/showinfo-26-57841-0.html線程池異常黑洞及其防范策略
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 一加 Ace 3 Pop-up 快閃活動來襲,二十城掀起搶購熱潮
下一篇: 如何使用Kotlin開發DSL?