Spring 的事务属性是用来设置事务管理器的行为特性,可以通过在方法上添加注解或 XML 配置文件中配置来定义事务属性。
以下是 Spring 事务属性中常用的一些选项:
java.lang.Exception
,也可以使用类名称指定具体的异常类,如 com.example.MyException
。在 Spring 中,可以通过两种方式来设置事务属性:注解方式和 XML 配置方式。无论是注解方式还是 XML 配置方式,都需要事先配置好事务管理器(例如 DataSourceTransactionManager)并指定给 <tx:annotation-driven>
或 <tx:advice>
的 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
。
在 Spring 的配置文件(如 applicationContext.xml
)中使用 <tx:advice>
和 <tx:annotation-driven>
标签来配置事务属性。示例配置代码如下:
<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"/>
Spring 的事务传播属性主要用于解决多个事务方法之间相互调用时,事务如何进行传播和管理的问题。当一个事务方法调用另一个事务方法时,事务传播属性定义了被调用方法应该如何处理事务。大事务中嵌套了很多小事务,它们彼此影响,最终导致最外层大的事务丧失了事务的原子性。
事务传播属性可以控制事务的边界和范围,以确保数据的一致性和完整性。它解决了以下几个问题:
Spring 的事务传播属性可以配置以下七个值,每个值都代表不同的含义和行为:
Spring 的事务隔离属性主要是用来解决并发事务可能导致的数据不一致问题(多个事务在相差极小的同一时间操作相同的数据)。在数据库中,多个事务同时进行时,可能会出现脏读(Dirty Read)、不可重复读(Non-Repeatable Read)、幻读(Phantom Read)和丢失更新(Lost Update)等问题。事务隔离级别就是用来解决这些问题的。
Spring 事务管理的隔离级别有以下几种:
需要注意的是,较高的隔离级别可能会对并发性能产生一定的影响,因此在选择隔离级别时需要综合考虑应用程序的并发访问情况和性能需求。
一个事务读取了另一个事务未提交的数据。
时间 | 事务 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 自定义拦截器开发乐观锁的方式来解决(进行版本比对)。
Spring 的事务异常属性主要解决事务回滚和异常处理的问题。
在 Spring 中,事务管理器会捕获事务方法中抛出的异常,并根据异常类型和事务配置进行相应的处理。通过设置事务异常属性,可以指定哪些异常需要回滚事务,哪些异常需要忽略或转换为其他异常类型。
具体来说,Spring 的事务异常属性包括以下几种:
@Transactional(rollbackFor = RuntimeException.class)
表示当事务方法抛出 RuntimeException 及其子类异常时,事务将回滚,默认值。@Transactional(noRollbackFor = IOException.class)
表示当事务方法抛出 IOException 及其子类异常时,事务不会回滚。@Transactional(rollbackForClassName = "java.lang.RuntimeException")
表示当事务方法抛出 RuntimeException 及其子类异常时,事务将回滚。@Transactional(noRollbackForClassName = "java.io.IOException")
表示当事务方法抛出 IOException 及其子类异常时,事务不会回滚。通过设置事务异常属性,可以灵活地控制事务的回滚和异常处理。这可以避免因为一些不必要的异常导致事务回滚,从而提高系统的可靠性和性能。同时,也可以将一些不可避免的异常转换为其他异常类型,以便更好地进行异常处理和日志记录。
Spring 默认对捕获 RuntimeException 类以及子类执行回滚操作,对 Exception 以及子类执行提交操作。
建议:实战中使用 RuntimeExceptin 及其子类,使用事务异常属性的默认值
@Transactional(rollbackFor = {Exception.class, RuntimeException.class})
Spring 的事务的只读属性主要解决对于只读操作的优化问题。
当一个事务方法中只包含读取数据库的操作,而没有任何修改数据的操作时,可以将该事务标记为只读事务。通过设置只读属性,可以告诉数据库引擎在执行这个事务期间采取一些优化措施,以提高性能和并发度。
只读事务的主要优势在于:
需要注意的是,只有在确定方法确实只涉及读取操作时,才应该将事务标记为只读。如果方法中包含了任何修改数据的操作,将其标记为只读事务可能导致数据不一致的问题。
@Transactional 注解中,只读属性默认为 false,要开启需要手动配置。
@Transactional(readOnly = true)
在开发中我们需要在只发出查询语句的方法上添加 @Transactional(readOnly = true)
注解,将之申明为只读事务。
@Transactional(readOnly = true)
注解能够优化查询,源码中提到 readOnly = true 也存在着可能的优化加上 @Transactional(readOnly = true)
可以保证读一致性和查询优化以及一些可能的优化,即使数据库和驱动底层不支持 readOnly 属性,那也不会报错。我们何乐而不为呢?
Spring 的事务超时属性主要解决事务执行时间过长导致资源浪费和阻塞问题。
当一个事务方法执行时间过长,可能会占用数据库连接和其他资源,导致其他事务无法及时获取到资源,从而产生阻塞和性能问题。为了避免这种情况的发生,可以设置事务超时属性,告诉 Spring 事务管理器在一定时间内强制结束当前事务。
具体来说,Spring 的事务超时属性可以指定一个时间限制(以秒为单位,超出指定时间将抛出异常:TransactionTimedOutException),如果事务方法执行时间超过这个限制,则事务管理器将自动回滚该事务,并释放相关资源。这可以避免事务执行时间过长导致资源浪费和阻塞的问题,提高系统的可靠性和性能。
需要注意的是,事务超时属性只对长时间运行的事务有效。对于短时间运行的事务,设置超时属性可能会增加系统的开销和复杂性。因此,在使用事务超时属性时,应该根据具体业务需求和系统性能进行权衡和调整。
// 当前事务最多等待 5 秒
@Transactional(timeout = 5)
超时属性的默认值为:-1,表示由对应的数据库来决定。
增删改的配置如下:
@Transactional
查询的配置如下:
@Transactional(propagation=Propagation.SUPPORTS, readOnly=true)