一:事务概述
当程序运行一个方法包括对多组数据处理,如提交订单、创建流水、扣除金额三个操作,当扣除金额失败订单自然是不能被提交修改订单状态,这种场景下就需要事务支持,可以理解为控制一组事件逻辑层面原子性执行。本文将从以下几方面阐述Spring的事务
- 事务基本运用
- 事务传播行为
- 事务隔离级别
- 事务失效
二:基本配置
Spring中事务使用具备声明式与注解式常用的两种方案,接下来就分别讲解这两种方法如何进行配置使用。且本文数据源示例采用MySQL进行,不同的事务数据源只需要修改PlatformTransactionManager实现即可
2.1 事务数据源
上面提到事务数据来源类型多种多样,Spring根据数据源创建事务管理提供统一API设置,抽象了统一的接口PlatformTransactionManager。该接口定义事务基本行为的三个方法,getTransaction()、commit()、rollback()。不同数据源事务只需要声明不同子类实例即可,下面以MySQL数据源为例,spring.xml中声明DataSourceTransactionManager事务管理,并注入数据源
2.2 事务通知
熟悉AOP应用的读者对下面的配置应该可以很快理解,个人为了深刻记忆就把数据源管理理解为一个切面,该切面下定义了事务通知定义方法触发事务条件以及事务行为
指的一提的是transaction-manager属性,看属性名就知道是告诉通知其管理的数据源是什么。为什么属性值是灰色,因为其默认读取的事务数据源就是transactionManager,如果其管理的事务数据源是这这个名称则可以省略,如若不是则需要显示声明
2.3 事务测试
根据配置编写删除数据的一个方法,手动实例化运行时异常空指针抛出。通过日志打印看到SQL已经执行,但是查看最后结果数据库中数据并没有被清理,所以事务管理生效
三:通知配置
3.1 <tx:method>配置解析
通过前面的示例大概知道了通知就是定义事务方法的行为,那么一个事务方法都有哪些参数可以配置定义?如下表所示
参数名称 | 参数作用 | 是否必须 | 默认值 | 备注 |
---|---|---|---|---|
name | 定义事务管理方法 | 是 | 无 | 可以使用*指示符,如get*表示所有get开头的方法 |
read-only | 事务方法只能执行select操作 | 否 | false | 如果执行true事务管理方法非select会抛出异常 |
rollback-for | 指定回滚异常 | 否 | RuntimeException与Error | 如果不指定Spring事务默认只回滚运行时异常与错误子类,受检异常想要回滚可以依靠这个参数进行声明 |
no-rollback-for | 指定不回滚异常 | 否 | 受检异常 | 如果异常并不需要事务回滚可以通过该参数指定 |
timeout | 指定方法超时 | 否 | 底层事务超时值 | 如果方法有限制执行时长则可以依靠该属性回滚,单位为秒s。注意这里的超时限定在事务开始时间到最后一个SQL执行完毕 |
isolation | 隔离级别 | 否 | DEFAULT | 隔离级别后面详解 |
propagation | 传播行为 | 否 | REQUIRED | 传播行为后面详解 |
3.2 read-only 只读测试
配置事务通知,规定get开头的方法只能执行select操作。如果非select操作则会抛出异常,测试结果如下所示。get开头方法操作delete数据未删除,并抛出异常
3.3 rollback-for回滚测试
按照默认配置rollback-for属性支持运行时异常与Error子类回滚,但是受检异常即throws抛出异常不能执行事务回滚。这也是事务经常失效的一个重灾区
测试结果表明service层抛出受检异常ClassNotFoundException,SQL已经执行但是最后数据并没有被删除,所以事务生效。当然除了配置异常名称外还支持配置异常类的完全限定路径,还有一点就是很多时候为了保证事务的可用都配置为Throwable
3.4 no-rollback-for 不回滚测试
有一些异常是运行异常,但是并不想让事务进行回滚。可以通过no-rollback-for属性指定不回滚异常类型
抛出运行时异常空指针,查询数据库发现数据已经被删除。即异常时事务也没进行回滚,参数设置有效
3.5 time-out 超时测试
这里的超时一定要注意计时范围,超时计时并不是说完整的事务方法执行时长,而是从事务开始到最后一个Statement获取 + 最后一个Statement执行时间。粗暴理解就是开始到最后一个SQL执行时长
使用线程睡眠3s最后测试发现抛出事务超时异常,查询数据库数据并未被删除。证明事务超时配置生效,如果将线程睡眠方法移动到SQL执行之后事务超时并不会生效,请自行测试
四:注解式事务
前面展示的都是依靠声明式事务方式,注解式事务使用与声明式事务基本没有差距,使用注解@Transactional,参数配置含义与<tx:method>属性配置类似
4.1 注解位置
可以看到基注解@Target定义范围为METHOD、TYPE,也就是说该注解可以在方法、类、接口上使用。对于接口就有一点必须注意,Spring事务实现依赖于AOP。AOP代理方式默认采用JDK动态代理,该代理基于接口生成代理对象。但是如果设置代理方式为
CGLIB基于类的动态代理,则注解在接口上的事务不会生效
同时还可以看到这个注解被基注解@Inherited修饰,所以当一个类被注解@Transactional修饰时自然也具备事务的管理特点
4.2 CGLIB测试
开启注解事务在spring.xml配置文件中声明即可,同时可以使用属性proxy-target-class指定是否true开启CGLIB代理
将事务注解加在接口上,查看最后执行效果发现虽然抛出了空指针运行时异常但是事务最终回滚了。按照设定来讲使用CGLIB代理之后事务注解在接口应该不生效,但是最后的结果却是生效了,这就不得其解
4.3 注解参数属性
事务的参数属性上面截图的含义与用法都与声明式事务保持一致,并没有任何区别。通过value指定数据源管理、propagation传播性、isolation隔离级别、timeout过期、readonly只读
指定回滚异常与不回滚异常的时候注解属性提供了两种方式,分别是名称与Class运行时对象
五:事务传播行为
5.1 传播行为总结表
传播行为 | 含义 |
---|---|
REQUIRED | 默认事务传播性。A方法调用B方法,A方法若不支持事务,则运行到B方法会创建一个新的事务。反之则运行在A的事务中 |
SUPPORTS | 支持事务。A方法调用B方法,A方法若支持事务,则B运行于A事务中。反之则不运行于事务中 |
MANDATORY | 必须事务。A方法调用B方法,A方法若支持事务,则B运行与A事务中,反之则抛出异常 |
REQUIRED_NEW | 创建新事务。不论方法A是否存在事务,运行到B创建新的事务。注意这个事务是完全隔离的事务,事务的回滚、提交与外部事务互不影响干扰 |
NOT_SUPPORTS | 挂起事务。A方法调用B方法,A方法若支持事务,运行到B时A方法事务挂起 |
NEVER | 不支持事务。A方法调用B方法,A方法若支持事务,运行到B则抛出异常 |
NESTED | 嵌套事务。若A不支持事务则与REQUIRED一致。若A支持事务,B创建内层事务。内层事务回滚不影响外层事务回滚。外层事务回滚影响内层事务回滚 |
5.2 REQUIRED测试
方法deleteStduent()不具备事务,但是运行到方法testPropagation()创建新的事务,最后结果id=1删除,id=2未删除
5.3 SUPPORTS测试
方法deleteStduent()不具备事务,运行到方法testPropagation()也不会创建新的事务,最后结果id=1删除,id=2删除
5.4 MANDATORY测试
方法deleteStduent()不具备事务,运行到方法testPropagation()抛出非法事务状态异常,最后结果id=1删除,testPropagation()未执行
5.5 REQUIRED_NEW 测试
方法deleteStduent()不具备事务,运行到方法testPropagation()创建新事务。deleteStudent()中抛出异常,则deleteStudent()事务回滚。testPropagation()事务未回滚
5.6 NOT_SUPPORTS
方法deleteStduent()具备事务,运行到方法testPropagation()挂起该事务。deleteStudent()中抛出异常,则deleteStudent()事务回滚。testPropagation()未回滚
5.7 NEVER 测试
方法deleteStduent()具备事务,运行到方法testPropagation()抛出非法事务状态异常,最后结果deleteStduent()事务回滚,testPropagation()未执行
5.8 NESTED 测试
deleteStudent()不支持事务,运行到testPropagation()开启新事务。抛出异常导致回滚,id=2未被删除
deleteStudent()支持事务,testPropagation()开启内层事务。内层事务回滚不影响外层事务回滚
deleteStudent()支持事务,testPropagation()开启内层事务。外层事务回滚影响外内层事务回滚
六:隔离级别
隔离级别 | 含义 |
---|---|
DEFAULT | 采用数据库默认的隔离级别 |
READ_UNCOMMITED | 读取其它事务未提交数据,可能会脏读、不可重复读、幻读 |
READ_COMMITED | 只能读取已经提交事务数据,可能产生不可重复读、幻读 |
REPEATABLE_READ | 可重复读,但是不能防止幻读 |
SERIALIZABLE | 最高等级,无数据安全问题 |
6.1 READ_UNCOMMITED测试
使用REQUIRED_NEW传播行为出创建独立事务查询deleteStudent()事务未提交数据
最后结果打印对象信息,该隔离级别会产生脏读、不可重复读、幻读,具体的事务隔离请移步数据库相关文章了解
七:特殊情况
7.1 非事务调用事务
非事务方法调用事务方法是导致事务失效的一个重要误区,事务实现基于代理对象,类中直接进行调用则可以理解为是通过目标对象,这时候事务肯定无效。若想使得事务有效则可以通过图一配置暴露代理对象,然后在代码中修改类调用方式,使用注释的方式通过代理对象调用,则事务生效
7.2 try-catch事务
当异常被try-catch处理之后自然事务是不会生效的,这时候可以通过上图所示代码控制事务回滚