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

相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
相关文章
新闻发布项目——注册页面(reg.jsp)
新闻发布项目——注册页面(reg.jsp)
|
监控 数据可视化 关系型数据库
微服务架构+Java+Spring Cloud +UniApp +MySql智慧工地系统源码
项目管理:项目名称、施工单位名称、项目地址、项目地址、总造价、总面积、施工准可证、开工日期、计划竣工日期、项目状态等。
452 6
|
设计模式 JSON 前端开发
SpringBoot中对LocalDateTime进行格式化并解析
SpringBoot中对LocalDateTime进行格式化并解析
1229 0
|
11月前
|
Kubernetes Docker 容器
掌握Docker容器化技术:从入门到实战
掌握Docker容器化技术:从入门到实战
170 0
|
11月前
|
人工智能 Scala Kotlin
Kotlin - 运算符与中缀表达式
Kotlin - 运算符与中缀表达式
53 6
|
Linux
09Linux - 文件管理(输出重定向命令:>)
09Linux - 文件管理(输出重定向命令:>)
66 0
|
关系型数据库 MySQL Linux
linux篇-linux 主从配置
linux篇-linux 主从配置
183 0
linux篇-linux 主从配置
|
安全 开发者
最酷的黑客马拉松地点?30000英尺的高空
说到黑客马拉松,你很容易想起这些:拿着不放的手机,随处开着放在桌上的笔记本,当然还有互联网连接。但是英国航空公司”不接地创新实验室“中的开发者可没有这些东西,他们在空中进行了11个小时的黑客马拉松。
269 0
最酷的黑客马拉松地点?30000英尺的高空
|
弹性计算 Windows CDN
如何选择阿里云服务器相关配置
什么配置的阿里云服务器是适合自己的呢?下面我们就来说说如何选择阿里云服务器配置
217 0
如何选择阿里云服务器相关配置