场景
由于从其他地方接数据,数据量较大。要进行较长时间的读写操作,前端请发出请求后,一直被挂起,现需要将接口改为异步返回。由于请求比较耗时,故还有需求:不允许五分钟内访问两次,详情请见之前的博客。SpringBoot接口限流
整体思路
使用各种方式实现接口的异步返回,需要在耗时操作开始的时候给前端返回结果,而不是执行完耗时操作以后再返回数据给前端。
实现方式
实现方式有很多,我自己使用的是CompletableFuture方式,其他方式整理起来当个笔记。
方式1:CompletableFuture类
CompletableFuture是Java 8中提供的一个用于异步编程的类,它提供了一些方法,可以在异步操作完成后执行特定的任务。CompletableFuture适用于需要执行多个异步操作的场景,可以使用CompletableFuture的方法来组合多个异步操作,提高代码的可读性和可维护性。(该方式为最后使用的方式,其他方式未进行验证使用。)
-
controller
public CompletableFuture<String> addData(){ //元组返回:T1-是否可以进入耗时操作 T2-提示语 Tuple2<Boolean, String> resTuple = addService.getLicense(); if(resTuple.getT1()){ return CompletableFuture.supplyAsync(()->{ //执行耗时操作,同时返回提示语 addService.timeConsuming(); return resTuple.getT2(); }); }else { return CompletableFuture.supplyAsync(resTuple::getT2); } }
-
Service
- getLicense获取是否可以进入耗时操作方法
@Override public Tuple2<Boolean, String> getLicense() { String requestId = RandomUtils.uuid(); Boolean locked = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, source, requestId, TimeUnit.MINUTES); if (!locked) { //正在更新 return Tuples.of(false, "更新中,请稍候。"); } String lastCallTime0 = redisTemplate.opsForValue().get(LAST_CALL_TIME_KEY); Long time = System.currentTimeMillis() - LIMIT_QUERY_TIME - 10 * 1000; String lastCallTimeTemp = lastCallTime0 == null ? String.valueOf(time) : lastCallTime0; long lastCallTime = Long.parseLong(lastCallTimeTemp); long now = System.currentTimeMillis(); if (now - lastCallTime < LIMIT_QUERY_TIME) { redisTemplate.delete(LOCK_KEY); //不到5分钟 return Tuples.of(false, "5分钟内不可重复更新,请稍后重试。"); } // 从ERP获取数据 return Tuples.of(true, "更新中,请稍候。"); }
- timeConsuming耗时操作所在方法
注意要加上@Async注解,开启新的线程来执行耗时操作。
@Async @Override public void timeConsuming() { // 执行耗时操作 long now = System.currentTimeMillis(); redisTemplate.opsForValue().set(LAST_CALL_TIME_KEY, String.valueOf(now)); redisTemplate.delete(LOCK_KEY); }
方式2:Callable接口
Callable是Java标准库提供的一个可以在另一个线程中执行的任务,它的call方法可以返回一个结果,并且可以抛出异常。Callable适用于需要执行长时间运行的代码的场景,可以使用线程池来执行多个Callable任务,提高应用程序的并发处理能力。
-
controller
@RestController public class MyController { @Autowired private MyService myService; @GetMapping("/async") public Callable<String> asyncMethod() { return () -> { String result; try { result = myService.longRunningMethod(); } catch (Exception e) { throw new Exception("异步操作发生异常:" + e.getMessage()); } return result; }; } }
-
service
@Service public class MyService { public String longRunningMethod() { // 长时间运行的代码 return "result"; } }
方式3:DeferredResult类
DeferredResult是Spring框架提供的一种异步处理机制,可以实现非阻塞的异步处理。它的实现原理是在请求处理的过程中,将结果的处理交由另外一个线程或者线程池来完成,这样在当前请求线程中就不需要等待结果的返回,从而实现了异步处理。客户端就可以立即返回结果,而不需要等待异步处理的完成。在异步处理完成后,DeferredResult对象会自动将结果返回给客户端。
使用DeferredResult的步骤如下:
- 在Controller中创建DeferredResult对象,将其返回给客户端。
- 在DeferredResult对象的回调函数中,实现异步处理逻辑,并将处理结果设置到DeferredResult对象中。
- 后台线程或线程池中处理完异步逻辑后,将结果设置到DeferredResult对象中。
需要注意的是,使用DeferredResult可能会带来一些问题,比如长时间的等待、线程池资源的消耗等,需要根据具体的业务场景进行合理的使用。
使用样例:
假设有一个接口 /download,客户端可以通过该接口下载一份大文件,由于文件比较大,下载可能需要很长时间,为了避免客户端等待太久,我们可以使用 DeferredResult 实现异步处理,客户端发起请求后,服务端先返回一个 DeferredResult 对象,然后在后台线程中下载文件,并将下载结果设置到 DeferredResult 对象中,当下载完成后,DeferredResult 对象会自动返回结果给客户端。
- controller
@RestController public class DownloadController { @Autowired private DownloadService downloadService; @RequestMapping("/download") public DeferredResult<ResponseEntity<InputStreamResource>> download() { DeferredResult<ResponseEntity<InputStreamResource>> deferredResult = new DeferredResult<>(); // 在后台线程中进行下载操作 downloadService.downloadFile(deferredResult); return deferredResult; } }
- service
@Service public class DownloadService { @Async public void downloadFile(DeferredResult<ResponseEntity<InputStreamResource>> deferredResult) { try { // 模拟文件下载,耗时5秒 Thread.sleep(5000); // 下载完成后,将结果设置到DeferredResult对象中 File file = new File("path/to/file"); InputStreamResource resource = new InputStreamResource(new FileInputStream(file)); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=test.txt"); ResponseEntity<InputStreamResource> responseEntity = new ResponseEntity<>(resource, headers, HttpStatus.OK); deferredResult.setResult(responseEntity); } catch (Exception e) { e.printStackTrace(); } } }
方式4:Guava异步
Guava是Google公司开源的一个Java工具类库,其中包含了许多实用的工具类,如集合、字符串、并发等。其中,Guava提供了异步编程的支持,可以帮助我们更加方便地实现异步操作。
Guava的异步编程支持主要包括以下两个部分:
- ListenableFuture
ListenableFuture是Guava提供的一个接口,它继承了JDK中的Future接口,同时提供了添加回调方法的功能。这样,在异步任务执行完成后,我们可以通过添加回调方法来处理异步任务的执行结果。ListenableFuture的主要作用是提供一个异步操作的结果的占位符,当异步操作执行完成后,占位符被填充上真正的结果。- ListeningExecutorService
ListeningExecutorService是Guava提供的一个接口,它扩展了JDK中的ExecutorService接口,同时提供了异步任务执行结果的回调功能。使用ListeningExecutorService可以很方便地实现异步任务的执行和结果处理。
在使用Guava的异步编程功能时,通常需要首先创建一个实现Callable接口的异步任务类,然后通过ListeningExecutorService的submit方法将异步任务提交到线程池中执行。执行完成后,通过ListenableFuture的回调方法来处理异步任务的执行结果。
总体来说,Guava的异步编程功能可以帮助我们更加方便地实现异步操作,提高代码的可读性和可维护性。
使用步骤:
- 添加依赖
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1.1-jre</version> </dependency>
- controller
@RestController public class UserController { @Autowired private UserService userService; @GetMapping("/users/{userId}") public ResponseEntity<User> getUserInfo(@PathVariable String userId) throws InterruptedException, ExecutionException { ListenableFuture<User> future = userService.getUserInfo(userId); User user = future.get(); return ResponseEntity.ok(user); } }
- service
@Service public class UserService { private final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); public ListenableFuture<User> getUserInfo(String userId) { return executorService.submit(() -> { // 模拟耗时操作 TimeUnit.SECONDS.sleep(3); // 从外部系统中获取用户信息 User user = externalSystem.getUserInfo(userId); return user; }); } public void processResult(ListenableFuture<User> future) { Futures.addCallback(future, new FutureCallback<User>() { @Override public void onSuccess(User result) { // 处理异步任务执行成功的结果 System.out.println("获取用户信息成功:" + result.getName()); } @Override public void onFailure(Throwable t) { // 处理异步任务执行失败的结果 System.out.println("获取用户信息失败:" + t.getMessage()); } }, MoreExecutors.directExecutor()); } }
写在最后
方法众多,选择合适的用。我只实际使用第一种,其他的未进行使用和验证,如果有什么问题欢迎指正!