如何控制方法的呼叫Timeout超時,並主動中斷呼叫請求
前言
在我們實際開發過程中,我們經常遇到一些場景:
1、如果呼叫方法超過1秒,就應該停止呼叫,不要一直阻塞下去,防止把本身的服務資源搞掛。
2、在不可預知可能出現死鎖/死迴圈的程式碼,要加上時間的閥值,避免阻塞。
很多開源框架都會有超時響應的設定;如果是我們自己開發的服務,怎麼能做到這點呢?
JDK的Future
在jdk中有個future類,裡面有獲取等待超時的方法。
主要方法: cancel():取消任務 get():等待任務執行完成,並獲取執行結果 get(long timeout, TimeUnit unit):在指定的時間內會等待任務執行,超時則拋異常。
本文不重點介紹future方法,可自行網補。
Guava中的超時
Google開源的Guava工具包,還是比較強大的;裡面即包含了超時的控制。裡面有個。
TimeLimiter 是個介面,下面有兩個子類。
FakeTimeLimiter, 常用於debug時,限制時間超時除錯。
SimpleTimeLimiter 常用於正式方法中,呼叫方法超時,即丟擲異常。
SimpleTimeLimiter
這個類有2種方式實現超時的控制,代理模式和回撥模式。
一、基於代理模式
Guava採用的是JDK動態代理實現的AOP攔截,所以代理類必須實現一個介面。可以達到對類中所有的方法進行超時控制。
pom依賴
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>29.0-jre</version> </dependency>
定義介面
定義了一個學生服務介面;
public interface StudentService { /** * 根據學生id 獲取 學生姓名 * @param studentId * @return */ String getStudentNameById(Integer studentId); /** * 根據學生id 獲取 學生愛好 * @param studentId * @return */ List<String> getStudentHobbyById(Integer studentId); }
介面實現
實現了根據id獲取姓名,以及獲取愛好;
@Service public class StudentServiceImpl implements StudentService { private static Logger logger = LoggerFactory.getLogger(StudentServiceImpl.class); @Override public String getStudentNameById(Integer studentId) { try{ TimeUnit.SECONDS.sleep(3); }catch (Exception e){ } return "張三"; } @Override public List<String> getStudentHobbyById(Integer studentId) { try{ TimeUnit.SECONDS.sleep(10); }catch (Exception e){ } return Lists.newArrayList("籃球","羽毛球"); } }
獲取姓名方法需耗時3秒;獲取愛好方法需耗時10秒。
如何呼叫
@RestController public class TimeoutController { private static Logger logger = LoggerFactory.getLogger(TimeoutController.class); @Autowired private StudentService studentService; @GetMapping("/test/timeout") public void test01(){ SimpleTimeLimiter simpleTimeLimiter = new SimpleTimeLimiter(); StudentService studentServiceProxy = simpleTimeLimiter.newProxy(this.studentService, StudentService.class, 6, TimeUnit.SECONDS); logger.info("獲取學生姓名------開始"); try { String studentNameById = studentServiceProxy.getStudentNameById(1); logger.info("學生姓名:{}",studentNameById); }catch (Exception e){ logger.error("獲取姓名呼叫異常:{}",e.getMessage()); } logger.info("獲取學生姓名------結束"); logger.info("=============================="); logger.info("獲取學生愛好------開始"); try { List<String> studentHobbyById = studentServiceProxy.getStudentHobbyById(1); logger.info("學生愛好:{}",studentHobbyById.toString()); }catch (Exception e){ logger.error("獲取愛好呼叫異常:{}",e.getMessage()); } logger.info("獲取學生愛好------結束"); } }
上面是呼叫程式碼,核心程式碼如下:
SimpleTimeLimiter simpleTimeLimiter = new SimpleTimeLimiter(); StudentService studentServiceProxy = simpleTimeLimiter.newProxy(this.studentService, StudentService.class, 6, TimeUnit.SECONDS);
利用SimpleTimeLimiter新建了代理物件studentServiceProxy,並傳遞了6秒的超時設定。
我們只要在呼叫方法的時候,捕獲TimeoutException異常即可。
執行結果如下:
上面的結果,獲取愛好方法超過了6秒就中斷了,並丟擲了異常。
我們發現配置了超時時間6秒後,StudentServiceProxy代理物件的所有方法都是6秒超時。
解耦合,重構程式碼
我們發現上面的程式碼需要在呼叫方實現SimpleTimeLimiter的配置,感覺耦合度高了點。我們可以把程式碼改造一下。
介面定義
/** * @author gujiachun */ public interface StudentService { /** * 根據學生id 獲取 學生姓名 * @param studentId * @return */ String getStudentNameById(Integer studentId); /** * 根據學生id 獲取 學生姓名---超時控制 * @param studentId * @return */ String getStudentNameByIdWithTimeout(Integer studentId); /** * 根據學生id 獲取 學生愛好 * @param studentId * @return */ List<String> getStudentHobbyById(Integer studentId); /** * 根據學生id 獲取 學生愛好---超時控制 * @param studentId * @return */ List<String> getStudentHobbyByIdWithTimeout(Integer studentId); }
介面實現
@Service public class StudentServiceImpl implements StudentService { private static Logger logger = LoggerFactory.getLogger(StudentServiceImpl.class); private static final TimeLimiter timeLimiter = new SimpleTimeLimiter(); private static final long TimeOutSec = 6; private StudentService studentServiceProxy; public StudentServiceImpl(){ studentServiceProxy = timeLimiter.newProxy(this,StudentService.class,TimeOutSec,TimeUnit.SECONDS); } @Override public String getStudentNameById(Integer studentId) { try{ TimeUnit.SECONDS.sleep(3); }catch (Exception e){ } return "張三"; } @Override public String getStudentNameByIdWithTimeout(Integer studentId) { return studentServiceProxy.getStudentNameById(studentId); } @Override public List<String> getStudentHobbyById(Integer studentId) { try{ TimeUnit.SECONDS.sleep(10); }catch (Exception e){ } return Lists.newArrayList("籃球","羽毛球"); } @Override public List<String> getStudentHobbyByIdWithTimeout(Integer studentId) { return studentServiceProxy.getStudentHobbyById(studentId); } }
呼叫方
@RestController public class TimeoutController { private static Logger logger = LoggerFactory.getLogger(TimeoutController.class); @Autowired private StudentService studentService; @GetMapping("/test/timeout") public void test01(){ logger.info("獲取學生姓名------開始"); try { String studentNameById = studentService.getStudentNameByIdWithTimeout(1); logger.info("學生姓名:{}",studentNameById); }catch (Exception e){ logger.error("獲取姓名呼叫異常:{}",e.getMessage()); } logger.info("獲取學生姓名------結束"); logger.info("=============================="); logger.info("獲取學生愛好------開始"); try { List<String> studentHobbyById = studentService.getStudentHobbyByIdWithTimeout(1); logger.info("學生愛好:{}",studentHobbyById.toString()); }catch (Exception e){ logger.error("獲取愛好呼叫異常:{}",e.getMessage()); } logger.info("獲取學生愛好------結束"); } }
這樣的改造就非常好了,呼叫方不需要關心具體的超時實現,直接呼叫即可。
二、基於回撥模式
上面的代理模式是針對類的,回撥模式是可以針對某段程式碼的。
@GetMapping("/test/timeout1") public void test02(){ logger.info("獲取學生姓名------開始"); SimpleTimeLimiter simpleTimeLimiter = new SimpleTimeLimiter(); Callable<String> task = new Callable<String>() { @Override public String call() throws Exception { try{ TimeUnit.SECONDS.sleep(10); }catch (Exception e){ } return "張三"; } }; try { simpleTimeLimiter.callWithTimeout(task,6,TimeUnit.SECONDS,true); }catch (Exception e){ logger.error("獲取姓名呼叫異常:{}",e.getMessage()); } logger.info("獲取學生姓名------結束"); }
上面程式碼中,定義Callable使用業務程式碼。執行結果如下
執行緒池定義
SimpleTimeLimiter是可以自定義執行緒池的
@Bean(name = "taskPool01Executor") public ThreadPoolTaskExecutor getTaskPool01Executor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); //核心執行緒數 taskExecutor.setCorePoolSize(10); //執行緒池維護執行緒的最大數量,只有在緩衝佇列滿了之後才會申請超過核心執行緒數的執行緒 taskExecutor.setMaxPoolSize(100); //快取佇列 taskExecutor.setQueueCapacity(50); //許的空閒時間,當超過了核心執行緒出之外的執行緒在空閒時間到達之後會被銷燬 taskExecutor.setKeepAliveSeconds(200); //非同步方法內部執行緒名稱 taskExecutor.setThreadNamePrefix("TaskPool-01-"); /** * 當執行緒池的任務快取佇列已滿並且執行緒池中的執行緒數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略 * 通常有以下四種策略: * ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。 * ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。 * ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程) * ThreadPoolExecutor.CallerRunsPolicy:重試添加當前的任務,自動重複呼叫 execute() 方法,直到成功 */ taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); taskExecutor.setWaitForTasksToCompleteOnShutdown(true); taskExecutor.initialize(); return taskExecutor; }
執行結果如下:
總結
SimpleTimeLimiter物件本質上也是使用了JDK中的Future物件實現了Timeout。
原始碼如下:
被Guava封裝了一下,使用起來特別方便。小夥伴可自行嘗試。
- Spring中實現非同步呼叫的方式有哪些?
- 帶引數的全型別 Python 裝飾器
- 整理了幾個Python正則表示式,拿走就能用!
- 設計模式之狀態模式
- 如何實現資料庫讀一致性
- SOLID:開閉原則Go程式碼實戰
- React中如何引入CSS呢
- 慢查詢 MySQL 定位優化技巧,從10s優化到300ms
- 一個新視角:前端框架們都卷錯方向了?
- 編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案
- 手寫程式語言-遞迴函式是如何實現的?
- 一文搞懂模糊匹配:定義、過程與技術
- 新來個阿里 P7,僅花 2 小時,做出一個多執行緒永動任務,看完直接跪了
- Puzzlescript,一種開發H5益智遊戲的引擎
- @Autowired和@Resource到底什麼區別,你明白了嗎?
- “四招”守護個人資訊保安
- CSS transition 小技巧!如何保留 hover 的狀態?
- React如此受歡迎離不開這4個主要原則
- 我是怎麼入行做風控的
- 重溫三十年前對於 NN 的批判:神經網路無法實現可解釋 AI