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】【不推荐】
改造流程:
- 在SpringBoot的配置类上或者Controller上加上@EnableAsync
- 在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的时间。
网友评论