Spring 事务管理
(个人学习记录)
本章主要记录spring事务的两种实现方式,及编程式事务和声明式事务(xml和注解方式),在实际过程中,我们绝大多数时候都是使用的声明式事务来实现我们的事务管理。对于事务的隔离级别和传播行为我们需要重点了解。
一、事务基础概念回顾
事务是一个最小的不可再分的工作单元。 一个事务对应一套完整的业务操作。事务管理是指这些操作要么全部成功执行,要么全部回滚,从而保证数据的一致性和完整性。
事务是一组原子性的操作,要么全部成功提交,要么全部失败回滚。它具有 ACID 四个特性:
-
原子性(Atomicity):事务中的操作要么全部完成,要么全部不完成,不会出现部分完成的情况。
-
一致性(Consistency):事务执行前后,数据库的完整性约束不会被破坏。例如,转账操作前后,两个账户的金额总和不变。
-
隔离性(Isolation):多个事务并发执行时,一个事务的执行不能被其他事务干扰。不同的隔离级别决定了事务之间相互影响的程度。
-
持久性(Durability):一旦事务提交,其所做的修改就会永久保存在数据库中,即使系统崩溃也不会丢失。
二、Spring 事务管理的优势
-
简化事务处理代码:使用 Spring 事务管理,开发者无需编写大量繁琐的事务处理代码,如事务的开始、提交、回滚等,只需通过简单的配置或注解即可实现事务管理。
-
支持多种事务管理方式:Spring 提供了编程式事务管理和声明式事务管理两种方式。声明式事务管理通过配置或注解的方式,将事务管理代码从业务逻辑中分离出来,使代码更加简洁,维护更加方便。
-
与多种持久化技术集成:Spring 事务管理可以与各种持久化技术(如 JDBC、Hibernate、JPA 等)无缝集成,无论你使用哪种持久化框架,都能轻松享受 Spring 事务管理带来的便利。
三、Spring对事务的支持
1、Spring实现事务的两种方式
- 编程式事务
通过编写代码的方式来实现事务的管理。
- 声明式事务
基于注解方式
基于XML配置方式
2、Spring事务管理API
Spring对事务的管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装。所以Spring专门针对事务开发了一套API,API的核心接口如下:
PlatformTransactionManager接口:spring事务管理器的核心接口。在Spring6中它有两个实现:
- DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate等事务管理。
- JtaTransactionManager:支持分布式事务管理。(了解即可)
下面我们将介绍一下PlatformTransactionManager接口
1. PlatformTransactionManager
PlatformTransactionManager接口是Spring提供的平台事务管理器,主要用于管理事务。该接口中提供了三个事务操作的方法,具体如下:
PlatformTransactionManager接口只是代表事务管理的接口,并不知道底层是如何管理事务的,具体如何管理事务则由它的实现类来完成。该接口常见的几个实现类如下,当底层采用不同的持久层技术时,系统只需使用不同的PlatformTransactionManager实现类即可。在SSM框架中,我们使用的是MyBatis,因此事务管理器我们选择的是DataSourceTransactionManager。
2. TransactionDefinition
TransactionDefinition接口是事务定义(描述)的对象,该对象中定义了事务基本属性,并提供了获取事务基本属性的方法,具体如下:
2.1. 事务基本属性
- 传播行为(propagation behavior):当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。Spring定义了七种传播行为,如下表所示
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
- 隔离级别(isolation level):定义了一个事务可能受其他并发事务的影响程度。多个事务并发运行,经常会操作相同的数据来完成各自的任务,可能会出现脏读,不可重复读和幻读的问题。隔离级别有四种,如下表
事务隔离级别类型 | 说明 |
---|---|
DEFAULT | Spring中默认的事务隔离级别。以连接的数据库的事务隔离级别为准。 |
READ_UNCOMMITTED | Spring事务最弱的隔离级别。一个事务可以读取到另一个事务未提交的事务记录。容易出现脏读、不可重复读、幻读的问题。 |
READ_COMMITTED | 一个事务只能读取到已经提交的记录,不能读取未提交的记录。可以解决脏读问题,但仍出现不可重复读、幻读的问题。 |
REPEATBLE_READ | 一个事务可以多次从数据库读取某条记录,而且多次读取的那条记录都是一致的、相同的。可以避免脏读、不可重复读的问题,但仍可能出现幻读的问题。 |
SERIALIZABLE | Spring最强的隔离级别,一般不推荐使用。 |
- 是否只读(isReadOnly):如果一个方法内都是对数据库的select操作,那么可以设置方法事务为只读,数据库也会对该事务进行特定的优化。只读事务内不能有insert、update、delete的操作
- 事务超时(timeout):事务可能涉及对后端数据库的锁定,所以长时间的事务运行会不必要的占用数据库资源,设置事务超时时间可以及时释放资源
2.2. DefaultTransactionDefinition
DefaultTransactionDefinition是Spring提供的TransactionDefinition接口的默认实现类,该类定义的事务规则如下,传播行为默认为PROPAGATION_REQUIRED,隔离级别默认为数据库的隔离级别,MySQL的事务隔离级别是REPEATABLE READ。事务超时默认为永不超时并且事务不是只读的。
class DefaultTransactionDefinition implements TransactionDefinition, Serializable {
private int propagationBehavior = PROPAGATION_REQUIRED;
private int isolationLevel = ISOLATION_DEFAULT;
private int timeout = TIMEOUT_DEFAULT;
private boolean readOnly = false;
//略
}
3. TransactionStatus
TransactionStatus接口是事务的状态,它描述了某一时间点上事务的状态信息。该接口中包含6个方法,具体如下:
四、Spring编程式事务管理
本次的用例:在银行账户表(t_account)中有两个账户:用户A(0102030)和用户B(0103040),现在假设用户A需要向用户B转账1000元。
Spring提供了专门用于事务管理的API。要使用Spring实现事务管理需要引入spring-tx依赖,但是在SSM框架中,我们引入的spring-jdbc依赖中已经包含了spring-tx依赖,因此无须额外引入。
<!-- spring-tx依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>6.1.8</version>
</dependency>
使用Spring实现编程式事务管理,需要在业务逻辑方法中,定义事务的开始、正常执行后的事务提交和异常时的事务回滚。具体使用方法如下:
- 在Spring配置文件(spring-mybatis.xml)中配置一个事务管理器组件,提供对事务处理的全面支持和统一管理
<!--定义事务管理器 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--关联数据源-->
<property name="dataSource" ref="dataSource" />
</bean>
- 在业务逻辑组件中使用事务管理器组件实现事务管理功能。
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private DataSourceTransactionManager txManager;
public String moneyTransfer(String accountA, String accountB, double money) {
//定义事务规则(隔离级别、传播行为)
DefaultTransactionDefinition definition=new DefaultTransactionDefinition();
//开启事务管理,并返回事务状态
TransactionStatus status = txManager.getTransaction(definition);
try {
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
txManager.commit(status); //提交事务
return "success";
} catch (Exception ex) {
txManager.rollback(status); //如果出现异常回滚事务
throw ex;
}
}
}
五、Spring声明式事务管理
编程式事务管理必须要在业务逻辑中包含额外的事务管理代码。和业务逻辑代码产生了耦合,产生了代码冗余,不方便代码的维护和扩展。
Spring声明式事务管理最大的优点在于开发者无需通过编程的方式来管理事务,只需在配置文件中进行相关的事务规则声明,就可以将事务应用到业务逻辑中。这使得开发人员可以更加专注于核心业务逻辑代码的编写,在一定程度上减少了工作量,提高了开发效率,所以在实际开发中,通常都推荐使用声明式事务管理。
Spring声明式事务管理通过AOP技术实现的事务管理,主要思想是将事务作为一个“切面”代码单独编写,然后通过AOP技术将事务管理的“切面”植入到业务目标类方法中。
Spring的声明式事务管理可以通过两种方式来实现,一种是基于XML的方式,另一种是基于注解的方式。
基于XML方式实现
基于XML方式的声明式事务是在配置文件中通过**tx:advice元素配置事务规则来实现的,然后通过使用aop:config编写的AOP配置**,让Spring自动对目标生成代理。tx:advice元素及其子元素如下图所示:
结合本次任务,基于XML方式实现银行转账操作的事务管理需要如下三个步骤:
- 添加spring-aspects依赖
因为要使用切面配置AOP相关语法,因此需要引入spring-aop和spring-aspects依赖,根据依赖传递性,spring-webmvc依赖会自动引入spring-aop,因此只需要额外添加spring-aspects依赖即可。
<!--spring和AspectJ整合依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.1.8</version>
</dependency>
- 配置事务管理器
在Spring配置文件中配置事务管理器,事务管理器要关联我们配置中的数据源。(在spring-mybatis.xml中)
<!--定义事务管理器 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--关联数据源-->
<property name="dataSource" ref="dataSource" />
</bean>
- 使用**tx:advice标签配置事务规则**,通过transaction-manager指定事务管理器,在tx:method标签中,通过name指定匹配的方法名,可以使用进行模糊匹配,对于不同类型的方法可以指定不同的事务规则。比如:"find"表示以find开头的方法,传播行为为SUPPRORTS,表示支持当前事务,如果当前没有事务,就以非事务方式执行,read-only为只读。对于"moneyTransfer"方法,传播行为为必须在事务中执行,也就是为该方法加了事务。(在spring-mybatis.xml中)
<!-- 配置事务规则 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<!-- 定义哪些方法需要进行事务处理,*表示任意字符,比如find*表示以find开头的方法 -->
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="moneyTransfer" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
- 使用**aop:config配置事务切面**,通过aop:pointcut配置切点,通过aop:advisor标签的advice-ref属性指定切点应用的事务规则,通过pointcut-ref属性指定关联的切点。(在spring-mybatis.xml中)
<!-- 定义切面 -->
<aop:config>
<!-- 定义切点 -->
<aop:pointcut expression="execution(* com.cg.service.*.*(..))" id="pointcut"/>
<!-- 在指定的切点上应用事务规则 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
</aop:config>
通过以上的配置,我们就可以通过XML配置文件的方式实现全局的配置。
基于注解方式实现
基于注解方式实现是通过使用**@Transactional注解**来实现方法的事务管理功能。
结合本次任务,具体实现步骤如下:
- 配置事务管理器
在Spring配置文件中配置事务管理器,事务管理器要关联我们配置中的数据源。(在spring-mybatis.xml中)
<!--定义事务管理器 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--关联数据源-->
<property name="dataSource" ref="dataSource" />
</bean>
2.在Spring容器中注册事务注解驱动,需要通过transaction-manager属性关联我们的事务管理器(在spring-mybatis.xml中)
<!--注册事务注解驱动-->
<tx:annotation-driven transaction-manager="txManager"/>
3.在需要事务管理的类或方法上使用**@Transactional**注解。
- 如果将注解添加在Bean类上,则表示事务的设置对整个Bean类的所有方法都起作用;
- 如果将注解添加在Bean类中的某个方法上,则表示事务的设置只对该方法有效。
- 使用@Transactional注解时,可以通过参数配置具体事务规则
结合本次任务,我们只需要在moneyTransfer方法上面加上@Transactional注解就可以对银行转账实现事务管理功能了。
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
@Transactional //事务管理注解
public String moneyTransfer(String accountA, String accountB, double money) {
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
return "success";
}
}
拓展
如果是基于配置类方式整合的SSM框架,要使用注解方式开启事务管理功能需要在对应配置类(SpringMyBatisConfig)中注册事务管理器(DataSourceTransactionManager)和开启注解驱动(@EnableTransactionManagement),配置类如下所示:SpringMyBatisConfig.java
@Configuration
@ComponentScan("com.cg.service")
@PropertySource("classpath:db.properties")
@EnableTransactionManagement //开启事务注解驱动
public class SpringMyBatisConfig {
@Bean
public DataSource dataSource(@Value("${jdbc.username}")String user,
@Value("${jdbc.password}")String password,
@Value("${jdbc.url}")String url,
@Value("${jdbc.driver}")String driver){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(user);
dataSource.setPassword(password);
dataSource.setUrl(url);
dataSource.setDriverClassName(driver);
return dataSource;
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
//实例化SqlSessionFactory工厂
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
//设置数据源
sqlSessionFactoryBean.setDataSource(dataSource);
//mybatis框架核心配置
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setAutoMappingBehavior(AutoMappingBehavior.FULL);
configuration.setLogImpl(StdOutImpl.class);
sqlSessionFactoryBean.setConfiguration(configuration);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.cg.entity");
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
//设置mapper接口和xml文件所在的共同包
mapperScannerConfigurer.setBasePackage("com.cg.mapper");
return mapperScannerConfigurer;
}
//向Spring容器中注册事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
PlatformTransactionManager transactionManager=new DataSourceTransactionManager(dataSource);
return transactionManager;
}
}
声明式事务失效场景
- 失效场景1:使用try…catch…代码块捕获异常并处理,未抛出
@Transactional
public String moneyTransfer(String accountA, String accountB, double money) {
try{
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
int i= 1/0;//模拟以外异常
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
return "success";
}catch (Exception ex){
return "fail";
}
}
原因分析:因为异常已经被捕获并处理,异常未抛出导致事务管理器未捕获到异常导致事务失效。
解决方案:去掉try…catch…代码块,或者将异常抛出去
@Transactional
public String moneyTransfer(String accountA, String accountB, double money) {
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
int i= 1/0;//模拟以外异常
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
return "success";
}
- 失效场景2:抛出的异常不是RuntimeException异常及其子类。
@Transactional
public String moneyTransfer(String accountA, String accountB, double money) throws Exception {
try{
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
//模拟抛出异常,异常类型不是RuntimeException
if(accountA.equals(accountB)){
throw new Exception("账户不能为同一个账户");
}
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
return "success";
}catch (Exception ex){
throw ex;
}
}
原因分析:声明式异常处理默认只能捕获处理RuntimeException异常及其子类异常。对于其他类型的异常不会处理。
解决方案1:抛出异常时,异常类型要使用RuntimeException或者其子类,而不是Exception,特别是自定义的业务异常,一定要继承RuntimeException及其子类。
@Transactional
public String moneyTransfer(String accountA, String accountB, double money) throws Exception {
try{
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
//模拟抛出异常,异常类型是RuntimeException及其子类异常
if(accountA.equals(accountB)){
throw new RuntimeException("账户不能为同一个账户");
}
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
return "success";
}catch (Exception ex){
throw ex;
}
}
解决方案2:可以在@Transactional注解中通过配置rollbackFor属性指定异常类型为Exception,表示对所有异常都会出发事务管理机制。
@Transactional(rollbackFor = Exception.class)
public String moneyTransfer(String accountA, String accountB, double money) throws Exception {
try{
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
//模拟抛出异常,异常类型不是RuntimeException
if(accountA.equals(accountB)){
throw new Exception("账户不能为同一个账户");
}
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
return "success";
}catch (Exception ex){
throw ex;
}
}
- 失效场景3:在同一个类中,未加@Transactional注解方法调用了标记有@Transactional注解的方法,事务管理也会失效。
//未加@Transactional注解
public String moneyTransfer(String accountA, String accountB) throws Exception {
return moneyTransfer(accountA,accountB,1000);
}
/**
* 转账操作,用户A账户向用户B账户转账
*
* @param accountA
* 用户A账户编号
* @param accountB
* 用户B账户编号
* @param money
* 转账金额
* @return 转账结果
*/
@Transactional(rollbackFor = Exception.class)
public String moneyTransfer(String accountA, String accountB, double money) throws Exception {
try{
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
//模拟抛出异常,异常类型不是RuntimeException
if(accountA.equals(accountB)){
throw new Exception("账户不能为同一个账户");
}
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
return "success";
}catch (Exception ex){
throw ex;
}
}
原因分析:方法未通过代理对象调用,无法触发事务管理。
解决方案:在每一个方法上都加上@Transactional注解或者尽量避免在同一个类中方法之间互相调用。
@Transactional(rollbackFor = Exception.class)
public String moneyTransfer(String accountA, String accountB) throws Exception {
return moneyTransfer(accountA,accountB,1000);
}
/**
* 转账操作,用户A账户向用户B账户转账
*
* @param accountA
* 用户A账户编号
* @param accountB
* 用户B账户编号
* @param money
* 转账金额
* @return 转账结果
*/
@Transactional(rollbackFor = Exception.class)
public String moneyTransfer(String accountA, String accountB, double money) throws Exception {
try{
System.out.println("开始转账");
// 获取A和B账户的详情
Account A = accountMapper.selectAccount(accountA);
Account B = accountMapper.selectAccount(accountB);
//用户A账户减少相应金额
accountMapper.updateMoney(accountA, A.getMoney() - money);
//模拟抛出异常,异常类型不是RuntimeException
if(accountA.equals(accountB)){
throw new Exception("账户不能为同一个账户");
}
//用户B账户增加相应金额
accountMapper.updateMoney(accountB, B.getMoney() + money);
return "success";
}catch (Exception ex){
throw ex;
}
}