事务(2)
如果觉得写的还可以,点个赞支持一下笔者呗!你的点赞和关注会让我更快更新哦。笔者会持续更新关于Java和大数据有关的文章。目前集中精力在更新java框架的内容。
1. 书接上回
事务(1)中我们一起学习了事务。了解了事务的特性,事务的隔离级别,悲观锁和乐观锁,还详细的分析了脏读、幻读以及不可重复读,最后稍微提了一下 Spring 中的事务传播行为。这一小节我们将会通过具体的应用来更加深入的学习事务。
说到事务的应用,通常分为编程式事务和声明式事务。编程式事务,顾名思义就是将事务管理编写到我们的业务代码中,这样一来对代码有很强的侵入性;二来会编写大量的冗余代码。所以,目前基本上都是采用声明式事务。
2. Spring 中使用声明式事务
Spring 中的声明式事务是通过 AOP 方式实现的,有关 AOP 的内容,后面我们会详细的讨论。
要在 Spring 中开启事务非常的简单,只需要在相应的类或者方法上加上 @Transactional 注解即可。例如我们新建一个 UserService 类,在 insert 方法(调用 UserMapper )上加上 @Transactional 注解。当 insert 方法被调用时,控制台会打印如下日志:
Creating new transaction with name [com.xxx.UserService.insert]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
根据这条日志,我们可以看到在执行 insert 方法时,开启了一个事务。还可以看到传播行为是 REQUIRED,隔离级别是 DEFAULT。但是这里我们并没有去指定这两项内容,显然这是 Spring 为我们设置的默认值。传播行为、隔离级别都可以通过 @Transactional 的相应属性来进行自定义设置,接下来我们看一下 @Transactional 的源码:
package org.springframework.transaction.annotation; ...... @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { // 通过 bean name 设置事务管理器 @AliasFor("transactionManager") String value() default ""; // 同上 @AliasFor("value") String transactionManager() default ""; // 事务传播行为 Propagation propagation() default Propagation.REQUIRED; // 事务隔离级别 Isolation isolation() default Isolation.DEFAULT; // 事务超时时间(秒) int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; // 是否只读 boolean readOnly() default false; // 事务回滚的异常,默认所有异常都回滚 Class<? extends Throwable>[] rollbackFor() default {}; // 同上,按名称 String[] rollbackForClassName() default {}; // 事务不回滚的异常,默认所有异常都回滚 Class<? extends Throwable>[] noRollbackFor() default {}; // 同上,按名称 String[] noRollbackForClassName() default {}; }
@Transactional 的源码很简单,通过上面的注释理解起来应该没什么难度。使用 @Transactional 是声明式事务的方式,与编程式事务不同,声明式事务是通过一个标识来告诉 Spring 我要在这里开启事务了。所以这种需要双方配合才能完成的事情,在事前一定要做好相应的约定。我们通过下图来一起看看 Spring 是如何进行事务管理的:
如上图所示,在执行带有 @Transactional 标识的方法时,Spring 会为其开启事务,如果方法中的业务逻辑一切正常,那么就万事大吉,Spring 会 commit 提交事务并释放资源;如果在执行业务逻辑的时候不幸发生了异常(并且是需要事务回滚的异常类型),那么 Spring 则会 rollback 将事务进行回滚并释放资源。
3. Spring 中的事务传播行为
在上一节中我们了解到事务具有四个特性 ——ACID。其中 A 代表原子性,意思是一个事务要么成功(将结果写入数据库),要么失败(不对数据库有任何影响)。这种方式在一个事务单打独斗的时候是一个非常好的做法,但是如果在一个批量任务里(假设包含 1000 个独立的任务),前面的 999 个任务都非常顺利、完美、漂亮、酷毙且成功的执行了,等到执行最后一个的时候,结果这个任务非常悲催、很是不幸的失败了。这时候 Spring 对着前面 999 个成功执行的任务大手一挥说:兄弟们,我们有一个任务失败了,现在全体恢复原状!如果这样的话,那可真是「一顿操作猛如虎,定睛一看原地杵」。
显然,Spring 并不会这么简单粗暴的处理。在 Spring 中, 当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。上一小节我们了解到,Spring 为我们提供了七种传播行为的策略,通过枚举类 Propagation 定义,源码如下:
package org.springframework.transaction.annotation; import org.springframework.transaction.TransactionDefinition; public enum Propagation { /** * 需要事务,它是默认传播行为,如果当前存在事务,就沿用当前事务, * 去否则新建一个事务运行内部方法 */ REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED), /** * 支持事务,如果当前存在事务,就沿用当前事务, * 如果不存在,则继续采用无事务的方式运行内部方法 */ SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS), /** * 必须使用事务,如果当前没有事务,则会抛出异常, * 如果存在当前事务,则沿用当前事务 */ MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY), /** * 无论当前事务是否存在,都会创建新事务运行方法, * 这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立 */ REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW), /** * 不支持事务,当前存在事务时,将挂起事务,运行方法 */ NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED), /** * 不支持事务,如果当前方法存在事务,则抛出异常,否则继续使用无事务机制运行 */ NEVER(TransactionDefinition.PROPAGATION_NEVER), /** * 在当前方法调用内部方法时,如果内部方法发生异常, * 只回滚内部方法执行过的 SQL ,而不回滚当前方法的事务 */ NESTED(TransactionDefinition.PROPAGATION_NESTED); ...... }
接下来我们通过对其中三种最常用的(REQUIRED、REQUIRES_NEW、NESTED)策略进行对比来更深入的理解。以下测试均在外部方法开启事务的情况下进行,因为在外部没有事务的情况下,三者都会新建事务,效果一样。
3.1 REQUIRED
当内部方法的事务传播行为设置为 REQUIRED 时,内部方法会加入外部方法的事务,可以看到日志中有 **Participating in existing transaction ** 这样一行内容:
# 开启事务 Creating new transaction with name [com.imooc.springboot.transaction.service.TransactionService.batchInsert]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception ......... # 加入当前事务 Participating in existing transaction ......... # 提交事务 Initiating transaction commit
3.2 REQUIRES_NEW
当内部方法的传播行为设置为 REQUIRES_NEW 时,内部方法会先将外部方法的事务挂起,然后开启一个新的事务 可以看到日志中有 Suspending current transaction, creating new transaction… 这样一行内容:
# 开启外部事务 Creating new transaction with name [com.imooc.springboot.transaction.service.TransactionService.batchInsert]: PROPAGATION_REQUIRES_NEW,ISOLATION_DEFAULT,-java.lang.Exception .......... # 挂起当前事务,并开启新事务 Suspending current transaction, creating new transaction with name [com.imooc.springboot.transaction.service.UserService.insert] .......... # 提交内部方法事务 Initiating transaction commit # 唤醒被挂起的外部事务 Resuming suspended transaction after completion of inner transaction # 提交外部事务 Initiating transaction commit
3.3 NESTED
当内部方法的传播行为设置为 NESTED 时,内部方法会开启一个新的嵌套事务 可以看到日志中有 Creating nested transaction… 这样一行内容:
# 开启主事务 Creating new transaction with name [com.imooc.springboot.transaction.service.TransactionService.batchInsert]: PROPAGATION_NESTED,ISOLATION_DEFAULT,-java.lang.Exception .......... # 开启嵌套事务 Creating nested transaction with name [com.imooc.springboot.transaction.service.UserService.insert] .......... # 释放保存点 Releasing transaction savepoint # 提交事务 Initiating transaction commit
另外,每个 NESTED 事务执行前会将当前操作保存下来,叫做 savepoint (保存点),如果当前 NESTED 事务执行失败,则回滚到之前的保存点,以便之前的执行结果不受当前 NESTED 事务的影响。NESTED 事务在外部事务提交以后自己才会提交。
3.4 区别
REQUIRES_NEW 最为简单,不管当前有无事务,它都会开启一个全新事务,既不影响外部事务,也不会影响其他内部事务,真正的井水不犯河水,坚定而独立。
REQUIRED 在没有外部事务的情况下,会开启一个事务,不影响其他内部事务;而当存在外部事务的情况下,则会与外部事务还有其他内部事务同命运共生死。有条件会直接上,没条件是会自己创造条件,然后再上。
NESTED 在没有外部事务的情况下与 REQUIRED 效果相同;而当存在外部事务的情况下,则与外部事务生死与共,但与其他内部事务互不相干。要么孑然一身,要么誓死追随主公(外部事务)。
4. 拓展
在大部分数据库中,一段 SQL 语句中可以设置一个标志位,如果标志位后面的代码执行过程中发生异常,则只需回滚到这个标志位的数据状态,而不会让这个标志位之前的代码也回滚。这个标志位,在数据库的概念中被称为保存点(savepoint)。Spring 传播行为中 NESTED 策略就是利用数据库保存点的技术实现的。但需要注意的是,有一些数据库是不支持保存点的,这个时候 NESTED 策略就会像 REQUIRES_NEW 一样创建一个全新的事务(而非嵌套事务)。但此时二者仍有一些不同,NESTED 传播行为会沿用外部事务的隔离级别和锁等特性,而 REQUIRES_NEW 则可以拥有自己独立的隔离级别和锁等特性,这一点小区别在实际应用中要注意。