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

1|0前言

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

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

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

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

2|0JDK的Future

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

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

本文不重点介绍future方法,可自行网补

3|0Guava中的超时

Google开源的Guava工具包,还是比较强大的;里面即包含了超时的控制。里面有个

TimeLimiter 是个接口,下面有两个子类,

FakeTimeLimiter, 常用于debug时,限制时间超时调试

SimpleTimeLimiter 常用于正式方法中,调用方法超时,即抛出异常

4|0SimpleTimeLimiter

这个类有2种方式实现超时的控制,代理模式和回调模式

5|0一、基于代理模式

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秒超时

6|0解耦合,重构代码

我们发现上面的代码需要在调用方实现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("获取学生爱好------结束"); } }

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

7|0二、基于回调模式

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

@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使用业务代码。执行结果如下

 

8|0线程池定义

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; }

 

执行结果如下

 

9|0总结

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

源码如下:

 

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


__EOF__

本文作者菜菜
本文链接https://www.cnblogs.com/caicz/p/16729880.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   菜菜聊架构  阅读(1038)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示