Spring | 事务管理
一、简介
事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性
。事务是一系列的动作,这些动作要么全部完成,要么全部不起作用。
比如去银行取款,总共分两个步骤,取出钱,卡里扣钱,如果你在取钱的时候,突然出现停电或者及其故障,此时很有可能出现两种情况,要么你取出钱,但是卡里没扣钱,或者是,你没取出钱,但是卡里白白扣了那么多钱。这肯定是不允许的,因此,使用事务,可以通过回滚操作,将已经进行的操作全部取消,相当于返回到了你取出钱之前的状态
事务的四个关键属性(ACID):原子性、一致性、隔离性、持久性
二、Spring中的事务管理器
Spring 从不同的事务管理 API 中抽象了一整套的事务机制,开发人员不必了解底层的事务 API,就可以利用这些事务机制。有了这些事务机制,事务管理代码就能独立于特定的事务技术了
Spring事务管理器的接口是 org.springframework.transaction.PlatformTransactionManager
,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器
三、JDBC中的事务
如果应用程序中直接使用 JDBC 来进行持久化,DataSourceTransactionManager
会为你处理事务边界。为了使用 DataSourceTransactionManager
,你需要使用如下的XML将其装配到应用程序的上下文定义中:
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
配置文件中需要引用 JDBC 的 Bean 来进行配置。实际上,DataSourceTransactionManager
是通过调用 java.sql.Connection
来管理事务,而后者是通过 DataSource
获取到的。通过调用连接的 commit()
方法来提交事务,同样,事务失败则通过调用 rollback()
方法进行回滚。
四、使用两种方式来管理事务
使用@Transactional注解声明式地管理事务
- 在需要定义事务操作的方法上加上
@Transactional
注解,且只能标注共有方法 - 在 Bean 配置文件中只需使用
<tx:annotation-driven>
元素,并为之指定事务管理器就可以了 - 如果事务处理器的名称是
transactionManager
,就可以在<tx:annotation-driven>
元素中省略transaction-manager
属性,这个元素会自动检测该名称的事务管理器
<!-- 启用事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
使用事务通知来声明式的管理事务
- 事务管理是一种横切关注点
- 使用
<tx:advice>
元素声明事务通知,同时在里面设置事务的各种属性 - 通过
<aop:config>
配置事务切入点,由于事务通知在<aop:config>
元素外部声明的,所以无法直接与切入点产生关联,必须在<aop:config>
元素里面声明一个<aop:advisor>
(增强器)与切入点想联系 - 由于 Spring AOP 是基于代理的方法,因此只能增强公共方法
<!-- 1. 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 2. 配置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase" propagation="REQUIRES_NEW"/>
<!-- 指定 get 开头的方法为只读的 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 3. 配置事务切入点, 以及把事务切入点和事务属性关联起来 -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut expression="execution(* edu.just.spring.jdbc.transaction.BookShopService.*(..))" id="txPointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
五、事务传播属性
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播,如:被调用的方法是在调用自己的方法所在事务中运行,还是创建一个自己的新事务,并在自己的新事务中运行。以下是就介绍几种传播行为:
-
REQUIRED
:如果有事务在运行,当前的方法就在这个事务内运行;否则就创建一个新的事务,并在自己的事务内运行 -
REQUIRED_NEW
:当前的方法必须启动新事务,并在它自己的事务内运行,如果运行自己事务同时还有其他事务在运行,应该把其他事务暂时挂起 -
SUPPORTS
:如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务内 -
NOT_SUPPORTED
:当前的方法不应该运行在事务中,如果有运行的事务,将它挂起 -
MANDATORY
:当前的方法必须在事务内部,如果没有正在运行的事务,就抛出异常 -
NESTED
:如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行
如何设置?
- 使用 @Transactional 注解
@Transactional(propagation=Propagation.REQUIRES_NEW)
@Override
public void purchase(String username, String isbn) {
....
}
- 在事务通知中配置传播属性
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>
可以在 <tx:method>
元素中设置传播事务属性
五、并发事务所导致的问题
当同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时,可能会出现许多意外的问题
- 脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。此时若这个数据进行回滚,则事务读取的数据就是临时且无效的。
- 不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,对于两个事务,第一个事务先读取了一个字段,然后第二个事务修改了该字段,之后,第一个事务再次读取了同一个字段,那么两次读取到的值是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
-
幻读:第一个事务从表中读取了一个字段,然后第二个事务在该表中插入了一些新的行,之后如果第一个事务再次读取同一个表,会发现多出几行,如同发僧侣幻觉一般
六、事务的隔离级别
理论上说,事务应该彼此完全隔离,以避免并发事务所导致的问题。然而,那样会对性能产生极大的影响,因此为事务必须按顺序执行。在实际开发中,为了提升性能,事务会以比较低的隔离级别来运行
-
DEFAULT
:使用底层数据库的默认隔离级别,对于大多数数据库来说,默认隔离级别为 READ_COMMITED,Mysql 的默认隔离级别是 REPEATABLE-READ -
READ_UNCOMMITEED
:允许事务读取未被其他事务提交的变更,会出现脏读、不可重复度和幻读 -
READ_COMMITEED
:只允许事务读取已经被其他事务提交的变更,可以避免脏读,但不可重复读和幻读可能出现 -
REPEATABLE_READ
:确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读,但是幻读的问题依旧存在 -
SERIALIZABLE
:确保事务可以从一个表中读取相同的行,在这个事务期间,禁止其他事务对改变执行增、删、改操作,所有并发问题都可以避免,但是性能低下
PS:Oracle 支持两种事务隔离级别,READ_COMMITEED
和 SERIALIZABLE
。MySql 支持4种事务隔离级别
如何设置?
- 用 @Transactional 注解
@Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.READ_COMMITTED)
@Override
public void purchase(String username, String isbn) {
...
}
可以在 @Transactional
地 isolation
属性中设置隔离级别
- 可以在
<tx:method>
元素中指定隔离级别
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase" propagation="REQUIRES_NEW" isolation="READ_COMMITTED"/>
</tx:attributes>
</tx:advice>
七、设置超时和只读事务属性
- 超时和只读属性可以在
@Transactional
注解中定义。超时属性以秒为单位来计算
@Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.READ_COMMITTED, readOnly=false, timeout=3)
@Override
public void purchase(String username, String isbn) {
...
}
- 也可以在
<tx:method>
中进行指定
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase" propagation="REQUIRES_NEW" isolation="READ_COMMITTED"/>
<!-- 指定 get 开头的方法为只读的 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>