目录
概念:简单理解,就是多个事务方法互相调用过程中,事务的传播形式
rollbackFor & rollbackForClassName
noRollbackFor & noRollbackForClassName
注意正确设置rollbackFor,保证发生异常后能正确回滚。
@Transactional注解只能应用到public方法才能生效。
3 上述场景 ,假设methodB的传播机制被调整为REQUIRED_NEW,如果此时methodB的异常抛出到methodA中被捕获了,methodA会不会回滚?为什么
原理
① 事务开始时,通过AOP生成一个代理connection对象。并将其放入DataSource实例的某个与
DataSourceTransactionManager相关的某处容器中。
②在接下来的整个事务中,客户端代码都应该使用该connection连接数据库,执行所有数据库命令。DataSource与TransactionManager需要配置相同的数据源。
③事务结束时,回滚①中得到的代理 connection对象上执行的数据库命令,然后关闭改代理connection对象。
@Transactional注解作用于哪些地方
作用于类
表示所有该类的public方法都配置相同的事务属性信息。
@Transactional
public class ServiceA{
}
作用于方法
当类配置了@Transactional注解并且方法也配置了@Transactional,方法上的事务属性会覆盖类的配置。
public class ServiceA{
@Transactional
public void methodA(){
}
}
作用于接口
不推荐这种使用这种用法,因为一旦标注在Interface上并且配置了SpringAop使用CGLib动态代理,将会导致@Transactional注解失效
// !!! 如果AOP使用cglib动态代理,将导致事务失效
@Transactional
public interface IService{
}
属性
// transational 注解的属性
@Transactional(transactionManager = "txManager",
timeout = 1000,
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
readOnly = true,
rollbackForClassName = {"classA","classB"},
noRollbackFor = {ExcelCommonException.class},
noRollbackForClassName = {"classC","classD"},
rollbackFor = {Exception.class,RuntimeException.class})
isolation(隔离级别)
为什么要有隔离级别这个概念?
1.为了防止在并发操作数据库时产生脏读、不可重复读、幻读 这些数据问题而引申出来的一个处理机制。
2.隔离的本质是使事务在一定程度上串行化执行。也正因为如此,数据库事务隔离越严格,并发副作用越小,系统开销代价越大。 隔离可以理解成数据库操作的串行。
数据问题有哪些
- 脏读: 按一个事务在处理过程中读取了另一个事物未提交的数据。(你还没提交就被我读出来了,万一你回滚了怎么办?够脏吧)下表格说明复现场景:
操作顺序 | 事务A | 事务B |
1 | begin | |
2 | begin | |
3 | select amount from salary where id = 1; // 假设结果是1 | |
4 | update salary set amount = 2 where id = 1; //结果被变更为2,但未提交 | |
5 | select amount from salary where id = 2;//发现结果为2 |
- 不可重复读:一个事务内,多次查询某个数据,却得到不同的结果。(脏读是读到未提交的数据,不可重复读读到的却是已经提交的数据,但实际上违背了事务一致性原则)
操作顺序 | 事务A | 事务B |
1 | begin | |
2 | begin | |
3 | select amount from salary where id = 1; // 假设结果是1 | |
4 | update salary set amount = 2 where id = 1;//变更 | |
5 | select amount from salary where id = 1; // 结果是1 | |
6 | commit | |
7 | select amount from salary where id = 1; // 结果是2 |
- 幻读:在可重复读(repeatable read 隔离级别下),一个事务可能会遇到幻读的问题。事务A按照指定搜索条件去读取若干行,事务B以C、U、D等行为来修改事务A的结果集,然后再提交。这时事务A再次按照刚才的条件去读取发现数据条数发生了变化,数据时多时少似乎是产生了幻觉。
操作顺序 | 事务A | 事务B |
1 | begin | |
2 | begin | |
3 | select amount from salary where amount > 10000; // 假设结果有10条 (id 1~10) | |
4 | insert into salary (id,amount)value(11,20000);//插入一条amount 为 20000 id为11的数据。 | |
5 | commit | |
6 | select amount from salary where amount > 10000; // 发现结果有11条 (id 1~11) |
- 丢失修改:有两个事务先后修改一条数据,前者修改后,后者立即修改同一条数据。最终导致数据丢失。
概念
为了解决隔离与并发的矛盾,SQL92 数据库标准中定义了4个隔离级别:
@Transactional有哪些隔离级别?
READ_UNCOMMITTED 读未提交
概念:表示事务A可以读取事物B修改了但还未提交的数据。 会存在 脏读、幻读、不可重复读、丢失修改等问题
重点掌握: 此种隔离级别最不安全,并发量大时很容易出现数据问题。
READ_COMMITTED 读已提交
概念:表示事务A可以读取到事务B提交了的数据。会存在 不可重复度、幻读问题。解决了脏读问题。
如何解决脏读举例:
操作顺序 | 事务A | 事务B |
1 | begin | |
2 | select stock from warehouse; // 库存量为2 | begin |
3 | update warehouse set stock = (2-1);//扣减库存 | select stock from warehouse; // 由于A未提交,库存量仍为2 |
4 | update warehouse set stock = (2-1);//扣减库存 | |
5 | rollback;//执行失败出现异常,回滚A | commit;//B执行完后,最终库存为1(正确),克服A的脏数据。 |
重点掌握: PostGreSql,Oracle,SQLserver 均是这种隔离级别
REPEATABLE_READ 可重复读
概念:可重复读,表示在同一事务内,事务第一次或者第n次读取某一条数据其结果都是一致的。解决了脏读、不可重复读的问题,但仍存在幻读问题。
如何解决不可重复读举例:
操作顺序 | 事务A | 事务B |
1 | begin | |
2 | select stock from warehouse; // 库存量为1 | begin |
3 | update warehouse set stock = (2-1);//扣减库存 | select stock from warehouse; //由于事务A已占用这条数据的锁,不允许读取直到A提交或回滚。 |
4 | commit;//库存为 0 | select stock from warehouse; // 库存量为0,库存不足,无法扣减 |
5 | commit; |
重点掌握: Mysql是这种隔离级别。
SERIALIZABLE 串行化
概念:串行化,相当于事务之间串行执行、提交。杜绝了脏读、不可重复读、幻读、丢失修改的问题。
重点掌握: 绝对安全,效率底下。
枚举源码
package org.springframework.transaction.annotation;
public enum Isolation {
DEFAULT(-1),//默认值,表示使用底层数据默认隔离级别。 常见数据库的默认隔离级别mysql ---repeatable,oracle,pg,sql server ---read commited”
READ_UNCOMMITTED(1),//读未提交,表示事务A可以读取事物B修改了但还未提交的数据。 会存在 脏读、幻读、不可重复读、丢失修改等问题
READ_COMMITTED(2),//读已提交,表示事务A可以读取到事务B提交了的数据。会存在 不可重复度、幻读问题。解决了脏读问题。
REPEATABLE_READ(4), //可重复读,表示在同一事务内,事务第一次或者第n次读取某一条数据其结果都是一致的。解决了脏读、不可重复读的问题,但仍存在幻读问题。
SERIALIZABLE(8);//串行化,相当于事务之间串行执行、提交。杜绝了脏读、不可重复读、幻读、丢失修改的问题。
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
propagation(传播机制)
为什么要有传播机制这个概念?
在方法的嵌套调用中,是重新创建事务还是使用父方法的事务?以及父方法的回滚对子方法的事务是否有影响。这些因素需要被精确地控制,于是引进了传播机制这个概念。
概念:简单理解,就是多个事务方法互相调用过程中,事务的传播形式
@Transactional有哪些传播机制?
//事务a
A {
void methodA() {
//在A中调用B的methodB()
B.methodB();
}
}
//服务b
B {
void methodB() {
}
}
REQUIRED (spring默认)
概念:无则新建,有则加入。
举例分析:
①如果A中的methodA已有事务,此时B.methodB()的操作直接加入到A的事务中,无论methodA还是methodB发生异常,将会一起回滚methodA所在的事务中的整体操作。
②如果A中的methodA没有事务,此时B.methodB()的操作新建一个属于methodB()的事务,methodB方法中发生异常仅回滚自身的事务,不影响methodA()。
SUPPORTS
概念:当前在事务中则以事务形式运行,当前不在事务中则和普通代码运行。
举例分析:
①如果A中的methodA已有事务,此时B.methodB()的操作直接加入到A的事务中,无论methodA还是methodB发生异常,将会一起回滚methodA所在的事务中的整体操作。
②如果A中的methodA没有事务,此时B.methodB()的操作将和普通代码一样不受事务控制。methodB方法即是发生异常也不会回滚,不影响methodA()。
MANDATORY
概念:当前方法必须被一个父事务调用,否则抛出异常。
举例分析:
①methodA如果被调用,则父方法中必须包含事务控制,否则抛异常。
REQUIRES_NEW(新建一个事务)
概念:新建一个事务,如果当前处在事务中,则将当前事务挂起。
举例分析:
①methodB如果被调用,直接新建一个事务。如果当前methodB的调用方处在事务中,则挂起当前事务,等methodB的事务提交后恢复继续执行。methodB中发生异常仅回滚methodB中的事务操作。
NOT_SUPPORTED
概念:如果当前处在事务中,则挂起当前事务,然后以非事务方式运行。运行结束后恢复当前事务。
举例分析:
①如果methodB被调用,当前处在事务中,则挂起当前事务,等methodB以非事务方式运行完毕后再恢复当前事务。
②如果methodB被调用,当前不处在事务中 ,则以普通方式运行。
NEVER
概念:不能在事务中运行。否则抛异常。
举例分析:
①如果methodB被调用,当前关联了任何一个事务则直接抛异常。否则可以正常以非事务方式运行。
NESTED(嵌套事务)
概念:开始一个“嵌套”的事务,他是已经存在的事务的一个真正的子事务,嵌套事务开始执行时,它将取得一个savepoint保存点,如果这个嵌套事务失败,将回滚到此savepoint。嵌套事务并不是一个全新的事务,它是外部事务的一部分,只有外部事务提交后,它才会被提交。
举例分析:
//服务A
A{
@Transactional(propagation = Propagation.REQUIRED)
methodA(){
B.methodB();
}
}
//服务B
B{
@Transactional(propagation = Propagation.NESTED)
methodB(){
}
}
①methodA执行时将新建一个新的事务,B.methodB()执行时,将挂起methodA的事务,并得到一个savepoint。新建一个属于methodA事务的子事务。methodA的事务一直挂起到methodB事务中的所有操作执行完毕后,继续执行。
②如果methodB()中发生异常,将回滚methodB的事务操作。 如果它抛出的异常被methodA捕获,methodA的事务仍有可能提交,否则methodA的事务将一起回滚。
③如果methodA()中发生异常,将一起回滚methodA事务以及它在methodB中新建的子事务。
枚举源码
package org.springframework.transaction.annotation;
public enum Propagation {
REQUIRED(0),//默认 有则新建 无则加入
SUPPORTS(1),//有则支持 无则非事务运行
MANDATORY(2),//必须有 否则异常
REQUIRES_NEW(3),//挂起当前,另起炉灶
NOT_SUPPORTED(4),//挂起当前,普通运行
NEVER(5),//坚决反对事务
NESTED(6);//嵌套父子 荣辱与共
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
rollbackFor & rollbackForClassName
概念
指定遇到时必须进行回滚的异常类型,可以为多个。
noRollbackFor & noRollbackForClassName
概念
指定遇到时不回滚的异常类型,可以为多个。
timeout & readonly
为什么要有超时和只读属性
由于事务可以在行和表上获得锁,因此事务会占用资源,并且对整体性能产生影响,如果一个事务只读取数据但不做修改,数据库引擎就可以对这个事务进行优化。
概念
timeout(超时时间,int,单位秒):事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
readonly(事务的读写性,boolean):表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务。
注意事项
正确配置propagation属性
有些传播机制配置后可以不启动事务,如果期望目标方法进行事务管理则不能配置以下三种:
①SUPPORTS 存在则加入,不存在则以非事务运行。不启动事务
②NOT_SUPPORTS 不支持事务运行,如果当前处在事务中则挂起当前事务,然后以非事务方式运行完,然后恢复当前事务。
③NEVER 以非事务方式运行,如果当前存在事务则抛异常。
注意正确设置rollbackFor,保证发生异常后能正确回滚。
默认情况下,事务中抛出 继承自 RuntimeException 的异常或者Error,Spring将回滚事务。其他异常将不会回滚。
如果期望事务中抛出其他类型的异常,并希望Spring能够回滚事务
可以指定 rollbackFor。通过源码分析可知,如果目标方法中抛出的rollbackFor指定的异常的子类,事务同样会回滚。
private int getDepth(Class<?> exceptionClass, int depth) {
if (exceptionClass.getName().contains(this.exceptionName)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionClass == Throwable.class) {
return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}
@Transactional注解只能应用到public方法才能生效。
因为在使用Spring AOP代理时,Spring在调用TransactionInterceptor在目标方法执行前后进行拦截之前,会通过DynamicAdviseInterceptor(CglibAopProxy的内部类)的intercept方法或者jdkDynamicAopProxy的invoke方法,这两个方法会间接调用AbstractFallbackTransactionAttributeSource(Spring通过这个类获取@Transactional注解的事务属性配置信息)的computeTransactionAttribute方法。而computeTransactionAttribute会检查目标方法是不是public修饰,如果不是则不会获取@Transactional的属性配置信息,最终造成不会对目标方法进行拦截并进行事务控制。
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;}
避免Spring的AOP自调用问题
在Spring 的Aop代理下,只有目标方法由外部调用,目标方法才由Spring生成的代理对象来管理。如果一定要解决,可以使用AspectJ取代SpringAop代理,具体如何取代就不展开了。
以下为自调用问题代码举例:
transationalMethod方法虽然有@Transactional注解,但是它被内部方法insert调用,事务会被忽略,出现异常后事务不会回滚。
@Service
public class Service{
private void insert(){
transationalMethod();
}
@Transactional
public void transationalMethod(){
//insert
//update
//...
}
}
可能遇到的问题
1 @Transtional注解在什么情况下会失效?
① 注解修饰的非public方法
②错误配置了rollbackFor,导致抛出非RuntimeException后事务不会正常回滚
③传播机制propagation设置错误。三种 supports\not_supported\never
④同一个类中方法调用,即Spring Aop的自调用。
⑤异常被catch吃掉,导致没有抛出,事务不会回滚。
⑥数据库引擎不支持事务。比如mysql默认用的InnoDB引擎,但是如果切换为myisam引擎,事务则彻底失效。
2 现在有两个不同的服务A、B各有methodA和methodB,此时methodA中调用了methodB,methodA上加了@Transactional注解,事务默认隔离级别,传播机制为REQUIRED;methodB上也加了同样的@Transactional注解。然后运行到methodB时发生异常,此时methodB的操作会不会被回滚?methodA的操作会不会被回滚?为什么
//服务A
A{
@Transactional(propagation = Propagation.REQUIRED)
methodA(){
B.methodB();
}
}
//服务B
B{
@Transactional(propagation = Propagation.REQUIRED)
methodB(){
}
}
由于传播机制时REQUIRED,methodA交给事务管理,methodB将加入到methodA的事务中,无论methodA或者methodB哪个地方发生异常,methodB和methodA都将视为一个整体事务回滚。
3 上述场景 ,假设methodB的传播机制被调整为REQUIRED_NEW,如果此时methodB的异常抛出到methodA中被捕获了,methodA会不会回滚?为什么
//服务A
A{
@Transactional(propagation = Propagation.REQUIRED)
methodA(){
B.methodB();
}
}
//服务B
B{
@Transactional(propagation = Propagation.REQUIRED_NEW)
methodB(){
}
}
由于传播机制时REQUIRED_NEW所以methodB的操作将启动一个新的事务,methodB发生异常只会回滚自身事务的操作,如果methodB抛出的异常在methodA中被捕获而没有抛出,methodA的操作将不会回滚。