主要以结果为导向解释Spring 事务原理,连接池的消耗,以及事务内开启事务线程要注意的问题.
Spring 事务原理这里不多说,网上一搜一大堆,也就是基于AOP配合ThreadLocal实现.
这里强调一下Spring Aop 以及Spring 注解式注入在非Spring容器管理的类中是无效的.
因为Spring Aop是在运行时实现字节码增强,字节码增强有多种实现方法,请自行了解,原生AspectJ是编译时织入,但是需要特定的编译器.语法并没有Spring Aop好理解.
先看下Spring的 事务传播行为类型
事务传播行为类型 |
说明 |
PROPAGATION_REQUIRED |
如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是 最常见的选择。 |
PROPAGATION_SUPPORTS |
支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY |
使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW |
新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED |
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER |
以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED |
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED类似的操作。 |
打开日记debug模式,留意控制台输出
以下测试为了可读性以及更容易理解全是基于Spring注解式事务,而没有配置声明式事务.
测试1:
可以看见只创建了一个SqlSession以及一个事务,在方法内所有操作都使用同一个连接,同一个事务
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) @Transactional(propagation = Propagation.REQUIRED) public void testThreadTx(){ //此方法没有事务(当前方法是 Propagation.REQUIRED) Quotation quotation = quotationService.findEntityById(new String("1")); //此方法没有事务(当前方法是 Propagation.REQUIRED) quotationService.updateEntity(quotation); }
//查看控制台输出(红字关键部分,第三个查询是更新方法内部需要先查询一次再更新,可以无视)
Creating a new SqlSession Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] ansaction(line/:54) -JDBC Connection [1068277098(com.mysql.jdbc.JDBC4Connection@5d92bace)] will be managed by Spring otationMapper.findEntityById(line/:54) -==> Preparing: SELECT * FROM table WHERE id = 1 otationMapper.findEntityById(line/:54) -==> Parameters: otationMapper.findEntityById(line/:54) -<== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] from current transaction Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] from current transaction otationMapper.updateEntity(line/:54) -==> Preparing: update ….. where id = 1 otationMapper.updateEntity(line/:54) -==> Parameters: otationMapper.updateEntity(line/:54) -<== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] .impl.UserOperationLogServiceImpl(line/:41) -请求所用时间:207 .impl.UserOperationLogServiceImpl(line/:42) -请求结束******************************************************************************* Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4]
测试2:不使用事务
可以看出在非事务操作数据库,会使用多个连接,非常不环保,这里给稍微多线程插入埋下一个陷阱
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) // @Transactional(propagation = Propagation.REQUIRED) public void testThreadTx(){ Quotation quotation = quotationService.findEntityById(new String("1")); quotationService.updateEntity(quotation); }
//查看控制台输出(红字关键部分,第三个查询是更新方法内部需要先查询一次再更新,可以无视) Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f7b94f] was not registered for synchronization because synchronization ansaction(line/:54) -JDBC Connection [352410768(com.mysql.jdbc.JDBC4Connection@c63fcb6)] will not be managed by Spring otationMapper.findEntityById(line/:54) -==> Preparing: SELECT * FROM table WHERE id = 1 otationMapper.findEntityById(line/:54) -==> Parameters: otationMapper.findEntityById(line/:54) -<== Total: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f7b94f] Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7a41785a] was not registered for synchronization because synchronization ansaction(line/:54) -JDBC Connection [1615108970(com.mysql.jdbc.JDBC4Connection@38377d86)] will not be managed by Spring otationMapper.findEntityById(line/:54) -==> Preparing: SELECT * FROM table WHERE id = 1 otationMapper.findEntityById(line/:54) -==> Parameters: otationMapper.findEntityById(line/:54) -<== Total: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7a41785a] Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@181e5a22] was not registered for synchronization because synchronization ansaction(line/:54) -JDBC Connection [2096339748(com.mysql.jdbc.JDBC4Connection@5d4e9892)] will not be managed by Spring otationMapper.updateEntity(line/:54) -==> Preparing: update …. where id = 1 otationMapper.updateEntity(line/:54) -==> Parameters: otationMapper.updateEntity(line/:54) -<== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@181e5a22] .impl.UserOperationLogServiceImpl(line/:41) -请求所用时间:614 .impl.UserOperationLogServiceImpl(line/:42) -请求结束*******************************************************************************
测试3:
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) @Transactional(propagation = Propagation.REQUIRED) public void testThreadTx(){ final ExecutorService executorService = Executors.newFixedThreadPool(3); Quotation quotation = quotationService.findEntityById(new String("1")); quotationService.updateEntity(quotation); List<Future<Integer>> futures = new ArrayList<Future<Integer>>(3); for(int i=0;i<3;i++){ Callable<Integer> task = new Callable<Integer>() { @Override public Integer call() throws Exception { Quotation quotation = quotationService.findEntityById(new String("1")); quotationService.updateEntity(quotation); return null; } }; futures.add(executorService.submit(task)); } executorService.shutdown(); }
//查看控制台输出(红字关键部分,第三个查询是更新方法内部需要先查询一次再更新,可以无视)
为了节篇幅,这里不贴出控制台数据
大概就是输出了10个Creating a new SqlSession(大概有些同学使用了多线程,把线程池耗完了也没弄明白原因)
外层方法启动一个,内部3个线程,每个线程3个.一共是使用了10个连接.
为什么?这涉及到ThreadLocal以及线程私有栈的概念.如果Spring 事务使用InhertableThreadLocal就可以把连接传到子线程,但是为什么Spring不那么干呢?因为这样毫无意义,如果把同一个连接传到子线程,那就是SQL操作会串行执行,那何必还多线程呢?
有关于ThreadLocal,InhertableThreadLocal配合线程池的一些陷阱
请看我另一篇文章:
ThreadLoacl,InheritableThreadLocal,原理,以及配合线程池使用的一些坑
测试4:
既然使用同一个事务,不能实现并发操作,那么只能折中,在每一个线程开启一个事务,减少创建更多的连接,执行完毕以后可以返回操作成功失败结果,反馈给用户
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) // @Transactional(propagation = Propagation.REQUIRED) public void testThreadTx(){ ExecutorService executorService = Executors.newFixedThreadPool(3); List<Future<Integer>> futures = new ArrayList<Future<Integer>>(3); for(int i=0;i<3;i++){ Callable<Integer> task = new Callable<Integer>() { @Override public Integer call() throws Exception { quotationService.doSomeThing(); return null; } }; futures.add(executorService.submit(task)); } executorService.shutdown(); } //封装一下 @Override @Transactional(propagation =Propagation.REQUIRED) public void doSomeThing(){ Quotation quotation = this.findEntityById(new String("1")); this.updateEntity(quotation); }
//查看控制台输出,只会创建3个连接,为节省篇幅,这里不贴出控制台所有数据 Creating a new SqlSession Creating a new SqlSession Creating a new SqlSession
最后小技巧PROPAGATION_NOT_SUPPORTED(仅仅为了让Spring能获取ThreadLocal的connection),如果不使用事务,但是同一个方法多个对数据库操作,那么使用这个传播行为可以减少消耗数据库连接
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testThreadTx(){ Quotation quotation = quotationService.findEntityById(new String("1")); quotation.setStatus(ClassDataManager.STATE_N); quotationService.updateEntity(quotation); } //这样只会创建一个SqlSession