--洱涷zZ
多个事务之间是是隔离的,相互独立的,如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别,就可以解决这些问题。
存在问题:
脏读:一个事务读取到另一个事务没有提交的数据
栗子:问李四接了500,李四将事务隔离级别设置成读未提交,对张三进行转账后但是没有提交,张三查看自己确实多了500,然后给李四写了借条,李四收到借条并将数据回滚,张三的500又回到李四账户,张三还得给李四还500。
不可重复读(虚读):在同一个事务中,两次读取到的数据不一样
幻读:一个事务操作(DML)数据表中的所有记录,另一个事务添加了一条数据,则第一个事务查询不到自己的修改。
隔离级别:
1.read uncommitted:读未提交
产生的问题:脏读,虚读,幻读
2.read committed:读已提交(Oracle默认的)
产生的问题:虚读,幻读
3.repeatable read:可重复读(MySQL默认的)
产生的问题:幻读
4.serializable:串行化(其实就是锁表 当一个事务在操作这个表的时候,其他事务是无法操作这个表的)可以解决所有的问题。
注意:隔离级别从小到大 安全性越来越高,但是效率越来越低
事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。
spring支持编程式事务管理和声明式事务管理两种方式。
编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。
当@Transactional作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
在项目中,@Transactional(rollbackFor=Exception.class),如果类加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚
@Transactional属性:
属性 | 类型 | 描述 |
---|---|---|
value | String | 可选的限定描述符,指定使用的事务管理器 |
propagation | enum: Propagation | 可选的事务传播行为设置 |
isolation | enum: Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 读写或只读事务,默认读写 |
timeout | int (in seconds granularity) | 事务超时时间设置 |
rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 |
rollbackForClassName | 类名数组,必须继承自Throwable | 导致事务回滚的异常类名字数组 |
noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 |
noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 |
在Spring中对于事务的传播行为定义了七种类型分别是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。
Spring中事务的默认实现使用的是AOP,也就是代理的方式。
先创建一个事务应用的场景
public void testMain(){
A(a1);
testB();
}
public void testB(){
B(b1);
throw Exception;
B(b2);
}
如果说给这两个方法都没有添加事务,那么此时运行testMain()方法,会发生什么情况呢,a1,b1分别存入对应的数据库表tableA1,tableB1,然后抛出异常,程序停止执行,b2不会被插入tableB2
此种事务传播类型是Spring的默认事务传播类型
意思是:如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入
举栗子:
@Transational(propagation=Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
}
@Transational(propagation=Propagation.REQUIRED)
public void testB(){
B(b1);
throw Exception;
B(b2);
}
此场景下,此时数据库表数据为空,如果执行testMain(),先插入a1,然后插入b1,然后方法抛出异常后,事务发生回滚,因为testMain已经有事务,所以testB就直接加入了testMain的事务,那么当前testMain和testB是同一个事务,testB抛出异常事务回滚,testMain也会发生事务回滚,那么此时数据库表还是为空,所有数据都没有插入。
再举个栗子:
public void testMain(){
A(a1);
testB();
}
@Transational(propagation=Propagation.REQUIRED)
public void testB(){
B(b1);
throw Exception;
B(b2);
}
此场景下,testB有事务,testMain没有事务,那么在调用testMain时,testB判断testMain没有事务后自己就会新建一个事务,此时调用testMain,在testB抛出异常后,testB会发生回滚,而testMain就不会,所以此时a1就会被插入,b1会插入后因为事务回滚消失,b1,b2都不会被插入。
意思是:创建一个新事务,如果存在当前事务,则挂起该事务
举栗子:为了说明requires_new会开启一个新事务,我把异常的位置放在testMain,然后给testMain声明事务,传播类型为required,testB也设置事务,传播方式为requires_new,如下
@Transational(propagation=Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
throw Exception;
}
@Transational(propagation=Propagation.REQUIRES_NEW)
public void testB(){
B(b1);
B(b2);
}
此时调用testMain,a1不会被插入,b1,b2会被插入,因为testMain和testB是两个事务,testMain抛出异常之后,事务回滚并不会影响到testB。
与该场景相对应的就是两个传播方式都是REQUIRED,那么上面执行时,数据都不会被存储,因为是同一个事务,事务发生回滚时,所有数据都会发生回滚。
如果说异常抛出点还是在testB的b1插入后,那么这个栗子,运行后结果是a1,b1,b2都不会被存储成功,此时并不是因为是一个事务所以一回滚都回滚,而是testB抛出异常后,testMain检测到它调用的方法出现异常,那么它也会回滚。
意思是:如果当前存在事务,那么就加入该事务,如果不存在事务,就以非事务方法执行
举栗子:
public void testMain(){
A(a1);
testB();
}
@Transational(propagation=Propagation.SUPPORTS)
public void testB(){
B(b1);
throw Exception;
B(b2);
}
此时只在testB上声明事务传播机制为SUPPORTS,testMain无事务,当调用testMain时,a1存储,然后调用testB,testB判断testMain无事务,所以它也会按照无事务方法执行,结果就是a1,b1都被存入数据库,b2没有存入,因为抛出异常时,程序已停止运行,b2未被执行。
那么当我们在testMain上声明事务传播机制为REQUIRED,testB上声明事务传播机制为SUPPORTS,此时testB就会加入testMain的事务,最终结果就是a1,b1,b2都没有被存储。
意思是:始终以非事务方式执行,如果当前存在事务,则挂起该事务
可以理解为设置事务传播类型为NOT_SUPPORTED的方法,在执行时,不管调用它的方法事务传播机制是啥,它都会按照非事务方法执行
举个栗子:
@Transational(propagation=Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
}
@Transational(propagation=Propagation.NOT_SUPPORTED)
public void testB(){
B(b1);
throw Exception;
B(b2);
}
调用testMain,最终结果为只有b1存储成功,因为testB是按照非事务方法执行,所以b1存储成功,b2未存储,此时testMain事务传播机制为REQUIRED,其检测到调用有异常被抛出,因为testMain和testB不是一个事务,所以只有testMain会发生回滚,a1也不会被存储成功。
意思是:如果当前存在事务,那么就加入,如果当前事务不存在,则抛出异常
举个栗子
public void testMain(){
A(a1);
testB();
}
@Transactional(propagation = Propagation.MANDATORY)
public void testB(){
B(b1);
throw Exception;
B(b2);
}
这种场景下最终结果为a1插入成功,b1,b2没有插入,此时未插入并不是因为testB数据回滚,而是因为testMain没有声明事务,所以testB会直接抛出事务要求的异常,所以testB中的方法就没执行
如果testMain声明了事务,且设置为REQUIRED,那么在调用testB时就会正常的数据回滚,a1,b1,b2都不会被存储。
意思是:不使用事务,如果当前事务存在则抛出异常
很容易理解,就是我这个方法不使用事务,并且调用我的方法也不允许有事务,如果调用我的方法有事务则我直接抛出异常。
举个栗子:
@Transational(propagation=Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
}
@Transational(propagation=Propagation.NEVER)
public void testB(){
B(b1);
B(b2);
}
此种场景运行testMain会直接爆出异常,因为testB的事务传播机制是不允许调用它的事务存在事务的,此时testMain检测发现方法内部有异常就会发生回滚,所以数据库数据是没有发生更新的。
意思是:如果当前事务存在,那就在事务中进行嵌套执行,如果不存在,则开启一个事务,传播机制为REQUIRED。
REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。
在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。
REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚
而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响
举个栗子:
@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
throw Exception;
}
@Transactional(propagation = Propagation.NESTED)
public void testB(){
B(b1);
B(b2);
}
该场景下,所有数据都不会存入数据库,因为在testMain发生异常时,父事务回滚则子事务也跟着回滚了。
再举个栗子:
@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
A(a1);
try{
testB();
}catch(Exception e){
}
A(a2);
}
@Transactional(propagation = Propagation.NESTED)
public void testB(){
B(b1);
throw Exception;
B(b2);
}
这种场景下,结果是a1,a2存储成功,b1和b2存储失败,因为调用方catch了被调方的异常,所以只有子事务回滚了。
同样的代码,如果我们把testB的传播类型改为REQUIRED,结果也就变成了:没有数据存储成功。就算在调用方catch了异常,整个事务还是会回滚,因为,调用方和被调方共用的同一个事务。