Spring事务管理原理及方法详解
这篇文章主要介绍了Spring事务管理原理及方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
事务,在日常开发或者面试中都必定会涉及到。开发工作中,结合数据库开发理解就是:一组dml要么全部成功执行提交,要么因为某一个操作异常,撤销之前所做的成功的操作,整体执行失败。再简单点的一句话:生死与共。
由此,可以看出,事务的必要性:在开发工作中,保证操作数据的安全性。事务的控制也就是保证数据的访问安全性。
一、事务的四大特性
A:原子性(atomicity),对数据的修改,要么全部成功执行,要么全部不执行。
C:一致性(consistency),一旦事务完成,系统必须保证数据职员是满足业务状态的一种一致状态中。很难懂的解释,跟原子性很像。一个事务在操作过程中,数据可能会产生很多中间态,一致性保证中间态对其他事务不可见,因为这些中间态,与事务的开始和结束的状态是不一致的。也就是从一种正确的状态到另一种正确的状态。
I:隔离性(isolation),事务之间的执行应不相互影响,也即事务执行的独立。
D:持久性(durability),事务一旦提交,则对数据库的修改是永久性的。
二、事务的隔离级别
并发环境下,事务可能会存在若干问题:脏读、幻读、不可重复读、第一类更新丢失、第二类更新丢失。
类型 | 说明 | 举例 |
脏读 | A事务读取到了B事务未提交的数据 | A开启事务=>B开启事务,读取账户1000块,取走100块=>A读取账户金额,读取到900=>B回滚事务。此时A读取的余额数据是无效的 |
幻读 | 一个事务里面的操作发现了未被操作的数据 |
A开启事务,修改某些数据状态=>B开启事务,执行新增数据并提交=>A事务提交,会出现一条未被修改的数据。 幻读发生的前提是并发事务中发生了新增或者删除动作。 |
不可重复读 | 一个事务中,先后两次读取数据,读到的结果不一致 |
A开启事务,读取账户1000块=>B开启事务,读取账户1000块,取出100块并提交事务=>A再读取账户余额,余额900块。 一个事务范围内的两次同样的查询,却返回了两次不同的数据,这就是不可重复读 |
第一类更新丢失 | A事务撤销,把已经提交的B事务的更新的数据覆盖 | A开启事务,读取账户1000块=>B开启事务,读取账户1000块,然后增加100块,提交事务,账户变为1100=>A撤销回滚事务,账户成1000块 |
第二类更新丢失 | A事务提交,把已经提交的B事务的更新的数据覆盖 | A开启事务,读取账户1000块=>B开启事务,读取账户1000块,然后增加100块,提交事务,账户变为1100=>A增加100块,提交事务,账户变为1100。 |
针对并发环境下可能出现的事务问题,于是就出现了隔离级别的解决方案,由低到高依次是:读未提交(Read uncommitted)、读已提交(Read committed)、可重复读(Repeatable read)、串行序列化(serializable)。下表展示出不同的隔离级别,对于脏读、幻读、不可重复读是否会出现。
类型 | 脏读 | 不可重复读 | 幻读 | 说明 |
Read uncommitted | 会 | 会 | 会 | |
Read committed | 不会 | 会 | 会 | |
Repeatable read | 不会 | 不会 | 会 | mysql的默认隔离级别 |
serializable | 不会 | 不会 | 不会 | 最严格的隔离级别,将事务串行化执行,性能低。 |
mysql中查询当前隔离级别:select @@tx_isolation;
三、spring事务支持的隔离级别和传播特性
spring中定义了五种隔离界别和七种传播行为(可以在org.springframework.transaction.TransactionDefinition类中看到详细的解释)
1、spring支持的隔离级别
ISOLATION_DEFAULT:默认级别。一般是使用的是数据库本身的隔离级别(mysql - Repeatable read 、oracle - Read committed)
余下ISOLATION_READ_UNCOMMITTED、ISOLATION_READ_COMMITTED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE分别对应上述数据库隔离级别配置。
2、传播特性
事务的传播特性就是在多个事务方法互相调用的时候,事务该如何在方法之间使用传播:
- PROPAGATION_REQUIRED:如果当前有事务,则加入该事务中。如果没有,则新建事务。
- PROPAGATION_SUPPORTS:如果当前有事务,则加入该事务中。如果没有,则以非事务状态运行。
- PROPAGATION_MANDATORY:如果当前有事务,则加入该事务中。如果没有,则抛出无事务异常
- PROPAGATION_REQUIRES_NEW:新建事务。如果当前存在事务,则挂起该事务。
- PROPAGATION_NOT_SUPPORTED:非事务状态运行。如果当前存在事务,则挂起该事务。
- PROPAGATION_NEVER:非事务状态运行。如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED:如果当前有事务,则嵌套事务(父子事务)执行。如果没有,类似PROPAGATION_REQUIRED处理。
spring默认的传播行为是PROPAGATION_REQUIRED,一般适用于绝大多数的开发工作。
3、事务的超时属性
事务在超过预定时间内还未完成操作,则自动回滚事务。TransactionDefinition 中以int值来表示超时时间,单位是秒,提供的默认值是TIMEOUT_DEFAULT = -1,即永不超时,一直等待操作完成
四、spring事务配置方法
spring并不具体直接的管理事务,而是提供了一个接口org.springframework.transaction.PlatformTransactionManager,该接口中主要定义了三个方法:getTransaction(获取事务)、commit(提交)和rollback(回滚)。
根据不同的持久化策略,spring提供了不同的实现,比如jdbc -
org.springframework.jdbc.datasource.DataSourceTransactionManager、hibernate - org.springframework.orm.hibernate5.HibernateTransactionManager等,在其他的实现可以通过源码去查询。
spring事务使用,可以分为编程式事务和声明式事务。编程式事务每次业务使用都得书写获取事务、设置事务隔离级别和传播特性、提交或回滚事务,代码的重复太高,费时费力,且如果代码的功能性复杂时候,使用编程式变得更加痛苦。而声明式的事务,属于无侵入式的,不会影响主业务流程,且编写上非常简单。所以目前开发工作中,更多的是使用声明式事务。
1、编程式事务
//to do。后续补充
2、声明式事务
声明式事务分为两种:基于aop的织入和@Transactional的注解。
2.1、基于aop的事务织入
之后在servcie中定义方法,最好的就是tx:method中定义的格式开头,就会执行特定的事务。
2.2、注解事务
注解事务第一部分的数据源和事务管理器配置同上,配置文件中需要修改的是开启注解配置:
然后在service编程中,加注解@Transactional即可(建议只在service实现类中加),如下是配置样例,其中的属性可以按需设置:
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ,timeout = 100, readOnly = false, rollbackFor = {},noRollbackFor = {})
注意点:
事务的异常回滚只检查RuntimeException的异常,checked exception(如ClassNotFoundException、FileNotFoundException等)不会滚,捕获异常不抛出也不会回滚。
五、spring和springmvc父子容器
现在开发工作中,一般大多数都使用的spring和springmvc构建,这里,spring容器和springmvc容器就构成了父子容器的关系。父容器spring是发现不了子容器springmvc中的bean的,而子容器可以发现父容器中注册的bean。由此,实际开发工作中,不注意的话,往往会产生一些意想不到的问题。
首先,通常我们配置spring配置文件applicationContext.xml的时候,会配置如下的扫描:
<context:component-scan base-package="com.cfang" />
这个配置会扫描指定包下面的所有@Component类型注解,包括@Controller,@Service,@Respository,并将扫描到的bean注册到spring容器中。
一般,spring配置文件中,还会出现下面的配置,作用是扫描@Required、@Autowired、 @PostConstruct、@PersistenceContext、@Resource、@PreDestroy等注解。理论上,此配置为可选配置,因为上面的扫描配置会默认打开。
<context:annotation-config/>
接下来配置springmvc的配置文件spring-mvc.xml,配置扫描注解@RequestMapping、@RequestBody、@ResponseBody等,同时,该配置默认加载很多参数绑定方法 。
<mvc:annotation-driven />
上面一句话,相当于:
<!--配置注解控制器映射器,它是SpringMVC中用来将Request请求URL到映射到具体Controller--> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/> <!--配置注解控制器映射器,它是SpringMVC中用来将具体请求映射到具体方法--> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
上面聊完基本配置,以下梳理下,来了解可能产生的容器冲突,对于事务管理的影响。
首先,有两个基本的容器:spring和springmvc,配置文件分别为applicationContext.xml和spring-mvc.xml
1、applicationContext.xml中配置<context:component-scan base-package="com.cfang" />,扫描指定包下的所有bean,并自动注册到spring容器中
2、spring-mvc.xml配置<mvc:annotation-driven />,扫描相关的springmvc的注解
3、为了保证springmvc的正常跳转,通常我们还得在spring-mvc.xml文件中配置包扫描<context:component-scan base-package="com.cfang" />。
按照以上配置信息,就会产生事务失效。原因就在于:
Spring容器优先加载由ServletContextListener(对应applicationContext.xml)产生的父容器,
而SpringMVC(对应spring-mvc.xml)产生的是子容器。子容器Controller进行扫描装配时装配的@Service注解的实例是没有经过事务加强处理,即没有事务处理能力的Service,
而父容器进行初始化的Service是保证事务的增强处理能力的。如果不在子容器中将Service排除(exclude)掉,此时得到的将是无事务处理能力的Service。
解决办法按照官方建议的来配置,各自负责一部分加载:
spring扫描:
<context:component-scan base-package="com.cfang.WeChat" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/> <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/> </context:component-scan>
springmvc扫描:
<context:component-scan base-package="com.cfang.WeChat" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。