理解事务
在软件开发领域,全有或全无的操作被称为事务(transaction)。事务允许你将几个操作组合成一个要么全部发生要么全部不发生的工作单元。传统上 Java EE 开发对事务管理有两种选择:全局事务或本地事务,两者都有很大的局限性。
事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID。
快速入门
创建 Service 接口实现类 TransactionServiceA 类,在 methodA()
方法中先往 tablea 表格插入一条数据,随后会调用 TransactionServiceB 服务的 methodB()
方法往 tableb 表格中插入一条数据。
1 |
|
假设业务需求是:往 tablea 和 tableb 插入的数据,要么都完成,要么都不完成。
这时候,应该怎么操作呢?
首先,需要在 TransactionServiceA 类的 methodA() 方法上配置 @Transactional
注解,同时也在 TransactionServiceB 类的 methodB() 方法上配置 @Transactional
注解。修改之后的 TransactionServiceA 和 TransactionServiceB 代码如下所示。
1 | // TransactionServiceA |
在 methodB() 中模拟了业务异常, tablea 和 tableb 都没有插入数据,此时就是事务起作用了
事务传播类型
事务传播类型,指的是事务与事务之间的交互策略。
例如:在事务方法 A 中调用事务方法 B,当事务方法 B 失败回滚时,事务方法 A 应该如何操作?这就是事务传播类型。
Spring 事务中定义了 7 种事务传播类型,分别是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。
其中最常用的只有 3 种,即:REQUIRED、REQUIRES_NEW、NESTED。
针对事务传播类型,要弄明白的是 4 个点:
- 子事务与父事务的关系,是否会启动一个新的事务?
- 子事务异常时,父事务是否会回滚?
- 父事务异常时,子事务是否会回滚?
- 父事务捕捉异常后,父事务是否还会回滚?
REQUIRED
REQUIRED 是 Spring 默认的事务传播类型,该传播类型的特点是:当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。
为了验证 REQUIRED 事务传播类型的特点,来做几个测试:
还是上面 methodA 和 methodB 的例子。当 methodA 不开启事务,methodB 开启事务,这时候 methodB 就是独立的事务,而 methodA 并不在事务之中。因此当 methodB 发生异常回滚时,methodA 中的内容就不会被回滚。
用如下的代码就可以验证我们所说的:
1 | public void methodA(){ |
最终的结果是:tablea 插入了数据,tableb 没有插入数据,符合了我们的猜想。
当 methodA 开启事务,methodB 也开启事务。按照结论,此时 methodB 会加入 methodA 的事务。此时,验证当父子事务分别回滚时,另外一个事务是否会回滚。
先验证第一个:当父方法事务回滚时,子方法事务是否会回滚?
1 |
|
结果是:talbea 和 tableb 都没有插入数据,即:父事务回滚时,子事务也回滚了。
继续验证第二个:当子方法事务回滚时,父方法事务是否会回滚?
1 |
|
结果是:talbea 和 tableb 都没有插入数据,即:子事务回滚时,父事务也回滚了。
继续验证第三个:当子方法事务回滚时,父方法捕捉了异常,父方法事务是否会回滚?
1 |
|
结果是:talbea 和 tableb 都没有插入数据,即:子事务回滚时,父事务也回滚了。所以说,这也进一步验证了之前所说的:REQUIRED 传播类型,它是父子方法共用同一个事务的。
REQUIRES_NEW
REQUIRES_NEW 也是常用的一个传播类型,该传播类型的特点是:无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。 为了验证 REQUIRES_NEW 事务传播类型的特点,来做几个测试。
首先,来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?
1 |
|
结果是:tablea 没有插入数据,tableb 插入了数据,即:父方法事务回滚了,但子方法事务没回滚。这可以证明父子方法的事务是独立的,不相互影响。
下面,来看看:当子方法事务发生异常时,父方法事务是否会回滚?
1 |
|
结果是:tablea 没有插入了数据,tableb 没有插入数据。
从这个结果来看,貌似是子方法事务回滚,导致父方法事务也回滚了。但不是说父子事务都是独立的,不会相互影响么?怎么结果与此相反呢?
其实是因为子方法抛出了异常,而父方法并没有做异常捕捉,此时父方法同时也抛出异常了,于是 Spring 就会将父方法事务也回滚了。如果在父方法中捕捉异常,那么父方法的事务就不会回滚了,修改之后的代码如下所示。
1 |
|
结果是:tablea 插入了数据,tableb 没有插入数据。这正符合刚刚所说的:父子事务是独立的,并不会相互影响。
这其实就是上面所说的:父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。因为如果执行过程中发生 RuntimeException 异常和 Error 的话,那么 Spring 事务是会自动回滚的。
NESTED
NESTED 也是常用的一个传播类型,该方法的特性与 REQUIRED 非常相似,其特性是:当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。
可以看到 NESTED 与 REQUIRED 的区别在于:父方法与子方法对于共用事务的描述是不一样的,REQUIRED 说的是共用同一个事务,而 NESTED 说的是在嵌套事务执行。这一个区别的具体体现是:在子方法事务发生异常回滚时,父方法有着不同的反应动作。
对于 REQUIRED 来说,无论父子方法哪个发生异常,全都会回滚。而 REQUIRED 则是:父方法发生异常回滚时,子方法事务会回滚。而子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。
为了验证 NESTED 事务传播类型的特点,来做几个测试。
首先,来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?
1 |
|
结果是:tablea 和 tableb 都没有插入数据,即:父子方法事务都回滚了。这说明父方法发送异常时,子方法事务会回滚。
接着,继续验证一下:当子方法事务发生异常时,如果父方法没有捕捉异常,父方法事务是否会回滚?
1 |
|
结果是:tablea 和 tableb 都没有插入数据,即:父子方法事务都回滚了。这说明子方法发送异常回滚时,如果父方法没有捕捉异常,那么父方法事务也会回滚。
最后,验证一下:当子方法事务发生异常时,如果父方法捕捉了异常,父方法事务是否会回滚?
1 |
|
结果是:tablea 插入了数据,tableb 没有插入数据,即:父方法事务没有回滚,子方法事务回滚了。这说明子方法发送异常回滚时,如果父方法捕捉了异常,那么父方法事务就不会回滚。
Spring事务失效
什么时候 Spring 事务会失效?
- 若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务会失效。这是由于 Spring AOP 代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。
- 另外,如果直接调用,不通过对象调用,也是会失效的。因为 Spring 事务是通过 AOP 实现的。
- @Transactional 注解只有作用到 public 方法上事务才生效。
- 被 @Transactional 注解的方法所在的类必须被 Spring 管理。
- 底层使用的数据库必须支持事务机制,否则不生效。
彩蛋
Spring 事务执行过程中,如果抛出非 RuntimeException 和非 Error 错误的其他异常,那么是不会回滚的哦。例如下面的代码执行后,tablea 和 tableb 两个表格,都会插入一条数据。
1 |
|