目录
- 一、事务管理方式
- 二、事务提交方式
- 三、事务隔离级别
- 四、事务传播行为
- 1、Propagation.REQUIRED
- 2、Propagation.SUPPORTS
- 3、Propagation.MANDATORY
- 4、Propagation.REQUIRES_NEW
- 5、Propagation.NOT_SUPPORTED
- 6、Propagation.NEVER
- 7、Propagation.NESTED
- 五、事务回滚
- 六、只读事务
- 七、解决Transactional注解不回滚
- 1、检查你方法是不是public的
- 2、检查是不是通过类内部调用事务方法
- 3、检查抛出的异常类型是不是unchecked异常
- 4、数据库引擎是否支持事务
- 5、异常是不是被你catch住了
- 6、新开启一个线程
一、事务管理方式
在Spring中,事务有两种实现方式:编程式事务管理
和 声明式事务管理
。
-
编程式事务管理
:编程式事务管理使用TransactionTemplate
或者直接使用底层的PlatformTransactionManager
。对于编程式事务管理,Spring推荐使用TransactionTemplate
。 -
声明式事务管理
:建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务管理
不需要入侵代码,通过注解 @Transactional
就可以进行事务操作,更快捷而且简单,推荐使用。
二、事务提交方式
默认情况下,数据库处于自动提交模式
。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。
对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,Spring会将底层连接的自动提交特性设置为false。也就是在使用Spring进行事物管理的时候,Spring会将是否自动提交设置为false
,等价于JDBC中的 connection.setAutoCommit(false);
,在执行完之后在进行提交 connection.commit();
。
三、事务隔离级别
隔离级别:指若干个并发的事务之间的隔离程度
。@Transactional
注解的 isolation
属性,可用来设置隔离级别。默认值为 Isolation.DEFAULT
。Isolation枚举了多种隔离级别:
隔离级别 | 说明 |
---|---|
Isolation.DEFAULT | 这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是 TransactionDefinition.ISOLATION_READ_COMMITTED 。 |
Isolation.READ_UNCOMMITTED | 该隔离级别表示 一个事务可以读取另一个事务修改但还没有提交的数据 。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。 |
Isolation.READ_COMMITTED | 该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。 |
Isolation.REPEATABLE_READ | 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。 |
Isolation.SERIALIZABLE | 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 |
四、事务传播行为
事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。@Transactional
注解的 propagation
属性可用来设置事务传播行为,默认值为 Propagation.REQUIRED
,Propagation
枚举了多种事务传播模式,我们以一个简单的例子来说明事务是如何传播的。
public class ServiceA {
public void methodA() {
...
serviceB.methodB();
...
}
}
...
public class ServiceB {
// 通过propagation属性指定methodB方法的事务传播行为
@Transactional(propagation = ...)
public void methodB() {
...
}
}
上面的代码中指定 methodB()
方法的不同事务传播属性值,根据 methodA()
是否开启事务,来说明 methodB()
方法的事务如何传播的。
1、Propagation.REQUIRED
业务方法需要在一个容器里运行。如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务。
2、Propagation.SUPPORTS
该方法在某个事务范围内被调用,则方法成为该事务的一部分。如果方法在该事务范围外被调用,该方法就在没有事务的环境下执行。
3、Propagation.MANDATORY
该方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果在没有事务的环境下被调用,将抛出IllegalTransactionStateException异常,此时不仅 methodB()
方法无法得到执行,也会打断 methodA()
方法的执行流程,除非在 methodA()
方法中捕获处理该异常。
4、Propagation.REQUIRES_NEW
不管是否存在事务,该方法总汇为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务挂起,新的事务被创建。
5、Propagation.NOT_SUPPORTED
声明方法不需要事务。如果方法没有关联到一个事务,容器不会为他开启事务,如果方法在一个事务中被调用,该事务会被挂起,调用结束后,原先的事务会恢复执行。
6、Propagation.NEVER
该方法绝对不能在事务范围内执行。如果在,则抛出 IllegalTransactionStateException
异常,此时不仅 methodB()
方法无法得到执行,还会打断 methodA()
方法的执行流程,甚至导致 methodA()
方法发生回滚。
7、Propagation.NESTED
如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。
例如:
-
一旦
methodA()
方法进行回滚,则methodB()
方法也会进行回滚。但由于该传播行为是通过数据库事务的保存点进行实现的,那么一旦methodB()
方法抛出异常发生回滚。 -
如果
methodA()
方法捕获了methodB()
方法所抛出的异常,则methodA()
就不会因此而回滚;而methodA()
方法如果继续向上抛出异常则其也会被回滚。
五、事务回滚
@Transactional
注解默认只会对 unchecked异常进行回滚
,checked异常则不回滚
。
-
unchecked异常
:RuntimeException(比如空指针,1/0)、Error及其子类; -
checked异常
:继承自java.lang.Exception得异常,如IOException、TimeoutException等;
如果期望对检查异常进行回滚,可通过 rollbackFor
、rollbackForClassName
属性添加新的回滚条件:
// 方式1: 支持对所有异常类型进行回滚
@Transactional(rollbackFor = Exception.class)
// 方式2:支持对所有异常类型进行回滚
@Transactional(rollbackForClassName = {"Exception"})
类似地,还可以排除某些异常,使之不发生回滚:
// 方式1: 抛出ArithmeticException异常不进行回滚
@Transactional(noRollbackForClassName = {"ArithmeticException"} )
// 方式2: 抛出ArithmeticException异常不进行回滚
@Transactional(noRollbackFor = {ArithmeticException.class} )
六、只读事务
@Transactional
注解的 readOnly
属性默认为false,如需只读事务可将其配置为true。在只读事务中不允许执行读之外操作,否则事务将会回滚。
@Transactional(readOnly=true)
启动事务会增加线程开销,数据库因共享读取而锁定(具体跟数据库类型和事务隔离级别有关)。通常情况下,仅是读取数据时,不必设置只读事务而增加额外的系统开销
。
七、解决Transactional注解不回滚
1、检查你方法是不是public的
IDEA 直接会给出提示 Methods annotated with ‘@Transactional’ must be overridable
,原理很简单,@Transactional
注解只能应用到 public
可见度的方法上。 如果应用在 protected
、private
或者 package
可见度的方法上,也不会报错,不过事务设置不会起作用。
@Transactional
private void deleteUser() throws MyException{
userMapper.deleteUserA();
userMapper.deleteUserB();
}
2、检查是不是通过类内部调用事务方法
在A类内部通过一个普通方法 methodA()
调用事务方法 methodB()
,那么 methodB()
的事务会生效么?
public class A {
public void methodA() {
...
methodB();
...
}
@Transactional
public void methodB() {
...
}
}
答案是 否
,原因很简单。
这里我们将 Spring AOP
后的动态代理类 ProxyA
用伪代码的形式给出,如下所示。可以看到,虽然动态代理类 ProxyA
中的 methodB()
方法被加入了事务切面,但事实上调用 ProxyA
的methodA()
方法后,会直接进入目标类A中,即执行 a.methodA()
方法,然后直接调用A类中的methodB()
方法。换言之,methodB()
方法没有通过代理类 ProxyA
进行调用,自然其事务注解不会生效。
public class ProxyA {
private A a = new A();
public void methodA() {
// 执行目标方法
a.methodA();
}
public void methodB() {
// 前置增强
...
// 执行目标方法
a.methodB();
// 后置增强
...
}
}
即使在A类的 methodA()
上也添加 @Transactional
事务注解,methodB()
方法由于没走代理类ProxyA
,故 methodB()
方法依然还是使用 methodA()
方法的事务。即使将 methodB()
方法的传播行为设置为 Propagation.REQUIRES_NEW
,也不会重新开启一个新的事务。因为 methodB()
方法连 @Transactional
注解都无法生效,设置传播行为更是无任何意义。
3、检查抛出的异常类型是不是unchecked异常
异常虽然抛出了,但是抛出的是非RuntimeException类型的异常,依旧不会生效。
@Transactional
public void deleteUser() throws MyException{
userMapper.deleteUserA();
try {
int i = 1 / 0;
userMapper.deleteUserB();
} catch (Exception e) {
throw new MyException();
}
}
如果我想check异常也想回滚怎么办,注解上面写明异常类型即可(指定了回滚异常类型为Exception)
@Transactional(rollbackFor=Exception.class)
类似的还有norollbackFor,自定义不回滚的异常。
4、数据库引擎是否支持事务
如果是MySQL,注意表要使用支持事务的引擎,比如 InnoDB,如果是MyISAM,事务是不起作用的。
5、异常是不是被你catch住了
当异常被捕获后,并且没有再抛出,那么deleteUserA是不会回滚的。
@Transactional
public void deleteUser() {
userMapper.deleteUserA();
try {
int i = 1 / 0;
userMapper.deleteUserB();
} catch (Exception e) {
e.printStackTrace();
}
}
6、新开启一个线程
如下的方式 deleteUserA()
也不会回滚,因为spring实现事务的原理是通过 ThreadLocal
把数据库连接绑定到当前线程中,新开启一个线程获取到的连接就不是同一个了。
@Transactional
public void deleteUser() throws MyException{
userMapper.deleteUserA();
try {
//休眠1秒,保证deleteUserA先执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
int i = 1/0;
userMapper.deleteUserB();
}).start();
}