事务 + 异步线程使用
描述
在前端通过Excel导入数据时,数据量过大需要等待很长时间,这个时候就可以使用异步线程,在后台另开一个线程来处理数据导入,同时在前台展示一条处理日志,显示未完成,等到异步线程处理完成,修改该状态为完成状态。这样做的好处就是减少用户等待的时长,前台不会阻塞在当前位置,文件上传完成后,用户可以使用其他功能,不影响后台处理数据。
在springBoot项目中使用异步线程
1、在启动类中加入@EnableAsync
2、(可选)在启动类或者新建一个configuration中添加一个bean
@Bean(name = "自定义")
public ThreadPoolTaskExecutor threadPoolTaskExecutor()
{
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(maxPoolSize);// 最大可创建的线程数
executor.setCorePoolSize(corePoolSize);// 核心线程池大小
executor.setQueueCapacity(queueCapacity);// 队列最大长度
executor.setKeepAliveSeconds(keepAliveSeconds);// 线程池维护线程所允许的空闲时间
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
3、在需要开启异步线程的方法上添加@Async(“自定义的线程池名称,可不写”)
@Async("threadPoolTaskExecutor")
@Override
public void importInfo(byte[] bytes, String type, PersonDeptImportLog log) throws Exception {
文件的传递
在使用异步线程的过程中,还出现了一个问题,controller层接收到前端传过来的文件后,需要将这个文件传到处理数据的方法内,问题就是在 当controller这个线程执行完后,这个文件就被销毁了,后面方法就会报错,这个文件不存在。后面我将这个文件转换为输入流之后再进行传递也还是不行,会报流被关闭的错误。这个问题出现的原因可能和前面说的一样,当controller方法执行完成后,线程关闭,输入流也会被关闭。
解决方案
这个时候其实有一种解决办法,就是讲这个临时文件保存到本地,然后在进行操作,完成后再将这个文件删除,避免服务器被占满。这个方法也可以,但是我对文件的这些操作不是很熟悉,所以就没有采用。
最后我找到了另一种解决办法,就是将文件的输入流转换为byte数组,org.springframework.util.FileCopyUtils.copyToByteArray() 这个方法,后面再将数组转换成输入流就可以了。
事务的使用
描述
上文已经讲到我们需要对数据库进行批量操作,如果在操作过程中出现了错误,那么我们是需要将之前的所有操作都撤销,也就是回滚,回到最开始的状态。
这一步其实很简单,SpringBoot为我们提供了一个注解**@Transactional**。只需要在类的方法头上添加即可。不过Transcational有几条注意事项
- 不应在接口上声明**@Transactional**,应在具体类的方法上使用 ,否则注解可能无效
- 哪个方法需要事务,就在哪方法的头上加注解,若是放到类的头上,会使类中的所有方法都带有事务
- 使用了 @Transactional的方法,对同一个类里面的方法调用, @Transactional无效。
- 事务的传播属性:
- 在类A事务方法内,调用了类B非事务的方法,没有事务的方法一样会拥有事务特性,和类A事务的方法使用的是同一个事务.(无论哪个方法出错,都会回滚)
- 在类A事务方法内,调用了类A的非事务方法,非事务方法同样会拥有事务特性,且为同一个事务.(无论类A的事务方法出现错误,还是类A的非事务方法出现错误,数据库的任何操作都会回滚)
- 在类A中的非事务方法内,调用类A的事务方法(事务不生效)
事务传播行为类型 | 说明 |
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
事务的传播是发生在不同类之间的,这一点一定要注意!!!
注意
spring AOP 异常捕获原理:被拦截的方法需显式抛出异常,并且不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获runtimeException的异常
如果使用了事务,并且你捕获了异常,在catch子句里做一些业务处理,那么一定要在catch子句里面再次抛出异常,而且是运行时异常RuntimeException和error, 或者你在注解这里指定一下,抛出异常的类型,
@Transactional(rollbackFor = Exception.class) //这个配置仅限于 Throwable 异常类及其子类。
或者在catch语句中增加这行代码,手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
效率
在未开启事务之前,两万多条数据需要五分钟以上才能执行完毕,开启事务之后,只需要三十多秒即可完成,效率提升10倍。因为在未开启事务时,MySQL默认自动提交,每插入一条数据,就提交一次,两万多条数据需要提交两万多次。肯定会大大降低插入数据的速度。而开启事务之后,插入数据时只要中间没有出错,等到执行完成就只提交一次,这样会节省很多时间。当然事务是有一定限制的,MySQL有innodb_log_buffer_size配置项,超过这个值会把innodb的数据刷到磁盘中,这时,效率会有所下降。
最后
我使用的是异步线程 + spring容器获取实例来处理数据和更新日志的。
在解决这个问题的过程中,我使用了以前极少使用的线程相关知识,还有接触过一点事务相关知识。