Spring 的事务属性
Spring 的事务属性是用来设置事务管理器的行为特性,可以通过在方法上添加注解或 XML 配置文件中配置来定义事务属性。
以下是 Spring 事务属性中常用的一些选项:
- 传播行为(Propagation):指在嵌套事务中如何控制事务的传播,常用值包括:
- REQUIRED:使用当前事务,如果不存在则创建一个新的事务。
- SUPPORTS:支持当前事务,如果不存在则以非事务方式执行。
- MANDATORY:使用当前事务,如果不存在就抛出异常。
- REQUIRES_NEW:创建一个新的事务,如果当前存在事务则挂起当前事务。
- NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务则挂起当前事务。
- NEVER:以非事务方式执行操作,如果当前存在事务就抛出异常。
- NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前不存在事务,则创建一个事务并执行。嵌套事务可以独立提交或回滚,也可以随父事务一起提交或回滚。
- 隔离级别(Isolation):指并发事务之间如何隔离交错,常用值包括:
- DEFAULT:使用默认隔离级别。(数据库默认的隔离级别,例如 MySQL 默认为 REPEATABLE READ)
- READ_UNCOMMITTED:未提交读,最低的隔离级别,允许读取其他事务未提交的数据,容易出现脏读、不可重复读和幻读等问题。
- READ_COMMITTED:已提交读,保证一个事务提交后另一个事务才能读取修改过的数据,避免脏读问题,但会出现不可重复读和幻读问题。
- REPEATABLE_READ:可重复读,保证一个事务在整个过程中多次读取数据时,数据的状态不发生变化,避免出现脏读和不可重复读问题,但依然会出现幻读问题。
- SERIALIZABLE:串行化,最高的隔离级别,完全隔离各个事务,一次只允许一个事务访问一个数据,也是最耗费资源的隔离级别。
- 回滚规则(Rollback rules):指当哪些异常出现时需要回滚事务,可使用通配符指定回滚规则,如
java.lang.Exception
,也可以使用类名称指定具体的异常类,如com.example.MyException
。 - 读写规则(Read-only rules):指是否允许当前事务进行读写操作,默认值为 false,表示该事务既可以进行读操作也可以进行写操作,当设置为 true 时表示只允许进行读操作。
- 超时时间(Timeout):指事务允许执行的最长时间,超时后事务将被回滚。默认值为 -1,表示不设置超时时间。
事务属性的配置方式
在 Spring 中,可以通过两种方式来设置事务属性:注解方式和 XML 配置方式。无论是注解方式还是 XML 配置方式,都需要事先配置好事务管理器(例如 DataSourceTransactionManager)并指定给 或
的
transaction-manager
属性。
这样,在调用被设置了事务属性的方法时,Spring 会自动开启、提交或回滚事务,以保证数据的一致性和完整性。
注解方式
在类或者方法上添加 @Transactional
注解来设置事务属性。示例代码如下:
@Service public class UserService { @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) public void updateUser(User user) { // 业务逻辑代码 } }
在上述示例中,使用了 @Transactional
注解,并指定了传播行为为 Propagation.REQUIRED
、隔离级别为 Isolation.READ_COMMITTED
。
XML 配置方式
在 Spring 的配置文件(如 applicationContext.xml
)中使用 和
标签来配置事务属性。示例配置代码如下:
<bean id="userService" class="com.example.UserService"> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="updateUser" propagation="REQUIRED" isolation="READ_COMMITTED"/> </tx:attributes> </tx:advice> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.example.UserService.updateUser(..))"/> </aop:config> <tx:annotation-driven transaction-manager="transactionManager"/>
传播行为(Propagation)
Spring 的事务传播属性主要用于解决多个事务方法之间相互调用时,事务如何进行传播和管理的问题。当一个事务方法调用另一个事务方法时,事务传播属性定义了被调用方法应该如何处理事务。大事务中嵌套了很多小事务,它们彼此影响,最终导致最外层大的事务丧失了事务的原子性。
事务传播属性可以控制事务的边界和范围,以确保数据的一致性和完整性。它解决了以下几个问题:
- 嵌套事务:当一个事务方法内部调用另一个事务方法时,是否创建一个新的事务或者加入已存在的事务。嵌套事务允许在一个事务中存在多个子事务,每个子事务都有自己的保存点,可以独立地进行提交或回滚。通过事务传播属性,可以控制是否开启新的嵌套事务。
- 事务边界:当一个事务方法被另一个非事务方法调用时,是否开启新的事务。如果事务方法被非事务方法调用,那么根据事务传播属性的设置,可以选择开启新的事务或者不开启事务。
- 多事务方法协作:当多个事务方法相互协作完成一个复杂的业务逻辑时,事务传播属性可以确保这些方法都在同一个事务中执行,以保证数据的一致性。通过将事务传播属性设置为REQUIRED,可以要求被调用方法必须在一个已存在的事务中执行,如果不存在事务,则会开启新的事务。
- 事务的隔离性:事务传播属性还可以影响事务的隔离级别。当一个事务方法被另一个事务方法调用时,事务传播属性可以决定被调用方法使用的事务隔离级别。例如,如果将事务传播属性设置为 REQUIRES_NEW,被调用方法将在一个新的事务中执行,使用独立的隔离级别。
Spring 的事务传播属性可以配置以下七个值,每个值都代表不同的含义和行为:
- REQUIRED:如果当前已经存在一个事务,则加入该事务,否则新建一个事务。这是默认值。主要用于增删改的方法。
- SUPPORTS:如果当前已经存在一个事务,则加入该事务,否则以非事务的方式执行。主要应用在查询方法。
- REQUIRES_NEW:每次都会新建一个事务,如果当前已经存在一个事务,则将当前事务挂起(暂停),始终采用独立事务方法,主要用于日志记录的方法。
- MANDATORY:如果当前已经存在一个事务,则加入该事务,否则抛出异常,不常用。
- NOT_SUPPORTED:以非事务的方式执行操作,如果当前存在一个事务,则将当前事务挂起(暂停),不常用。
- NEVER:以非事务的方式执行操作,如果当前存在一个事务,则抛出异常,不常用。
- NESTED:如果当前已经存在一个事务,则在该事务内嵌套一个子事务,否则新建一个事务。如果子事务失败,则只回滚子事务,而不回滚父事务。如果父事务失败,则回滚所有事务。
隔离级别(Isolation)
Spring 的事务隔离属性主要是用来解决并发事务可能导致的数据不一致问题(多个事务在相差极小的同一时间操作相同的数据)。在数据库中,多个事务同时进行时,可能会出现脏读(Dirty Read)、不可重复读(Non-Repeatable Read)、幻读(Phantom Read)和丢失更新(Lost Update)等问题。事务隔离级别就是用来解决这些问题的。
Spring 事务管理的隔离级别有以下几种:
- DEFAULT:这是默认值,使用后端数据库默认的隔离级别。大多数数据库默认的事务隔离级别就是 READ_COMMITTED。
- READ_UNCOMMITTED:这是最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会导致很多并发问题,如脏读、不可重复读和幻读。
- READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另一个事务不能读取该事务未提交的数据。
- REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本事务自己修改。
- SERIALIZABLE:这是最高的隔离级别,所有的事务依次逐个执行,即事务序列化,完全避免了并发问题。但是这也是代价最大的事务隔离级别,因为它会导致数据库性能明显下降。
需要注意的是,较高的隔离级别可能会对并发性能产生一定的影响,因此在选择隔离级别时需要综合考虑应用程序的并发访问情况和性能需求。
脏读
一个事务读取了另一个事务未提交的数据。
时间 | 事务 A(存款) | 事务 B(取款) |
T1 | 开始事务 | - |
T2 | - | 开始事务 |
T3 | - | 查询余额(当前余额 1000 元) |
T4 | - | 取出余额(当前余额 0 元) |
T5 | 查询余额(当前余额 0 元) | - |
T6 | - | 撤销事务(当前余额 1000 元) |
T7 | 存入500元(当前余额 500元) | - |
T8 | 提交事务 | - |
图中表格可以分析出,事务 A 读取到了 事务 B 未提交的事务,以为余额为 0 元,但是此时事务 B 撤销事务后,事务 A 将当前余额(读取的脏数据 0 元)更新为 500 元。
Spring 解决方案:
@Transactional(isolation = Isolation.READ_COMMITTED)
不可重复读
在同一事务内,多次读取同一数据返回的结果有所不同。
时间 | 事务 A(存款) | 事务 B(取款) |
T1 | 开始事务 | - |
T2 | 查询余额(当前余额 1000 元) | 开始事务 |
T3 | - | 查询余额(当前余额 1000 元) |
T4 | - | 取出余额(当前余额 0 元) |
T5 | - | 提交事务(当前余额 0 元) |
T6 | 再次查询(当前余额 0 元) | - |
图中表格可以分析出,事务 A 第一次查询的时候余额为 1000,此时事务 B 操作了数据,将余额更新为 0 并提交,事务 A 再次查询的时候余额变成了 0。因为事务 B 更新数据后已经提交,所以事务 A 再次查询到的数据不是脏数据。
Spring 解决方案(本质上是在数据库此条记录加入了行级锁):
@Transactional(isolation = Isolation.REPEATABLE_READ)
幻读
在一个事务内读取了几行记录后,另一个并发事务插入了一些记录,之后,第一个事务再次读取记录,发现多了几行。
时间 | 事务 A(统计金额) | 事务 B(新建账户) |
T1 | 开始事务 | - |
T2 | 统计总存款金额为 10000 元 | 开始事务 |
T3 | - | - |
T4 | - | 新增银行账户,存入 2000 元 |
T5 | - | 提交事务 |
T6 | 再次统计总存款金额为 12000 元 | - |
图中表格可以分析出,事务 A 再次统计时,读取到了 事务 B 提交的新增数据。造成了两次统计结果不一致的情况。
不可重复读和幻读的区别是:前者是指读到了已经提交的事务的更改数据(修改或删除),后者是指读到了其他已经提交事务的新增数据。
不可重复读需要添加行级锁,幻读需要加表级锁。
Spring 解决方案(本质上是在数据库此条记录加入了表级锁):
@Transactional(isolation = Isolation.SERIALIZABLE)
丢失更新
两个并发的事务都读取了同一个数据库记录,然后基于这个记录的当前值,都做了修改,然后第一个事务把它的修改写进数据库,然后第二个事务也把它的修改写进数据库,覆盖了第一个事务的修改结果。
Spring 解决方案(本质上是在数据库此条记录加入了行级锁):
@Transactional(isolation = Isolation.REPEATABLE_READ)
数据库对于隔离属性的支持
数据库 | READ_COMMITTED | REPEATABLE_READ | SERIALIZABLE |
MySQL | 支持 | 支持 | 支持 |
Oracle | 支持 | 不支持 | 支持 |
Oracle 不支持 REPEATABLE_READ 属性,它是通过多版本比对的方式解决不可重复读的问题。
默认隔离属性
如果我们没有在 @Transactional 注解中指定 isolation,那么 Spring 会默认指定为 ISOLATION_DEFAULT,表示按照对应的数据库的默认隔离属性。
MySQL 的默认隔离属性为 REPEATABLE_READ,可以通过以下 SQL 查看:
select @@tx_isolation;
Oracle 的默认隔离属性为 READ_COMMITTED,可以通过以下 SQL 查看:
SELECT s.sid, s.serial#, CASE BITAND(t.flag, POWER(2, 28)) WHEN 0 THEN 'READ COMMITTED' ELSE 'SERIALIZABLE' END AS isolation_level FROM v$transaction t JOIN v$session s ON t.addr = s.taddr AND s.sid = sys_context('USERENV', 'SID');
隔离属性在实战中的建议
推荐使用 Spring 默认指定的 ISOLATION_DEFAULT,会根据不同的数据库,选择不同的隔离级别,而且并发的情况其实非常少,真遇到并发可以通用 MyBatis 自定义拦截器开发乐观锁的方式来解决(进行版本比对)。
回滚规则(Rollback rules)
Spring 的事务异常属性主要解决事务回滚和异常处理的问题。
在 Spring 中,事务管理器会捕获事务方法中抛出的异常,并根据异常类型和事务配置进行相应的处理。通过设置事务异常属性,可以指定哪些异常需要回滚事务,哪些异常需要忽略或转换为其他异常类型。
具体来说,Spring 的事务异常属性包括以下几种:
- rollbackFor:指定哪些异常需要回滚事务。当事务方法抛出指定类型的异常时,事务管理器将回滚当前事务。例如,
@Transactional(rollbackFor = RuntimeException.class)
表示当事务方法抛出 RuntimeException 及其子类异常时,事务将回滚,默认值。 - noRollbackFor:指定哪些异常不需要回滚事务。当事务方法抛出指定类型的异常时,事务管理器将不会回滚当前事务。例如,
@Transactional(noRollbackFor = IOException.class)
表示当事务方法抛出 IOException 及其子类异常时,事务不会回滚。 - rollbackForClassName:与 rollbackFor 类似,但是指定异常类型的全限定类名。例如,
@Transactional(rollbackForClassName = "java.lang.RuntimeException")
表示当事务方法抛出 RuntimeException 及其子类异常时,事务将回滚。 - noRollbackForClassName:与 noRollbackFor 类似,但是指定异常类型的全限定类名。例如,
@Transactional(noRollbackForClassName = "java.io.IOException")
表示当事务方法抛出 IOException 及其子类异常时,事务不会回滚。
通过设置事务异常属性,可以灵活地控制事务的回滚和异常处理。这可以避免因为一些不必要的异常导致事务回滚,从而提高系统的可靠性和性能。同时,也可以将一些不可避免的异常转换为其他异常类型,以便更好地进行异常处理和日志记录。
Spring 默认对捕获 RuntimeException 类以及子类执行回滚操作,对 Exception 以及子类执行提交操作。
建议:实战中使用 RuntimeExceptin 及其子类,使用事务异常属性的默认值
@Transactional(rollbackFor = {Exception.class, RuntimeException.class})
读写规则(Read-only rules)
Spring 的事务的只读属性主要解决对于只读操作的优化问题。
当一个事务方法中只包含读取数据库的操作,而没有任何修改数据的操作时,可以将该事务标记为只读事务。通过设置只读属性,可以告诉数据库引擎在执行这个事务期间采取一些优化措施,以提高性能和并发度。
只读事务的主要优势在于:
- 提高性能:只读事务不需要对数据进行修改和锁定,因此可以减少数据库引擎的工作量和资源消耗。数据库引擎可以针对只读事务进行一些优化,例如跳过日志记录、减少锁的竞争等,从而提高查询性能。
- 增加并发度:只读事务不会对数据进行修改,因此多个只读事务可以并发地执行,而不会相互干扰。这可以提高系统的并发度和吞吐量,减少用户的等待时间。
- 减少风险:只读事务不会修改数据,因此不会引入数据一致性的风险。如果一个方法只需要读取数据而不需要修改数据,将其标记为只读事务可以确保数据的完整性和一致性。
需要注意的是,只有在确定方法确实只涉及读取操作时,才应该将事务标记为只读。如果方法中包含了任何修改数据的操作,将其标记为只读事务可能导致数据不一致的问题。
@Transactional 注解中,只读属性默认为 false,要开启需要手动配置。
@Transactional(readOnly = true)
在开发中我们需要在只发出查询语句的方法上添加 @Transactional(readOnly = true)
注解,将之申明为只读事务。
- 多条查询下要使用该注解,能够防止多次查询到的数据不一致(维持可重复读),而且有一定的优化
- 尽管在单条查询下不会出现数据不一致现象,但是使用
@Transactional(readOnly = true)
注解能够优化查询,源码中提到 readOnly = true 也存在着可能的优化
加上 @Transactional(readOnly = true)
可以保证读一致性和查询优化以及一些可能的优化,即使数据库和驱动底层不支持 readOnly 属性,那也不会报错。我们何乐而不为呢?
超时时间(Timeout)
Spring 的事务超时属性主要解决事务执行时间过长导致资源浪费和阻塞问题。
当一个事务方法执行时间过长,可能会占用数据库连接和其他资源,导致其他事务无法及时获取到资源,从而产生阻塞和性能问题。为了避免这种情况的发生,可以设置事务超时属性,告诉 Spring 事务管理器在一定时间内强制结束当前事务。
具体来说,Spring 的事务超时属性可以指定一个时间限制(以秒为单位,超出指定时间将抛出异常:TransactionTimedOutException),如果事务方法执行时间超过这个限制,则事务管理器将自动回滚该事务,并释放相关资源。这可以避免事务执行时间过长导致资源浪费和阻塞的问题,提高系统的可靠性和性能。
需要注意的是,事务超时属性只对长时间运行的事务有效。对于短时间运行的事务,设置超时属性可能会增加系统的开销和复杂性。因此,在使用事务超时属性时,应该根据具体业务需求和系统性能进行权衡和调整。
// 当前事务最多等待 5 秒 @Transactional(timeout = 5)
超时属性的默认值为:-1,表示由对应的数据库来决定。
事务属性常用配置总结
- 隔离属性:默认即可
- 传播属性:对于增删改方法使用 Required(默认值),对于查询方法使用 Supports
- 回滚规则:默认即可
- 读写规则:对于增删改方法使用 false(默认值),对于查询方法使用 true
- 超时时间:默认即可
增删改的配置如下:
@Transactional
查询的配置如下:
@Transactional(propagation=Propagation.SUPPORTS, readOnly=true)