事务详解(2)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 你好看官,里面请!今天笔者讲的是事务。不懂或者觉得我写的有问题可以在评论区留言,我看到会及时回复。 注意:本文仅用于学习参考,不可用于商业用途,如需转载请跟我联系。

事务(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 则可以拥有自己独立的隔离级别和锁等特性,这一点小区别在实际应用中要注意。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
6月前
|
SQL 关系型数据库 MySQL
事务及事务的隔离级别
事务及事务的隔离级别
|
4月前
|
SQL Oracle 关系型数据库
第7章 事务
第7章 事务
27 0
|
6月前
|
存储 Java 中间件
事务一致性测试
事务一致性测试
52 0
|
6月前
|
SQL 前端开发 大数据
什么是大事务?以及大事务产生的问题
什么是大事务?以及大事务产生的问题
186 0
|
6月前
|
Java 数据库
JPA - EntityTransaction与事务
JPA - EntityTransaction与事务
60 0
|
人工智能 关系型数据库 MySQL
事务详解
事务是逻辑上的一组操作,要么都执行,要么都不执行。
71 0
|
存储 Oracle 固态存储
深入理解事务
事务将应用程序的多个读、写操作捆绑在一起成为一个逻辑执行单元。即事务中的所有读写是一个执行的整体,整 个事务要么成功(提交)、要么失败(中止 或者 回滚)。如果失败,应用程序可以安全地重试。
16432 0
深入理解事务
|
SQL 存储 关系型数据库
|
数据库
什么时候需要使用事务
什么时候需要使用事务
593 0
|
Oracle 安全 Java
事务详解(1)
你好看官,里面请!今天笔者讲的是事务。不懂或者觉得我写的有问题可以在评论区留言,我看到会及时回复。 注意:本文仅用于学习参考,不可用于商业用途,如需转载请跟我联系。
104 2