深圳幻海软件技术有限公司 欢迎您!

如何控制方法的调用Timeout超时,并主动中断调用请求

2023-02-28

前言在我们实际开发过程中,我们经常遇到一些场景:1、如果调用方法超过1秒,就应该停止调用,不要一直阻塞下去,防止把本身的服务资源搞挂。2、在不可预知可能出现死锁/死循环的代码,要加上时间的阀值,避免阻塞。很多开源框架都会有超时响应的设置;如果是我们自己开发的服务,怎么能做到这点呢?JDK的Futur

前言

在我们实际开发过程中,我们经常遇到一些场景:

1、如果调用方法超过1秒,就应该停止调用,不要一直阻塞下去,防止把本身的服务资源搞挂。

2、在不可预知可能出现死锁/死循环的代码,要加上时间的阀值,避免阻塞。

很多开源框架都会有超时响应的设置;如果是我们自己开发的服务,怎么能做到这点呢?

JDK的Future

在jdk中有个future类,里面有获取等待超时的方法。

主要方法:
cancel():取消任务
get():等待任务执行完成,并获取执行结果
get(long timeout, TimeUnit unit):在指定的时间内会等待任务执行,超时则抛异常。
  • 1.
  • 2.
  • 3.
  • 4.

本文不重点介绍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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

定义接口

定义了一个学生服务接口;

public interface StudentService {
    /**
     * 根据学生id 获取 学生姓名
     * @param studentId
     * @return
     */
    String getStudentNameById(Integer studentId);
  
    /**
     * 根据学生id 获取 学生爱好
     * @param studentId
     * @return
     */
    List<String> getStudentHobbyById(Integer studentId);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

接口实现

实现了根据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("篮球","羽毛球");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

获取姓名方法需耗时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("获取学生爱好------结束");
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

上面是调用代码,核心代码如下:

SimpleTimeLimiter simpleTimeLimiter = new SimpleTimeLimiter();
StudentService studentServiceProxy = simpleTimeLimiter.newProxy(this.studentService, StudentService.class, 6, TimeUnit.SECONDS);
  • 1.
  • 2.

利用SimpleTimeLimiter新建了代理对象studentServiceProxy,并传递了6秒的超时设置。

我们只要在调用方法的时候,捕获TimeoutException异常即可。

执行结果如下:

上面的结果,获取爱好方法超过了6秒就中断了,并抛出了异常。

QQW">我们发现配置了超时时间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);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

接口实现

@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);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

调用方

@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("获取学生爱好------结束");
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

这样的改造就非常好了,调用方不需要关心具体的超时实现,直接调用即可。

二、基于回调模式

上面的代理模式是针对类的,回调模式是可以针对某段代码的。

@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("获取学生姓名------结束");
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

上面代码中,定义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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

执行结果如下:

总结

SimpleTimeLimiter对象本质上也是使用了JDK中的Future对象实现了Timeout。

源码如下:

被Guava封装了一下,使用起来特别方便。小伙伴可自行尝试。