Spring之事务管理
今天补充一下Spring里的事务管理
回顾事务的概念
什么是事务
其实就是一句话,做一系列的事情,要么都成功,要么都失败。之前接触事务的概念主要是在数据库中,事务涉及到数据的一致性问题,要确保完整性和一致性。
事务的特性ACID
- 原子性(Atomicity):原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency):事务前后数据的完整性必须保持一致。
- 隔离性(isolation):事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
- 持久性(durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
并发事务存在的问题
- 更新丢失(Lost Update):多个事务修改同一行的记录(都未提交),后面的修改覆盖了前面的修改
- 脏读(Dirty Reads):一个事务读取了另一个事务未提交的数据。
- 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
- 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
不可重复度和幻读的主要区别是,不可重复读的重点是修改,幻读的重点在于新增或者删除。
讲到这些不讲隔离级别那肯定不行的
隔离级别
- 未提交读(READ-UNCOMMITTED),事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100.可能会导致脏读、幻读或不可重复读
- 提交读(READ-COMMITTED),对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了;可以阻止脏读,但是幻读或不可重复读仍有可能发生
- 可重复读(REPEATABLE-READ),就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的;可以阻止脏读和不可重复读,但幻读仍有可能发生。
- 可串行化读(SERIALIZABLE),在并发情况下,和串行化的读取的结果是一致的,没有什么不同,比如不会发生脏读和幻读;该级别可以防止脏读、不可重复读以及幻读。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ-UNCOMMITTED | √ | √ | √ |
| READ-COMMITTED | × | √ | √ |
| REPEATABLE-READ | × | × | √ |
| SERIALIZABLE | × | × | × |
Spring的事务管理
Spring事务管理的接口
- PlatformTransactionManager: (平台)事务管理器
- TransactionDefinition: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)
- TransactionStatus: 事务运行状态
其实这里的事务管理,就是“按照给定的事务规则来执行提交或者回滚操作”。
PlatformTransactionManager
Spring本身是不直接管理实务的,而是提供了多种事务管理器,将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的的相关平台框架的事务来实现。
Spring事务管理的接口是:org.springframework.transaction.PlatformTransactionManager
通过这个接口,Spring给各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但具体如何实现就是各个平台自己的事情了。

PlatformTransactionManager接口中有三个方法:
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
//根据指定的传播行为,返回当前活动的事务或创建一个新事务。
void commit(TransactionStatus status) throws TransactionException;
//使用事务目前的状态提交事务
void rollback(TransactionStatus status) throws TransactionException;
//执行的事务进行回滚
}
我们在JDBC或者Mybatis里进行数据持久化操作时,我们的xml配置通常如下:
<!--配置声明式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
TransactionDefinition
可以看到在PlatformTransactionManager源码里的getTransaction方法中返回了一个事务,这个方法里的参数就是TransactionDefinition类,这个类定义了一些基本的事务属性。

事务属性
事务属性的种类主要分为:传播行为、隔离级别、只读、事务超时
我们点进TransactionDefinition源码可以看到它的结构

传播行为
传播行为定义了被调用方法的事务边界
| 传播行为 | 意义 |
|---|---|
| PROPERGATION_MANDATORY | 表示方法必须运行在一个事务中,如果当前事务不存在,就抛出异常 |
| PROPAGATION_NESTED | 表示如果当前事务存在,则方法应该运行在一个嵌套事务中。否则,它等价于 PROPAGATION_REQUIRED |
| PROPAGATION_NEVER | 表示方法不能运行在一个事务中,否则抛出异常 |
| PROPAGATION_NOT_SUPPORTED | 表示方法不能运行在一个事务中,如果当前存在一个事务,则该方法将被挂起 |
| PROPAGATION_REQUIRED | 表示当前方法必须运行在一个事务中,如果当前存在一个事务,那么该方法运行在这个事务中,否则,将创建一个新的事务 |
| PROPAGATION_REQUIRES_NEW | 表示当前方法必须运行在自己的事务中,如果当前存在一个事务,那么这个事务将在该方法运行期间被挂起 |
| PROPAGATION_SUPPORTS | 表示当前方法不需要运行在一个是事务中,但如果有一个事务已经存在,该方法也可以运行在这个事务中 |
隔离级别
这里的隔离级别跟mysql中有相同的地方也有不同的地方,我们在操作数据的时候会有3个副作用,分别是脏读、不可重复读、幻读。为了避免这三个现象发生,在标准的SQL里定义了四种隔离级别,上面也说了,分别是未提交读、已提交读、可重复读、序列化。但是在Spring的事务里我们可以看到提供了5种隔离级别来对应SQL种定义的4种隔离级别
| 隔离级别 | 意义 |
|---|---|
| ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
| ISOLATION_READ_UNCOMMITTED | 允许读取未提交的数据(对应未提交读),可能导致脏读、不可重复读、幻读 |
| ISOLATION_READ_COMMITTED | 允许在一个事务中读取另一个已经提交的事务中的数据(对应已提交读)。可以避免脏读,但是无法避免不可重复读和幻读 |
| ISOLATION_REPEATABLE_READ | 一个事务不可能更新由另一个事务修改但尚未提交(回滚)的数据(对应可重复读)。可以避免脏读和不可重复读,但无法避免幻读 |
| ISOLATION_SERIALIZABLE | 这种隔离级别是所有的事务都在一个执行队列中,依次顺序执行,而不是并行(对应可序列化)。可以避免脏读、不可重复读、幻读。但是这种隔离级别效率很低,因此,除非必须,否则不建议使用。 |
只读
如果在一个事务中所有关于数据库的操作都是只读的,也就是说,这些操作只读取数据库中的数据,而并不更新数据,那么应将事务设为只读模式( READ_ONLY_MARKER ) , 这样更有利于数据库进行优化 。
因为只读的优化措施是事务启动后由数据库实施的,因此,只有将那些具有可能启动新事务的传播行为 (PROPAGATION_NESTED 、 PROPAGATION_REQUIRED 、 PROPAGATION_REQUIRED_NEW) 的方法的事务标记成只读才有意义。
如果使用 Hibernate 作为持久化机制,那么将事务标记为只读后,会将 Hibernate 的 flush 模式设置为 FULSH_NEVER, 以告诉 Hibernate 避免和数据库之间进行不必要的同步,并将所有更新延迟到事务结束。

事务超时
如果一个事务长时间运行,为了避免系统资源的浪费,我们应该给这个事务设置一个有效时间,使其等待数秒后自动回滚。与设置“只读”属性一样,事务有效属性也需要给那些具有可能启动新事物的传播行为的方法的事务标记成只读才有意义。

这里面也就几个方法干脆一起说说

TransactionDefinition内的方法
public interface TransactionDefinition {
//返回事务的传播行为
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
//返回事务的隔离级别,事务管理器根据他来控制另一个事务可以看到本事务的数据范围
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
//返回事务需要在多少秒内完成
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
//返回是否优化为只读事务
default boolean isReadOnly() {
return false;
}
//返回事务名字
@Nullable
default String getName() {
return null;
}
//返回一个不可修改的TransactionDefinition使用默认值。
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}
}
TransactionStatus
还是刚才的PlatformTransactionManager源码,你瞧getTransaction的返回值是什么

可以看到返回的是一个TransactionStatus对象。
源码看看
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
/**
* 返回该事务是否在内部带有保存点,
* 也就是说,已创建为基于保存点的嵌套事务。
*
* 此方法主要用于诊断目的,另外还有{@link #isNewTransaction()}。
* 对于自定义保存点的编程处理,使用{@link SavepointManager}提供的操作。
*
*/
boolean hasSavepoint();
/**
* 将底层会话刷新到数据存储(如果适用):
* 例如,所有受影响的Hibernate/JPA会话。
*
* 这实际上只是一个提示,如果底层事务管理器没有刷新的概念,则可能是一个no-op。
* 刷新信号可能应用于主资源或事务同步,这取决于底层资源。
*/
@Override
void flush();
}
老版的spring中的TransactionStatus里还有isNewTransaction()、setRollbackOnly()、isCompleted()等方法,新版直接在接口的实现类AbstractTransactionStatus中实现了,

附配置文件
声明式事务:AOP
编程式事务:需要在代码中,进行事务管理
在spring-dao中加入配置
<!--配置声明式事务,即事务管理器-->
<bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--结合AOP实现事务的织入-->
<!--配置事务(通知/的类)-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--给哪些方法配置事务-->
<!--配置事务的传播特性:new propagation:传播 默认为:REQUIRED-->
<tx:attributes>
<tx:method name="add" propagation="REQUIRED"/>
<tx:method name="delete" propagation="REQUIRED"/>
<tx:method name="select" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--配置事务切入-->
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.feng.mapper.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
如果对实例感兴趣的可以看看guide哥这篇