目录
Spring 事务
数据库事务
数据库事务(transaction)是一组数据库的操作,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。事务具有以下特性:
- 原子性(Atomicity):事务被视为不可分割的最小工作单位,事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的部分操作,这就是事务的原子性。例如,在银行账户转账中,从A账户扣款和向B账户存款这两个操作要么都成功,要么都失败,不会只执行其中一个操作。
- 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。一致性与原子性是密切相关的。事务在执行前后,数据库都必须处于一致性状态。这意味着事务执行的结果必须是合法的,即满足所有的完整性约束。以转账为例,转账前后,两个账户的总金额应该是一致的,不会凭空多或少了一部分。
- 隔离性(Isolation):数据库系统提供一定级别的隔离,使得多个事务并发执行时,一个事务的执行不会被其他事务的干扰。隔离性通过锁机制和并发控制来实现。不同的隔离级别对应不同的事务干扰程度,常见的隔离级别有:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
- 持久性(Durability):指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
前置准备
Spring 中支持声明式事务和编程式事务,要使用 Spring 事务,需要引入以下依赖:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
基本概念
TransactionManager
org.springframework.transaction.TransactionManager
TransactionManager 是一个标记接口,该接口没有任何方法,用于标记一个类是一个事务管理器。
Spring 通过 TransactionManager 的子接口 PlatformTransactionManager 接口来实际管理事务。
PlatformTransactionManager
org.springframework.transaction.PlatformTransactionManager
PlatformTransactionManager 定义了与事务相关的基本操作方法,包括开始、提交和回滚事务。
PlatformTransactionManager 接口定义了以下三个核心方法:
- TransactionStatus getTransaction (TransactionDefinition definition)
- 描述:根据传入的 TransactionDefinition,获取当前事务的状态。如果存在现有事务且传播行为允许,则返回当前事务;否则,开启一个新事务。
- 参数:TransactionDefinition definition,事务的定义,包括传播行为、隔离级别、超时时间等。
- 返回值:TransactionStatus 对象,表示当前事务的状态。
- void commit (TransactionStatus status)
- 描述:提交事务。如果事务中有任何更改,它们将被持久化到数据库。
- 参数:TransactionStatus status,表示当前事务的状态。
- void rollback (TransactionStatus status)
- 描述:回滚事务,撤销事务中的所有更改。
- 参数:TransactionStatus status,表示当前事务的状态。
基于注解实现声明式事务
声明式事务是一种通过配置来管理事务的方式,而不是通过编程方式。它允许开发者将事务管理代码从业务逻辑代码中分离出来,从而提高了代码的可读性和可维护性。Spring 通过 AOP(面向切面编程)来实现声明式事务管理,使得事务管理变得透明和灵活。
事务管理流程
- 使用 @EnableTransactionManagement 开启事务功能
- 在 Spring 容器启动时,会创建一个 TransactionInterceptor 类型的 bean
- 接下来,Spring 容器在扫描所有 @Component 的组件(包括派生的,例如 @Service)时,会顺带扫描并解析 @Transactional。如果发现 @Transactional 标注的类,则会创建代理对象,将 TransactionInterceptor 的功能加强到被代理的对象,扫描 TransactionInterceptor 这一步在 SpringTransactionAnnotationParser 这个类完成。
- Spring 使用 AOP 机制创建代理对象,通过代理模式将 TransactionInterceptor 植入被代理的对象。
- 当调用事务方法,Spring 的事务拦截器(TransactionInterceptor)会被触发,事务拦截器会根据当前事务上下文和传播行为(Propagation),决定是创建一个新的事务,还是加入现有的事务。假设当前没有事务上下文,并且方法使用默认的 REQUIRED 传播行为,那么拦截器会创建一个新的事务。
- 方法执行完毕后,控制权返回到代理对象的事务拦截器。拦截器会根据方法的执行情况(是否抛出了异常)决定提交或回滚事务。3
使用步骤
- 添加事务依赖
- 设法(配置类或 XML)配置一个 TransactionManager,下面是一个 XML 的例子:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="druidDataSource"/> </bean> <!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务 --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 --> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
- 添加事务注解 @Transactional 在需要事务的方法上,@Transactional 标识在方法上,则只会影响该方法,@Transactional 标识的类上,则会影响类中所有的方法。
@Transactional
只读
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化:
@Transactional(readOnly = true)
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
}
超时
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是 Java 程序或 MySQL 数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
//超时时间单位秒
@Transactional(timeout = 3)
public void buyBook(Integer bookId, Integer userId) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
}
回滚策略
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
-
rollbackFor 属性:需要设置一个Class类型的对象
-
rollbackForClassName 属性:需要设置一个字符串类型的全类名
-
noRollbackFor 属性:需要设置一个Class类型的对象
-
rollbackFor 属性:需要设置一个字符串类型的全类名
隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL 标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
-
读未提交(READ UNCOMMITTED):允许一个事务读取到另一个事务未提交的修改。
-
读已提交(READ COMMITTED): 要求一个事务只能读取到另一个事务已提交的修改。
-
可重复读(REPEATABLE READ):确保一个事务可以多次从一个字段中读取到相同的值,即该事务执行期间禁止其它事务对这个字段进行更新。
-
串行化(SERIALIZABLE):确保一个事务可以多次从一个表中读取到相同的行,在该事务执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
各种数据库产品对事务隔离级别的支持程度:
隔离级别 | Oracle | MySQL |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √ (默认) | √ |
REPEATABLE READ | × | √ (默认) |
SERIALIZABLE | √ | √ |
使用方式:
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
传播行为
在 service 类中有 a() 方法和 b() 方法,a() 方法上有事务,b() 方法上也有事务,当 a() 方法执行过程中调用了 b() 方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
一共有七种传播行为:
- REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行 【有就加入,没有就不管了】
- MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常 【有就加入,没有就抛异常】
- REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起 【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
- NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务 【不支持事务,存在就挂起】
- NEVER:以非事务方式运行,如果有事务存在,抛出异常 【不支持事务,存在就抛异常】
- NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和 REQUIRED 一样。】
可以通过 @Transactional 中的 propagation 属性设置事务传播行为。