如何控制方法的呼叫Timeout超時,並主動中斷呼叫請求

語言: CN / TW / HK

前言

在我們實際開發過程中,我們經常遇到一些場景:

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封裝了一下,使用起來特別方便。小夥伴可自行嘗試。