SpringBoot中使用异步处理

1. 同步处理

创建Controller:

@RestController
public class TestController {

  @Autowired
  private TestService testService;

  @RequestMapping(value = "/hello", method = RequestMethod.GET)
  public String hello() {
    System.out.println("TestController:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
    String res = testService.service();
    System.out.println("res:" + res);
    System.out.println("TestController:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
    return res;
  }
}

创建Service:

@Service
public class TestService {

  public String service() {
    System.out.println("TestService:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
    try {
      TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return "hello";
  }
}

后台打印结果:

TestController:http-nio-8080-exec-1:1591447326694
TestService:http-nio-8080-exec-1:1591447326694
res:hello
TestController:http-nio-8080-exec-1:1591447329695

前端结果:

3.07s后,接收到hello

2. 使用异步

2.1 默认线程池【SimpleAsyncTaskExecutor】【不推荐】

改造流程:

  1. 在SpringBoot的配置类上或者Controller上加上@EnableAsync
  2. 在service的方法加上@Async,代表该方法为异步处理
@RestController
@EnableAsync // 该Controller允许调用异步方法
public class TestController {

  @Autowired
  private TestService testService;

  @RequestMapping(value = "/hello", method = RequestMethod.GET)
  public String hello() {
    System.out.println("TestController:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
    String res = testService.service();
    System.out.println("res:" + res);
    System.out.println("TestController:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
    return res;
  }
}

@Service
public class TestService {

  @Async // 声明该方法为异步方法
  public String service() {
    System.out.println("TestService:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
    try {
      TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return "hello";
  }
}

后台输出结果:

TestController:http-nio-8080-exec-1:1591447797317
res:null
TestController:http-nio-8080-exec-1:1591447797320
TestService:task-1:1591447797325

前端结果:

72 ms后,响应200 OK,但是响应正文没有任何内容

为什么不推荐使用默认的SimpleAsyncTaskExecutor?

因为该线程池不是真正意义上得线程池,因为线程不重用,每次调用都会新建一条线程。可以通过控制台日志输出查看,每次打印的线程名都是[task-1]、[task-2]、 [task-3]、[task-4].... .递增的。

查看SimpleAsyncTaskExecutor源码:

protected void doExecute(Runnable task) {
    Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
    thread.start();
}

@Async注解异步框架提供多种线程:

  • SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。

  • SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。

  • ConcurrentTaskExecutor: Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。

  • ThreadPoolTaskScheduler:可以使用cron表达式。

  • ThreadPoolTaskExecutor : 最常使用,【推荐】。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

2.2 配置线程池【ThreadPoolTaskExecutor】【推荐】

新增一个配置类,里面声明一个线程池的bean:

@Configuration
public class AppConfig {

  @Bean(name = "asynPoolTaskExecutor")
  public ThreadPoolTaskExecutor getScorePoolTaskExecutor(){
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    //核心线程数
    taskExecutor.setCorePoolSize(10);
    //线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
    taskExecutor.setMaxPoolSize(100);
    //缓存队列
    taskExecutor.setQueueCapacity(50);
    //空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁,单位s
    taskExecutor.setKeepAliveSeconds(200);
    //异步方法内部线程名称
    taskExecutor.setThreadNamePrefix("asyn-");
    /**
     *当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize, 如果还有任务到來就会采取任务拒绝策略
     *通常有以下四种策略:
     *ThreadPoolExecutor.AbortPolicy :丟弃任务并抛出RejectedExecutionException异常。
     *ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
     *ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程).
     *ThreadPoolExecutor.CallerRunsPolicy: 重试添加当前的任务,自动重复调用execute()方法,直到成功.
     */
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    taskExecutor.initialize();
    return taskExecutor;
  }
}

在@Async中注明要使用哪个线程池:

@Service
public class TestService {

  @Async("asynPoolTaskExecutor")
  public String service() {
    System.out.println("TestService:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
    try {
      TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return "hello";
  }
}

后台允许结果:

@Service
public class TestService {

  @Async("asynPoolTaskExecutor")
  public String service() {
    System.out.println("TestService:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
    try {
      TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return "hello";
  }
}

4.最佳实践

异步不是任何场景都适用的,上述演示的例子其实并不适合做异步处理,因为进行异步处理后,导致客户端没有结果返回。

举一个合适一点的例子:用户注册这个过程,该请求发送给服务器后,一般会有用户信息拆入数据库(20ms)、赠送积分(红包)(20ms)、创建用户画像(100ms)、创建用户空间(100ms)等。

如果是按照传统的同步处理方式,那么这么一长串流程下来,服务端至少需要240ms时间去完成该请求操作,我们可以仔细分析一下,有很多操作可以异步执行:

如赠送积分(红包)(20ms)、创建用户画像(100ms)、创建用户空间(100ms)等完全可以进行异步处理,但是用户信息插入数据不可以进行异步【破坏了业务一致性】,其实赠送红包等操作进行异步操作,也是破坏了一致性的,但是对于这个功能影响不大。假如说用户注册成功后,赠送红包的逻辑执行失败了,那大不了用户就没有红包呗。如果说用户信息插入数据也异步的话,用户信息插入失败,那就出大事了。

大型系统中,一般使用消息队列去执行异步处理,这样比使用Servlet的异步【springmvc 异步处理是基于servlet3.0异步新特性实现的】至少有两个好处:减轻了应用服务器的处理压力【起到一个分流分压的作用】,另一个好处就是一旦业务执行失败,使用消息中间件的话,可以把执行失败的消息保存下来,积累到一定量后,触发警报,通知运维人员去检查执行失败的原因,将来便于根据滞留的消息再次恢复执行。

总之,使用异步处理的话,几乎花费20ms就可以完成用户注册的体验,在用户看来只耗费了原先1/10的时间。

你可能感兴趣的:(SpringBoot中使用异步处理)