Spring事务畅谈 —— 由浅入深彻底弄懂 @Transactional注解(2)

简介: Spring事务畅谈 —— 由浅入深彻底弄懂 @Transactional注解

3. 隔离级别

不同于”传播规则“是Spring提到的概念。隔离级别这个概念是数据库事务自带的,其目的是当存在多个事务时,如果这些事务访问到了共同的资源,该怎么处理事务间的数据隔离。一般来说数据库存在四种隔离级别,Spring则是定义了五种。它们分别如下


  • DEFAULT 用数据库的设置隔离级别,数据库设置的是什么我就用什么
  • UNCOMMITTED:读未提交,最低隔离级别、事务未提交前,就可被其他事务读取
  • COMMITTED:读已提交,一个事务提交后才能被其他事务读取到
  • REPEATABLE_READ:可重复读,本事务会建立快照,在快照建立前其他事务提交的内容可见,快照后提交的内容不可见
  • SERIALIZABLE:序列化,事务必须一个执行完才允许建下个事务,隔离级别最高

这里面有两点需说明:


  1. 数据库事务的隔离级别有两种,一种是全局的,一种是会话的,与数据库建立连接后,可以为本会话设置隔离级别。如果不设置,则会使用数据库的隔离级别。这也就是Spring能指定隔离级别的原因,其实际上指定的是当前会话的隔离级别
  2. 数据库的隔离级别是数据库事务的重要内容,我们会在后续详解Mysql的时候,仔细说明各隔离级别的实现和其特点

4. 超时时间

我们前面说到,在@Transactional注解上可以填写超时时间,而它的单位是秒。但关于超时时间的设置有很多问题,或者说陷阱,希望大家通过学习后能够避开。


首先 timeout 的设置只适用于以下两种传播规则: REQUIRED 和 REQUIRES_NEW,其他的传播规则只允许超时默认值 -1 (即永不超时),否则会抛出异常


另外此处的超时时间,其实是在判断最后一个SQL执行前是否超时,我们以一个经典案例来说明

    @Transactional(timeout = 2)
    public void methodA(UserInfo info) {
        userDao.updateInfo(info);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    @Transactional(timeout = 2)
    public void methodB(UserInfo info) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        userDao.updateInfo(info);
    }

我们不难看出,事务限时2秒,但方法执行至少3秒。那结果会如何呢?这里methodA不会报错,methodB会报错,但是是在第三秒(即线程恢复,开始执行Sql前)才报的错。


其原因是事务开启时,会读取当前时间 和 timeout 的值,并将其相加得到事务“过期时间”,而在每次执行sql前则会获取到statement,此时才会判断当前时间是否已超期,超期才会抛出超时异常。也就是说,通俗的理解,只有在每次执行SQL前,才会判断下是否已超过事务限时。


5. 是否只读

这里的只读是对开发者的提示,并不意味着你标注只读就真的不能写更新语句。实际上你更新,出错后回滚等功能都还是正常的。甚至该标注也支持 SUPPORTS 传播规则,说明即使没有开启事务也无妨。


我们可以看原文注释


This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts.A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction.return

这只是作为实际事务子系统的提示,它不一定会导致写访问尝试失败。不能理解只读提示的事务管理器在请求只读事务时也不会抛出异常


6. 回滚 / 不回滚异常类型

这里就是说你可以显式的指定什么异常需要回滚,什么异常不需要回滚。比方说我们有一种邮件通知异常,代表通知邮件发送失败,但这种异常无伤大雅,只是非核心功能,失败了不需要导致整个业务失败。此时我们可以指定这种异常不需要回滚事务。


我们可以看看提到的默认需要回滚的异常


By default, a transaction will be rolling back on RuntimeException and Error but not on checked exceptions business exceptions

默认情况下,事务将在RuntimeException和Error上回滚,但不会在受检异常(业务异常)上回滚


五、@Transactional 的功能实现

1. 使用代理的好处

@Transactional 注解之所以好用,是因为对开发者来说,不需要写额外的控制代码。然而,我们知道对事务的控制必不可少,而这部分代码其实全都被放进了“代理”中,我们如果把没有代理的Bean比作小鸡,那么普通小鸡,和被代理的小鸡的工作模式如下图

9d02b865603b4abda7c841ae622cd002.png


被代理过的小鸡,能够执行代理对象里给定的方法,同时也能继续调用小鸡本身的方法,这就是Spring的重要特性 —— AOP,此处就是利用代理对原有的功能进行了增强,在增强的方法里加入了关于事务管理的内容


2. 启用事务与创建代理

我们可以利用@EnableTransactional注解来启用事务,其原因就是使用该注解后,Spring会创建一个针对事务增强的Advisor ,该Advisor可以扫描标注有@Transctional的地方。同时,Spring还启用了一个可以利用Advisor来创建代理的 后置处理器,这样在Bean被创建时,后置处理器发挥作用,开始搜寻所有Advisor(包括我们这个事务的advisor),最终为这个Bean创建出代理对象,这个代理对象,就能进行事务的配置


其具体流程。可以见下图

fd195985942d4854b9242a0d2b39deed.jpg


3. 代理功能及要点

我们上面看了,一个带有事务功能的代理对象被创建,那么这个代理到底干了些什么呢?是怎么管理事务的呢?如果按上面的流程图,此处的拦截器为TransactionInterceptor,其增强部分的方法为TransactionAspectSupport.invokeWithinTransaction


其源码如下:

  protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
      throws Throwable {
    // 如果事务的属性为空,代表这个方法不需要事务
    final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
    // 根据注解里的属性选择事务管理器
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
      // 使用getTransaction和commit/rerollback调用进行标准事务划分。
      // 如果有需要,则创建事务
      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
      Object retVal = null;
      try {
        // This is an around advice: Invoke the next interceptor in the chain.
        // This will normally result in a target object being invoked.
        retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
        // target invocation exception
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
      }
      finally {
        cleanupTransactionInfo(txInfo);
      }
      // 方法执行完,返回后提交事务
      commitTransactionAfterReturning(txInfo);
      return retVal;
    }
    else {
      // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
      try {
        Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr,
            new TransactionCallback<Object>() {
              @Override
              public Object doInTransaction(TransactionStatus status) {
                TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
                try {
                  return invocation.proceedWithInvocation();
                }
                catch (Throwable ex) {
                  if (txAttr.rollbackOn(ex)) {
                    // A RuntimeException: will lead to a rollback.
                    if (ex instanceof RuntimeException) {
                      throw (RuntimeException) ex;
                    }
                    else {
                      throw new ThrowableHolderException(ex);
                    }
                  }
                  else {
                    // A normal return value: will lead to a commit.
                    return new ThrowableHolder(ex);
                  }
                }
                finally {
                  cleanupTransactionInfo(txInfo);
                }
              }
            });
        // Check result: It might indicate a Throwable to rethrow.
        if (result instanceof ThrowableHolder) {
          throw ((ThrowableHolder) result).getThrowable();
        }
        else {
          return result;
        }
      }
      catch (ThrowableHolderException ex) {
        throw ex.getCause();
      }
    }
  }

本文并不会针对源码层层展开,想看源码的上面已经给了具体的类和方法,可以自行阅览。这里只对关键部分进行表述。


3.1 基础功能

我们使用代理来实现事务,那么代理的基本功能或者说基本目的是什么呢?相信大家都明白,是在方法开始前新建一个事务 ,设置自动提交为否,方法结束后手动提交(上面代码已展示)

6bd314cf932a47ab88a27908fe3c5e04.png


3.2 ThreadLocal

我们看到前述方法 invokeWithinTransaction 中,有一步“如果需要则创建事务”,而这里面第一步就是要获取事务,更准确的说是获取对数据库的连接。


这里面就用到了threadLocal,每一个线程都会去“TransactionSynchronizationManager”(事务同步管理器)中获取连接(内部包含一个对数据库的连接)。因为threadLocal的特性,我们知道,连接对象其实是存储在线程内的。而连接是事务的根基,把连接存于线程内,随着线程去经历不同方法,这是Spring事务能在不同方法间传播的基础保证


关于 Threadlocal 的解析,可以看我的另一篇博文: 图解,深入浅出带你理解ThreadLocal


3.3 传播特性与回滚

从里我们知道,线程会先取出自己存着的事务对象(连接)。此处就有三种情况了:


  • 没有取到连接,即本线程还未连接数据库
  • 有连接但连接并没有开启事务
  • 有连接且已经开启事务

从事务角度看,就是两种情况,当前在事务中,或当前不在事务中。因此对于同一个传播属性就有两套逻辑了,举例来说,如果是@Transactional 里设置了传播属性是 REQUIRE,

那么如果当前有事务,则直接在这个事务内运行方法,如果当前没事务,就得新建事务,再在新建的事务内运行了


这里面比较特殊的是嵌套 NESTED 和 REQUIRED_NEW,为什么这么说?因为使用 这两种传播形式,即使当前已经有事务(连接),也必须新建一个事务(连接)。但我们说了,连接是以threadLocal的形式存在线程内的,而且只存了一个。那么这种情况,就会出现个问题: 新建事务(连接)后,原连接怎么处理?


对于 QUIRED_NEW 的处理,就是把当前信息的事务信息复制一份存起来并挂起,然后根据@Transacional注解里的属性重新填充当前事务信息,并重新取得一个连接。利用新的事务属性和新的连接去执行接下来的方法。因为是两个独立的连接与事务,所以外层方法和内部方法互不干涉,内部方法回滚,外部会毫无反应(除非你把异常往外抛,导致每层方法都吃到异常,那么每层都会回滚)

对于 NESTED 的处理,则利用了JDBC连接可以创建保存点的特性,会在此setSavePoint创建个保存点并保存下来。因为事实上用的同一个连接,中间有个保存点,所以一旦本方法内需要回滚,Spring会使用 rollback(savepoint) 恢复到本保存点,而不会全部回滚。但是如果是外部方法要回滚,就是全部回滚掉了

b1980699238c4092a81ae2c8426d7a92.jpg


目录
相关文章
|
10天前
|
Java 数据库 开发者
|
11天前
|
前端开发 Java 开发者
深入理解Spring Boot中的@Service注解
【4月更文挑战第22天】在 Spring Boot 应用开发中,@Service 注解扮演着特定的角色,主要用于标识服务层组件。本篇技术博客将全面探讨 @Service 注解的概念,并提供实际的应用示例,帮助开发者理解如何有效地使用这一注解来优化应用的服务层架构
52 1
|
11天前
|
Java 开发者 Spring
深入理解Spring Boot的@ComponentScan注解
【4月更文挑战第22天】在构建 Spring Boot 应用时,@ComponentScan 是一个不可或缺的工具,它使得组件发现变得自动化和高效。这篇博客将详细介绍 @ComponentScan 的基本概念、关键属性及其在实际开发中的应用。
26 4
|
12天前
|
Java 开发者 Spring
深入理解 Spring Boot 中的 @EnableAutoConfiguration 注解:概念与实践
【4月更文挑战第21天】在 Spring Boot 项目中,@EnableAutoConfiguration 注解是实现自动配置的核心,它可以根据项目的依赖和配置,自动地配置 Spring 应用程序中的 Bean
29 3
|
13天前
|
Java API 网络架构
深入理解 Spring Boot 中的 @RestController 注解:概念与实践
【4月更文挑战第20天】在现代Web开发中,创建RESTful服务已成为常态。Spring Boot通过提供@RestController注解,极大简化了REST API的开发过程。本篇博客旨在详细介绍@RestController的概念、优势以及在Spring Boot项目中的具体应用方法。
31 8
|
13天前
|
Java 开发者 Spring
Spring Framework 中的 @Autowired 注解:概念与使用方法
【4月更文挑战第20天】在Spring Framework中,@Autowired 注解是实现依赖注入(Dependency Injection, DI)的一种非常强大的工具。通过使用 @Autowired,开发者可以减少代码中的引用绑定,提高模块间的解耦能力
31 6
|
13天前
|
XML Java 数据库
探索 Spring Boot 中的 @Configuration 注解:核心概念与应用
【4月更文挑战第20天】在 Spring Boot 项目中,@Configuration 注解扮演了一个关键角色,它标识一个类作为配置源,这些配置用于定义和管理 Spring 应用程序中的 Bean
38 7
|
1天前
|
Java 数据库连接 数据库
16:事务-Java Spring
16:事务-Java Spring
12 5
|
2天前
|
消息中间件 Java 关系型数据库
Spring事务与分布式事务
这篇文档介绍了事务的概念和数据库事务的ACID特性:原子性、一致性、隔离性和持久性。在并发环境下,事务可能出现更新丢失、脏读和不可重复读等问题,这些问题通过设置事务隔离级别(如读未提交、读已提交、可重复读和序列化)来解决。Spring事务传播行为有七种模式,影响嵌套事务的执行方式。`@Transactional`注解用于管理事务,其属性包括传播行为、隔离级别、超时和只读等。最后提到了分布式事务,分为跨库和跨服务两种情况,跨服务的分布式事务通常通过最终一致性策略,如消息队列实现。
|
3天前
|
Java 测试技术 开发者
【亮剑】如何通过自定义注解来实现 Spring AOP,以便更加灵活地控制方法的拦截和增强?
【4月更文挑战第30天】通过自定义注解实现Spring AOP,可以更灵活地控制方法拦截和增强。首先定义自定义注解,如`@MyCustomAnnotation`,然后创建切面类`MyCustomAspect`,使用`@Pointcut`和`@Before/@After`定义切点及通知。配置AOP代理,添加`@EnableAspectJAutoProxy`到配置类。最后,在需拦截的方法上应用自定义注解。遵循保持注解职责单一、选择合适保留策略等最佳实践,提高代码可重用性和可维护性。记得测试AOP逻辑。