Hey, Nice to meet You. 

必有过人之节.人情有所不能忍者,匹夫见辱,拔剑而起,挺身而斗,此不足为勇也,天下有大勇者,猝然临之而不惊,无故加之而不怒.此其所挟持者甚大,而其志甚远也.          ☆☆☆所谓豪杰之士,

Spring中对事务的支持

1、事务的回顾

[1]、什么是事务?

事务就是由一组SQL组成的单元,该单元要么整体执行成功,要么整体执行失败。


[2]、事务的ACID属性

  • 原子性(Atomicity):指事务中包含所操作的SQL是一个不可分割的工作单位,要么都执行成功,要么都执行失败,其中只要有一条SQL出现错误都会回滚到原来的状态。
  • 一致性(Consistency):事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。比如A和B两者的钱加起来一共是1000,那么不管A和B之间如何转账、转几次账,事务结束后两个用户的钱相加起来应该还得是1000,并且在当前事务中,A减了多少钱,B加了多钱这个中间状态是不可见的,这就是事务的一致性。
  • 隔离性(Isolation):一个事务所做的修改在最终提交以前,对其他事务是不可见的。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。比如A正在从一张银行卡中取钱,在A取钱结束前,B不能向这张卡转账。
  • 持久性(Durability):指的是一个事务一旦被提交,数据就被永远的存储到磁盘上了,即使系统发生故障,数据仍然不会丢失。

[3]、事务执行过程中的并发问题

  • 脏读:事务A读取了事务B更新并且未提交的数据,然后B回滚操作,那么A读取到的数据是脏数据
    • 初始状态:数据库中age字段数据的值是20
    • T1把age修改为了30
    • T2读取了age现在的值:30
    • T1回滚了自己的操作,age恢复为了原来的20
    • 此时T2读取到的30就是一个不存在的“脏”的数据
  • 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
    • T1第一次读取age是20
    • T2修改age为30并提交事务,此时age确定修改为了30
    • T1第二次读取age得到的是30
  • 幻读:事务A从一个表中读取了一个字段,然后B在该表中插入/删除了一些新的行。 之后, 如果 A 再次读取同一个表, 就会多/少几行,就好像发生了幻觉一样,这就叫幻读。
    • T1第一次执行count(*)返回500
    • T2执行了insert操作
    • T1第二次执行count(*)返回501,感觉像是出现了幻觉

补充:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表


[4]、事务的隔离级别

SQL标准定义了4种隔离级别(从低到高),分别对应可能出现的数据不一致的情况:

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
读已提交(read-committed)
可重复读(repeatable-read)
串行化(serializable)

4种隔离级别的描述:

  • 读未提交(read-uncommitted):允许A事务读取其他事务未提交和已提交的数据
  • 读已提交(read-committed):只允许A事务读取其他事务已提交的数据
  • 可重复读(repeatable-read):确保事务可以多次从一个字段中读取相同的值。在这个事务持续期间,禁止其他事务对这个字段进行更新;注意:mysql中使用了MVCC多版本控制技术,在这个级别也可以避免幻读。
  • 串行化(serializable):锁定整个表,让对整个表的操作全部排队串行执行。能解决所有并发问题,安全性最好,但是性能极差,基本不用。

2、Spring中的事务介绍

Spring框架中对事务的支持有两种:

  • 编程式事务管理
  • 声明式事务管理(推荐)

[1]、编程式事务管理

编程式事务管理:事务的相关操作完全由开发人员通过编码实现。所以编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。但是我们基本不推荐使用编程式事务。下图展示的是编程式事务的实现,完全有程序员来实现。

image


[2]、声明式事务管理

声明式事务管理:事务的控制交给Spring框架来管理,开发人员只需要在Spring框架的配置文件中声明你需要的功能即可。

Spring中声明式事务管理的底层是基于AOP来完成的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。声明式事务它将具体业务与事务处理部分解耦,代码侵入性很低,所以在实际开发中声明式事务用的比较多。

image


[3]、Spring事务的相关接口

Spring事务管理的相关接口有三个,如下:

  • PlatformTransactionManager:事务管理器,为不同的数据访问技术的事务提供不同的接口实现
  • TransactionDefinition: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)
  • TransactionStatus: 事务的运行状态

Spring的事务机制是用统一的机制来处理不同数据访问技术的事务处理,Spring并不直接管理事务,而是提供了多种事务管理器。Spring的事务机制提供了一个org.springframework.transaction.PlatformTransactionManager接口,将事务管理的职责委托给JDBC或者Hibernate等持久化机制所提供的相关平台框架的事务来实现。通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,其具体的实现就是各个平台自己的事情了,对应的相关实现如下表所示。

数据库访问技术 实现
JDBC DataSourceTransactionManager
JPA JpaTransactionManager
Hibernate HibernateJpaTransactionManager
JDO JdoTransactionManager
分布式事务 JtaTransactionManager

3、基于注解的声明式事务

在Spring中使用声明式事务一般会使用注解来实现,即@Transactional注解,该注解可以使用在类、接口和方法上:

  • 作用在类:表示所有该类的 public 方法都配置相同的事务属性信息。
  • 作用在方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
  • 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效。
@Transactional
public class Trans {
    @Transactional
    public void saveSomething() {
        //...相关操作 
    }
}

需要特别注意的是,此@Transactional注解来自org.springframework.transaction.annotation包,而不是javax.transaction。

@Transactional注解中常用参数:

  • value:当在配置文件中有多个 TransactionManager,可以用该属性指定选择哪个事务管理器。
  • propagation:事务的传播行为,默认值为 REQUIRED
  • isolation:事务的隔离级别,默认值为 DEFAULT,即采用数据库的默认隔离级别。
  • timeout:事务的超时时间(单位是秒),默认值为 -1。如果超过该时间限制但事务还未提交,则自动回滚事务。
  • readOnly:用于指定事务是否为只读事务,默认值为 false。为了忽略那些不需要事务的方法,比如select读取数据,可以设置readOnly = true。
  • rollbackFor:指定能够触发事务回滚的异常类型,可以指定多个异常类型。
  • noRollbackFor:指定不用回滚事务的异常类型,可以指定多个异常类型。

下面是@Transactional注解的简单使用(定义的是异常类是class对象):

@Transactional(
    propagation = Propagation.REQUIRED, // 传播行为
    isolation = Isolation.DEFAULT,  // 隔离级别
    timeout = 1000,  // 事务的超时时间(单位是秒)
    readOnly = true,  // 事务是否为只读
    rollbackFor = Exception.class,  // 能够触发事务回滚的异常类型
    noRollbackFor = Exception.class  // 不用回滚事务的异常类型
)
public void doSomething() {  
  //...相关操作
}

注意:在Spring中使用事务还需要在xml配置文件中配置如下内容:

    <!-- 1.配置事务管理器的bean -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <!-- 给事务管理器装配数据源 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 2.开启基于注解的声明式事务 -->
    <!-- 在transaction-manager属性中指定前面配置的事务管理器的bean的id -->
    <!-- transaction-manager属性的默认值是transactionManager,如果正好前面bean的id就是这个默认值,那么transaction-manager属性可以省略不配 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <!-- 3.配置自动扫描的包 -->
    <context:component-scan base-package="com.thr.service"/>

4、事务的传播机制(行为)

事务的传播机制一般用在事务的嵌套中,当事务方法被另一个事务方法调用时,则应该指定事务如何传播。比如事务方法A直接或间接调用了方法B,那么这两个方法是各自作为独立的方法提交,还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。

注:事务的传播行为和隔离级别都定义在TransactionDefinition接口中:

image

事务的传播行为如下表所示(主要学习前两个即可,其它的简单了解):

事务传播行为
描述
PROPAGATION_REQUIRED 支持外层事务。这是Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,则创建一个新的事务。
PROPAGATION_REQUIRES_NEW 不支持外层事务。该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可。
PROPAGATION_SUPPORTS 支持外层事务。如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务
PROPAGATION_NOT_SUPPORTED 不支持外层事务。该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码
PROPAGATION_NEVER 不支持外层事务。该传播机制不支持外层事务,即如果外层有事务就抛出异常
PROPAGATION_MANDATORY 支持外层事务。与NEVER相反,如果外层没有事务,则抛出异常
PROPAGATION_NESTED Spring 所特有的。该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚,等价于TransactionDefinition.PROPAGATION_REQUIRED。

简单测试REQUIRED和REQUIRES_NEW两种传播行为:

①、在EmployeeServiceImpl中增加了两个方法:updateOne()和updateTwo():

image

②、创建一个PropagationServiceImpl类

image

③、junit测试代码:

image

④、测试结论:

测试REQUIRED:两个方法的操作都没有生效,updateTwo()方法回滚,导致updateOne()也一起被回滚,因为他们都在propagationService.update()方法开启的同一个事务内。


测试REQUIRES_NEW:把updateOne()和updateTwo()这两个方法上都使用下面的设置:

@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)

结果:

  • updateOne()没有受影响,成功实现了更新
  • updateTwo()自己回滚

原因:上面两个方法各自运行在自己的事务中。

5、事务的隔离级别

事务的隔离级别定义了一个事务可能受其他并发事务影响的程度。隔离级别可以不同程度的解决脏读、不可重复读、幻读。

  • ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别,Oracle 默认采用的 READ_COMMITTED隔离级别。
  • ISOLATION_READ_UNCOMMITTED:不可提交读,允许读取尚未提交事务的数据,可能会导致脏读、不可重复读、幻读。
  • ISOLATION_READ_COMMITTED:读已提交,读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • ISOLATION_REPEATABLE_READ:可重复读,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • ISOLATION_SERIALIZABLE:串行化,这种级别是最高级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。但是严重影响程序的性能。几乎不会用到该级别。

6、只读属性和超时属性

①、只读属性

一个事务如果是做查询操作,可以设置为只读,此时数据库可以针对查询操作来做优化,有利于提高性能。

@Transactional(readOnly = true)
public void doSomething() {  
  //...相关操作
}

如果是针对增删改方法设置只读属性,则会抛出下面异常:

表面的异常信息:TransientDataAccessResourceException: PreparedStatementCallback
    
根本原因:SQLException: Connection is read-only. Queries leading to data modification are not allowed(连接是只读的。查询导向数据的修改是不允许的。)

实际开发时建议把查询操作设置为只读。

②、超时属性

一个数据库操作有可能因为网络或死锁等问题卡住很长时间,从而导致数据库连接等资源一直处于被占用的状态。所以我们可以设置一个超时属性,让一个事务执行太长时间后,主动回滚。事务结束后把资源释放出来。

@Transactional(timeout = 60) //单位为秒
public void doSomething() {  
  //...相关操作
}

7、事务回滚的异常

在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。

image

设置方式如下所示(实际开发时通常也建议设置为根据Exception异常回滚):

@Transactional(
    propagation = Propagation.REQUIRED, // 传播行为
    isolation = Isolation.DEFAULT,  // 隔离级别
    timeout = 3000,  // 事务的超时时间
    readOnly = true,  // 事务是否为只读
    rollbackFor = Exception.class,  // 能够触发事务回滚的异常类型
)
public void doSomething() {  
  //...相关操作
}

8、基于XML的声明式事务

基于XML的方式配置声明式事务也比较的简单,其配置的方式如下所示:

    <!-- 配置基于XML的声明式事务 -->
    <aop:config>
        <!-- 配置事务切面的切入点表达式 -->
        <aop:pointcut id="txPointCut" expression="execution(* *..*Service.*(..))"/>
        <!-- 将切入点表达式和事务通知关联起来 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>
    <!-- 配置事务通知:包括对事务管理器的关联,还有事务属性 -->
    <!-- 如果事务管理器的bean的id正好是transactionManager,则transaction-manager属性可以省略 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 给具体的事务方法配置事务属性 -->
        <tx:attributes>
            <!-- 指定具体的事务方法 -->
            <tx:method name="get*" read-only="true"/>
            <tx:method name="query*" read-only="true"/>
            <tx:method name="count*" read-only="true"/>
            <!-- 增删改方法 -->
            <tx:method name="update*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="insert*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="delete*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        </tx:attributes>
    </tx:advice>

注意事项:

  1. 虽然切入点表达式已经定位到了所有需要事务的方法,但是在tx:attributes中还是必须配置事务属性。这两个条件缺一不可。缺少任何一个条件,方法都加不上事务。
  2. 另外,tx:advice导入时需要注意名称空间的值,不要导错了,因为导错了很难发现。

image


参考链接:

posted @ 2021-06-21 16:13  唐浩荣  阅读(1882)  评论(1编辑  收藏  举报