文章目录
- 应用场景
- 代码测试
- 全局配置
- 1、业务层接口
- 2、业务层实现
- 测试异步调用
- 1、无返回值
- 2、有返回值
- 3、事务回滚
- 模拟真实业务登录场景
- 1、 前端控制器分发任务
- 2、用户登录业务接口实现
- 3、异步manager
- 4、日志业务接口实现
应用场景
当我们登录系统的时候,我们的业务主要是验证账号和密码,而和登录影响不大的其它业务,例如:
- 发送邮箱
- 发送短信登录提醒
- 发送系统登录日志
等等其他业务操作。我们为了用户的体验,我们可以将其它的业务操作放到子线程中在后台慢慢执行。
众所周知,程序的运行默认是从上而下的单线程运行,当我们需要执行一个异步操作的时候,就需要手动的创建一个新的线程。一般我们的操作如下
public class ThreadTest {
public static void main(String[] args) {
// 这里我实现一个Runnable接口的实现
Runnable runnable=()->{
long start = System.currentTimeMillis(); // 获取线程的开始执行时间
try {
Thread.sleep(3000); // 线程沉睡3秒中,便于观察
System.out.println("异步执行一个方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis(); // 获取线程的结束时间
System.out.println(Thread.currentThread().getName()+", "+(end-start)+"毫秒");
};
// 这里我们开启两个线程相互执行
Thread thread = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread.start(); // 开启线程一
thread2.start(); // 开启线程二
}
}
控制台打印的时候是同时打印的,也就是说我们两个线程执行的时间一共是3+秒。当然我们也可以通过线程池中取得线程执行,这边就不进行展开了。
这篇文章我们讨论的重点就是:spring 3.0之后推出的一个异步注解的形式,如何优雅的开启异步调用。
代码测试
全局配置
新建一个全局的配置类,开启一个线程池,要想@Async这个注解起作用,必须加@EnableAsync这个注解,异步功能模块的功能加载。
@Async会默认从线程池获取线程,当然也可以显式的指定@Async(“thread-pool”)
关于为什么使用线程池的优点,我就不过多介绍了,无非一下几点
- 节省性能开销,程序不用频繁的创建线程,和销毁线程。
- 提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.mybatis.spring.annotation.MapperScan;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
@MapperScan("com.example.mapper")// 扫描mapper
public class ThreadPoolConfig {
@Bean("thread-pool")
public ThreadPoolTaskExecutor threadPool(){
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(50); //配置核心线程的大小
pool.setMaxPoolSize(200); //配置线程池的最大线程数
// 线程池维护线程所允许的空闲时间
pool.setKeepAliveSeconds(300);
// 队列最大长度
pool.setQueueCapacity(1000);
// 线程池对拒绝任务(无线程可用)的处理策略
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return pool;
}
}
配置好后,@Async会默认从线程池获取线程,当然也可以显式的指定@Async(“asyncTaskExecutor”)。
1、业务层接口
public interface IAsyncService {
/**
* 无返回值
*/
@Async
void insert();
/**
* 有返回值
* @param name
* @return
*/
@Async
Future<String> delete(String name);
/**
* 有返回值,测试事务
* @param to
* @return
*/
@Async
void update(Boolean to);
}
2、业务层实现
完全可以看到我实现了三个接口,每个接口我都模拟成执行2秒中结束。
import com.example.future.IAsyncFutureService;
import com.example.mapper.UserMapper;
import com.example.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.Future;
@Service
public class AsyncFutureServiceImpl implements IAsyncFutureService {
/**
* 加入datasource的 mapper
* 为了测试事务回滚,加入的数据代理模型
*/
@Autowired
private UserMapper userMapper;
@Async
@Override
public void insert() {
long start = System.currentTimeMillis();
try {
Thread.sleep(2000);
System.out.println("2秒执行任务insert");
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ", " + (end - start) + "毫秒");
}
@Async
@Override
public Future<String> delete(String name) {
long start = System.currentTimeMillis();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ", " + (end - start) + "毫秒");
return AsyncResult.forValue(name);
}
@Async
@Override
@Transactional //开启事务
public void update(Boolean to) {
User user = new User();
user.setName("张三");
userMapper.insertSelective(user);
// 模拟异常,事务回滚
if (to) {
throw new RuntimeException("模拟异常");
}
}
}
测试异步调用
将异步的调用放到spring容器中,程序启动的时候自动加载,这个方便我测试,免去了从外部请求的流程。
注意一下几点bug:
- 我们将异步Manager (就是下边的bean)、异步的业务层,分开放置。用异步Manager,调用业务层里边的逻辑。如果没有分开放置,异步调用不会起作用。
- 真实的开发环境的使用,肯定不会程序已启动就加载异步Manager,而是控制层进行调用的这个异步Manager。
1、无返回值
@Component
public class Manager {
// @Autowired
// private IAsyncService asyncService;
@Autowired
private IAsyncFutureService futureService;
/**
* 无返回值,异步调用
*/
// @Bean
public ApplicationRunner applicationInsert() {
return applicationArguments -> {
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ":开始调用异步业务");
//无返回值
futureService.insert();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ":调用异步业务结束,耗时:" + (endTime - startTime));
};
}
}
2、有返回值
/**
* 有返回值,异步调用
*/
@Bean
public ApplicationRunner applicationDelete() {
return applicationArguments -> {
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ":开始调用异步业务");
//返回值
Future<String> delete = futureService.delete("异步调用,有返回值输出");
String s = delete.get();
System.out.println("返回值:"+s);
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ":调用异步业务结束,耗时:" + (endTime - startTime));
};
}
3、事务回滚
# 打印调试日志,方便观察
logging:
level:
root:
debug
/**
* 启动成功
*/
@Bean
public ApplicationRunner applicationRunner() {
return applicationArguments -> {
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ":开始调用异步业务");
//事务测试,模拟异常事务回滚
futureService.update(true);
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ":调用异步业务结束,耗时:" + (endTime - startTime));
};
}
如图可以看到,我们的业务抛出了一个异常,并执行了事务回滚
看到数据库中并没有加进去数据,说明我们的事务已经起作用了
模拟真实业务登录场景
我们登录的时候,除了输入登录的账号密码外,我再加一个需求:保存登录的日志
1、 前端控制器分发任务
请求地址发送到控制器中,分发任务到各个业务接口。
{
"name":"zhangsan"
}
import com.example.db.service.UserService;
import com.example.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserService userService;
// 登录接口
@RequestMapping("/login")
public void login(@RequestBody User user){
userService.login(user);
}
}
2、用户登录业务接口实现
用户的登录业务逻辑模拟,引入异步manager,异步调用log日志创建
import com.example.Manager;
import com.example.db.service.UserService;
import com.example.mapper.UserMapper;
import com.example.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private Manager manager;
@Override
public void login(User user){
userMapper.insertSelective(user);
// 异步manager 调用log服务
manager.logInsert(user.getId());
}
}
3、异步manager
调用log异步服务,如果有其他的业务也是写在该方法下边一起异步执行登录的逻辑,例如发邮件,发登录短信提醒等。
public void logInsert(Integer userID){
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ":开始调用异步业务");
try {
Thread.sleep(3000);
Log log = new Log();
log.setUserId(userID);
logService.insert(log);
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ":调用异步业务结束,耗时:" + (endTime - startTime));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
4、日志业务接口实现
可以看到我们这里加入了@Async注解开启了异步执行的通道。
import com.example.db.service.LogService;
import com.example.mapper.LogMapper;
import com.example.model.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
@Async
@Override
public void insert(Log log){
log.setText("欢迎用户:"+log.getUserId()+"登录成功");
logMapper.insertSelective(log);
}
}
这里只有一个日志总共执行了3秒多,如果有其他的业务放在manager中的logInsert()方法总的执行时间也是在3秒+,原因是我的是异步调用,每个服务都是一个线程独立执行,互不干涉。