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。但是我们基本不推荐使用编程式事务。下图展示的是编程式事务的实现,完全有程序员来实现。
[2]、声明式事务管理
声明式事务管理:事务的控制交给Spring框架来管理,开发人员只需要在Spring框架的配置文件中声明你需要的功能即可。
Spring中声明式事务管理的底层是基于AOP来完成的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。声明式事务它将具体业务与事务处理部分解耦,代码侵入性很低,所以在实际开发中声明式事务用的比较多。
[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接口中:
事务的传播行为如下表所示(主要学习前两个即可,其它的简单了解):
事务传播行为 |
描述 |
---|---|
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():
②、创建一个PropagationServiceImpl类
③、junit测试代码:
④、测试结论:
测试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,可以让事物在遇到非运行时异常时也回滚。
设置方式如下所示(实际开发时通常也建议设置为根据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>
注意事项:
- 虽然切入点表达式已经定位到了所有需要事务的方法,但是在tx:attributes中还是必须配置事务属性。这两个条件缺一不可。缺少任何一个条件,方法都加不上事务。
- 另外,tx:advice导入时需要注意名称空间的值,不要导错了,因为导错了很难发现。
参考链接: