Spring事务及其注解

Spring事务相关

Spring事务是Spring框架中的重要知识点,本文针对之前秋招的学习成果进行简单总结。内容有点多,另外关于AOP动态代理的内容估计会另外写篇文章来总结了,建议看完后自己重新总结印象会比较深刻。


Spring事务的本质

Spring事务的本质是数据库对事务的支持。


Spring和事务的关系

关系型数据库、某些消息队列等产品或中间件称为事务性资源,因为它们本身支持事务,也能够处理事务。
Spring很显然不是事务性资源,但是它可以管理事务性资源,所以Spring和事务之间是管理关系。


Spring事务三要素

数据源:表示具体的事务性资源,是事务的真正处理者,如MySQL等。

事务管理器:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。

事务应用和属性配置:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属性如隔离级别、超时时间等。


Spring事务的注解配置

把一个DataSource(如DruidDataSource)作为一个@Bean注册到Spring容器中,配置好事务性资源。
把一个@EnableTransactionManagement注解放到一个@Configuration类上,配置好事务管理器,并启用事务管理。
把一个@Transactional注解放到类上或方法上,可以设置注解的属性,表明该方法按配置好的属性参与到事务中。


事务注解的本质

@Transactional这个注解仅仅是一些(和事务相关的)元数据,在运行时被事务基础设施读取消费,并使用这些元数据来配置bean的事务行为。
大致来说具有两方面功能,一是表明该方法要参与事务,二是配置相关属性来定制事务的参与方式和运行行为。


Spring事务的表达方式

spring支持编程式事务管理和声明式事务管理两种方式。

编程式事务 使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

声明式事务 是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。


Spring 事务属性分析

事务管理对于企业应用而言至关重要。它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。
在 Spring 中,事务是通过 TransactionDefinition 接口来定义的。该接口包含与事务属性有关的方法。
TransactionDefinition 接口中定义的主要方法

public interface TransactionDefinition{
    int getIsolationLevel();
    int getPropagationBehavior();
    int getTimeout();
    boolean isReadOnly();
}

接口只提供了获取属性的方法,而没有提供相关设置属性的方法。事务属性的设置完全是程序员控制的,因此程序员可以自定义任何设置属性的方法,而且保存属性的字段也没有任何要求。唯一的要求的是,Spring 进行事务操作的时候,通过调用以上接口提供的方法必须能够返回事务相关的属性取值。


事务的只读属性

事务的只读属性是指,对事务性资源进行只读操作或者是读写操作。所谓事务性资源就是指那些被事务管理的资源,比如数据源、 JMS 资源,以及自定义的事务性资源等等。如果确定只对事务性资源进行只读操作,那么我们可以将事务标志为只读的,以提高事务处理的性能。在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。


逻辑事务与物理事务

事务性资源实际打开的事务就是物理事务,如数据库的Connection打开的事务。Spring会为每个@Transactional方法创建一个事务范围,可以理解为是逻辑事务。
在逻辑事务中,大范围的事务称为外围事务,小范围的事务称为内部事务,外围事务可以包含内部事务,但在逻辑上是互相独立的。每一个这样的逻辑事务范围,都能够单独地决定rollback-only状态。
那么如何处理逻辑事务和物理事务之间的关联关系呢,这就是传播特性解决的问题。


事务的传播特性

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的**常量**:
• TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。在相同的线程中,这是一种很好的默认方式安排。(例如,一个service外观/门面代理到若干个仓储方法,所有底层资源必须参与到service级别的事务里)
在标准的REQUIRED行为情况下,所有这样的逻辑事务范围映射到同一个物理事务。因此,在内部事务范围设置了rollback-only标记,确实会影响外围事务进行实际提交的机会。
注:默认,一个参与到外围事务的事务,会使用外围事务的特性,安静地忽略掉自己的隔离级别,超时值,只读标识等设置。当然可以在事务管理器上设置validateExistingTransactions标识为true,这样当你自己的事务和参与到的外围事务设置不一样时会被拒绝。

• TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。与REQUIRED相比,总是使用一个独立的物理事务用于每一个受影响的逻辑事务范围,从来不参与到一个已存在的外围事务范围。这样安排的话,底层的事务资源是不同的,因此,可以独立地提交或回滚。外围事务不会被内部事务的回滚状态影响。这样一个独立的内部事务可以声明自己的隔离级别,超时时间和只读设置,并不继承外围事务的特性。

• TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。使用同一个物理事务,带有多个保存点,可以回滚到这些保存点,可以认为是部分回滚,这样一个内部事务范围触发了一个回滚,外围事务能够继续这个物理事务,尽管有一些操作已经被回滚。典型地,它对应于JDBC的保存点,所以只对JDBC事务资源起作用。

• TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。这样的一个逻辑事务范围,它背后可能没有实际的物理事务,此时的事务也成为空事务。

• TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 不支持当前事务,以非事务方式运行,如果当前存在事务,则把当前事务挂起,并在适合的时候恢复。

• TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

• TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

这里需要指出的是,前面的六种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而 PROPAGATION_NESTED是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚。


事务常出现的问题

脏读
一个事务修改了一行数据但没有提交,第二个事务可以读取到这行被修改的数据,如果第一个事务回滚,第二个事务获取到的数据将是无效的。
不可重复读
一个事务读取了一行数据,第二个事务修改了这行数据,第一个事务重新读取这行数据,将获得到不同的值。
幻读
一个事务按照一个where条件读取所有符合的数据行,第二个事务插入了一行数据且恰好也满足这个where条件,第一个事务再以这个where条件重新读取,将会获取额外多出来的这一行。


事务的隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:DEFAULT,READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ,SERIALIZABLE
• TransactionDefinition.ISOLATION_DEFAULT: 这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。MySQL的默认隔离级别是REPEATABLE-READ。

• TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据,即**读未提交**。脏读、不可重复读、幻读都会发生。

• TransactionDefinition.ISOLATION_READ_COMMITTED: 该隔离级别表示一个事务只能读取另一个事务已经提交的数据,即**读已提交**。可防止脏读,不可防止不可重复读和幻读,这也是大多数情况下的推荐值。

• TransactionDefinition.ISOLATION_REPEATABLE_READ: 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略,即**可重复读**。该级别可以防止脏读和不可重复读。

• TransactionDefinition.ISOLATION_SERIALIZABLE: 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,即**可串行化**。该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。


事务超时

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。


事务的回滚规则

通常情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常),则默认将回滚事务。如果没有抛出任何异常,或者抛出了已检查异常,则仍然提交事务。这通常也是大多数开发者希望的处理方式,也是 EJB 中的默认处理方式。但是,我们可以根据需要人为控制事务在抛出某些未检查异常时任然提交事务,或者在抛出某些已检查异常时回滚事务。


如何回滚一个事务

就是在一个事务上下文中当前正在执行的代码里抛出一个异常,事务基础设施代码会捕获任何未处理的异常,并且做出决定是否标记这个事务为回滚。


默认回滚规则

默认只把runtime, unchecked exceptions标记为回滚,即RuntimeException及其子类,Error默认也导致回滚。Checked exceptions默认不导致回滚。这些规则和EJB是一样的。


如何配置回滚异常

使用@Transactional注解的rollbackFor/rollbackForClassName属性,可以精确配置导致回滚的异常类型,包括checked exceptions。
noRollbackFor/noRollbackForClassName属性,可以配置不导致回滚的异常类型,当遇到这样的未处理异常时,照样提交相关事务。


编程式事务实现过程

• 基于 TransactionDefinition、PlatformTransactionManager、TransactionStatus 编程式事务管理是 Spring 提供的最原始的方式,通常我们不会这么写,但是了解这种方式对理解 Spring 事务管理的本质有很大作用。

• 基于 TransactionTemplate 的编程式事务管理是对上一种方式的封装,使得编码更简单、清晰。

transactionTemplate.execute(new TransactionCallback<Boolean>(){
	@Override
	public Boolean doInTransaction(TransactionStatus status) {
		// a、新增用户信息
	    int rows = userMapper.insert(user);
	    // b、新增用户岗位关联
	    insertUserPost(user);
	    // c、新增用户与角色管理
	    insertUserRole(user);
		return Boolean.TRUE;
	}
});

声明式事务实现过程

• 基于 TransactionInterceptor 的声明式事务是 Spring 声明式事务的基础,通常也不建议使用这种方式,但是与前面一样,了解这种方式对理解 Spring 声明式事务有很大作用。

• 基于 TransactionProxyFactoryBean 的声明式事务是上中方式的改进版本,简化的配置文件的书写,这是 Spring 早期推荐的声明式事务管理方式,但是在 Spring 2.0 中已经不推荐了。

• 基于 <tx>和 <aop>命名空间的声明式事务管理是目前推荐的方式,其最大特点是与 Spring AOP 结合紧密,可以充分利用切点表达式的强大支持,使得管理事务更加灵活。
• 基于 @Transactional 的方式将声明式事务管理简化到了极致。开发人员只需在配置文件中加上一行启用相关后处理 Bean 的配置,然后在需要实施事务管理的方法或者类上使用 @Transactional 指定事务规则即可实现事务管理,而且功能也不必其他方式逊色。


Spring事务加载过程:

1.配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。

2.spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。

查看相关注解的类的方法:
1. getTransactionAttribute获取事务属性
判定方法的事务属性
先从缓存中尝试获取属性信息.
如果缓存的是一个无事务对象
直接返回空,这里使用==表示比较的对象引用是否相等.
当没有缓存时,需要重新计算获取事务属性.
computeTransactionAttribute(method, targetClass);
如果方法没有事务,直接放入空事务对象.
ClassUtils.getQualifiedMethodName(method, targetClass);
将事务对象属性放到缓存中,缓存的key与类的类型和方法相关

2.computeTransactionAttribute计算事务属性
这个方法的主要功能是,根据方法和类的类型获取事务信息
如果事务是只适用于public方法,且当前方法的修饰不是public,那么直接返回空.
这个方法可能是一个接口,但是我们需要的是class
AopUtils.getMostSpecificMethod(method, targetClass);
调用findTransactionAttribute方法获取事务属性

3.determineTransactionAttribute判定事务属性
这个方法主要是通过逐个调用解析器解析,获取事务属性
for (TransactionAnnotationParser annotationParser : this.annotationParsers) {
annotationParser.parseTransactionAnnotation(ae);不为空则返回

4.parseTransactionAnnotation解析事务注解
解析事务注解中的属性
这个地方事务属性对象是有很多默认值的,它继承自DefaultTransactionDefinition
//传播// 隔离等级// 超时时间// 是否只读// 事务管理器bean的名称// 回滚相关配置

到这里事务注解中的属性已经被加载到spring容器中了,通过cglib动态代理已经将注解加入到拦截链中了


Spring声明式事务实现原理

经过Spring事务加载的流程,事务属性已近被加载到spring容器中了,然后就要探索代码在运行过程中事务注解时中的属性是怎么被利用起来的。声明式事务成为可能,主要得益于Spring AOP。使用一个**事务拦截器**,在方法调用的前后/周围进行事务性增强(advice),来驱动事务完成。拦截器是通过ProxyTransactionManagementConfiguration这个自动配置类加载进来的。

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionInterceptor transactionInterceptor() {
    TransactionInterceptor interceptor = new TransactionInterceptor();
    interceptor.setTransactionAttributeSource(transactionAttributeSource());
    if (this.txManager != null) {
        interceptor.setTransactionManager(this.txManager);
    }
    return interceptor;
}

AOP实现原理

Spring声明式事务主要通过AOP实现,AOP底层实现主要依靠动态代理,Spring中的动态代理包括JDK动态代理和CGLIB动态代理。具体使用哪一种动态代理需要视情况而定。

JDK动态代理 是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
cglib动态代理 是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

Spring声明式事务是通过@Transactional注解实现的。@Transactional注解可以用在接口上,也可以在类上。在接口上时,必须使用基于接口的代理才行,即JDK动态代理。
事实是Java的注解不能从接口继承,如果你使用基于类的代理,即CGLIB,或基于织入方面,即AspectJ,事务设置不会被代理和织入基础设施认出来,目标对象不会被包装到一个事务代理中。
Spring团队建议注解标注在类上而非接口上。

总结:
1.如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,但 可以强制使用CGLIB实现AOP 。
2.如果目标对象没有实现接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换。


动态代理流程

动态代理主要通过以下几个类方法实现事务代理。
1.invokeWithinTransaction
获取事务属性信息 //获取事务管理器 // 获取切点定义信息//创建事务信息对象

2.determineTransactionManager 事务管理器选择
DataSourceTransactionManager是最常用的事务管理器,从下面的依赖中引入。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

它的回滚操作如下,是通过connection完成的回滚

protected void doRollback(DefaultTransactionStatus status) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    Connection con = txObject.getConnectionHolder().getConnection();
    if (status.isDebug()) {
        logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
    }
    try {
        con.rollback();
    } catch (SQLException ex) {
        throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
    }
}

3.completeTransactionAfterThrowing 事务异常处理

4.commitTransactionAfterReturning 事件完成后的操作


事务注解的相关知识点

事务注解在类/方法上
@Transactional注解既可以标注在类上,也可以标注在方法上。当在类上时,默认应用到类里的所有方法。如果此时方法上也标注了,则方法上的优先级高。

事务注解在类上的继承性
@Transactional注解的作用可以传播到子类,即如果父类标了子类就不用标了。但倒过来就不行了。
子类标了,并不会传到父类,所以父类方法不会有事务。父类方法需要在子类中重新声明而参与到子类上的注解,这样才会有事务。

只在public方法上生效?
当采用代理来实现事务时,(注意是代理),@Transactional注解只能应用在public方法上。当标记在protected、private、package-visible方法上时,不会产生错误,但也不会表现出为它指定的事务配置。可以认为它作为一个普通的方法参与到一个public方法的事务中。
如果想在非public方法上生效,考虑使用AspectJ(织入方式)。
目标类里的自我调用没有事务?
在代理模式中(这是默认的),只有从外部的方法调用进入通过代理会被拦截,这意味着自我调用(实际就是,目标对象中的一个方法调用目标对象的另一个方法)在运行时不会导致一个实际的事务,即使被调用的方法标有注解。

如果你希望自我调用也使用事务来包装,考虑使用AspectJ的方式。在这种情况下,首先是没有代理。相反,目标类被织入(即它的字节码被修改)来把@Transactional加入到运行时行为,在任何种类的方法上都可以。


事务与线程、会话

1、会话可以创建多个事务
比如:使用客端连接数据库,这样你就可以执行很多个事务了

2、一个事务只能由一个会话产生
在数据库里的事务,如果在执行的SQL都是由会话发起的,哪怕是自动执行的JOB也是由系统会话发起的

3、一个事务可能会产生一个或多个线程
比如RMAN备份,是可以创建多个线程可加快备份速度

4、一个线程在同一时间内只能执行一个事务
而一个线程,在没结束当前事务是无法释放资源来执行第二个事务

事务:简单理解局势一个业务需求的最小处理单位。

如:从A银行卡转账500元到B银行卡,事务就包括两部分,1、从A卡减掉500元 2、从B卡加上500元
这两个部分只要一个部分出错,就要整体“回滚”,那这就是一个事务

会话:可以包含N个事务

如:你登陆网银之后,可以重复转账步骤2次,第二次转账失败,并不影响你第一次转账成功。

线程:一个事情,一个人干和多个人干的问题

如:比如植树,任务是植树500棵,一个人(线程)干5天,那五个人(线程)干1天。

保证Spring事务内的连接唯一性

通过ThreadLocal来保证Spring事务的唯一性。

和JavaEE事务上下文一样,Spring事务和一个线程的执行相关联,底层是一个ThreadLocal<Map<Object, Object>>,就是每个线程一个map,key是DataSource,value是Connection。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值