本文原文链接
45岁老架构 尼恩说在前面
在45岁老架构师 尼恩的读者交流群(100+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的相关面试题:
- 什么是Spring事务?
- Spring事务失效的10 种常见场景?
- Spring加入型事务和嵌套型事务有什么区别?
- spring事务隔离级别与数据库事务隔离级别的关系?
最近有小伙伴面试美团、JD,都问到了这个面试题。 小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
基础知识:Spring 两种事务管理方式
Spring 支持两种事务管理方式:编程式事务和声明式事务。
事务分为 编程式事务 和声明式事务两种。
- 编程式事务指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强。
- 声明式事务是通过配置来实现的,不需要在代码中显式地管理事务。
编程式事务是指在代码中显式地开启、提交或回滚事务。这种方式需要在代码中编写事务管理的相关逻辑,比较繁琐,但是灵活性较高,可以根据具体的业务需要进行定制。
关于 编程式事务 是如何实现,请参见尼恩架构团队的 顶奢好文:
声明式事务是通过配置来实现的,不需要在代码中显式地管理事务。这种方式需要在配置文件中声明事务的属性,比如事务的传播行为、隔离级别等。声明式事务的好处是可以将事务管理的逻辑与业务逻辑分离,使得代码更加简洁、清晰,同时也方便了事务管理的统一配置和维护。
在 Spring 中,声明式事务 是基于 AOP 面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,声明式事务也有两种实现方式。
关于 声明式事务 是如何基于 AOP 面向切面实现,请参见尼恩架构团队的 顶奢好文:
Spring 提供了两种声明式事务的方式:
- 基于 XML 配置
- 基于注解配置。
基于 XML 配置的方式需要在 Spring 配置文件中声明事务管理器和事务通知等相关信息,
而基于注解配置的方式则可以在代码中通过注解来声明事务的属性,比如 @Transactional。一种是基于 TX 和 AOP 的 xml 配置文件方式,二种就是基于 @Transactional 注解了,实际开发中 @Transactional 用的比较多。
声明式事务1:基于 XML 配置文件进行配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
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/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">
<!-- 开启扫描 -->
<context:component-scan base-package="com.dpb.*"></context:component-scan>
<!-- 配置数据源 -->
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl"/>
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="username" value="pms"/>
<property name="password" value="pms"/>
</bean>
<!-- 配置JdbcTemplate -->
<bean class="org.springframework.jdbc.core.JdbcTemplate" >
<constructor-arg name="dataSource" ref="dataSource"/>
</bean>
<!--
Spring中,使用XML配置事务三大步骤:
1. 创建事务管理器
2. 配置事务方法
3. 配置AOP
-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="fun*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- aop配置 -->
<aop:config>
<aop:pointcut expression="execution(* *..service.*.*(..))" id="tx"/>
<aop:advisor advice-ref="advice" pointcut-ref="tx"/>
</aop:config>
</beans>
声明式事务2:基于注解的声明式配置
一般来说,更加推荐声明式事务比编程式事务,因为它可以使代码更加简洁、清晰,同时也方便了事务管理的统一配置和维护。
所以,这里使用 声明式事务 进行演示,并且是使用 基于注解配置的 声明式事务。
首先必须要添加 @EnableTransactionManagement 注解,保证事务注解生效
@EnableTransactionManagement
public class AnnotationMain {
public static void main(String[] args) {
}
}
其次,在方法上添加 @Transactional 代表注解生效
@Transactional
public int insertUser(User user) {
userDao.insertUser();
userDao.insertLog();
return 1;
}
下面的案例,用到基于注解的声明式配置,具体的注解是 @Transactional。
@Transactional 注解的使用
@Transactional 可以作用在类上,当作用在类上的时候,表示所有该类的 public 方法都配置相同的事务属性信息。
@Transactional 也可以作用在方法上,当方法上也配置了 @Transactional,方法的事务会覆盖类的事务配置信息。
我们日常操作里,对于单个方法使用事物,经常是这样:
@Transactional(rollbackFor = Exception.class)
public Boolean add(UserInfo userInfo) {
//... 业务处理
//... 业务处理
//... 业务处理
//手动抛异常 触发回滚等
retrun xxx;
}
或者说配合手动回滚使用,是这样:
@Transactional(rollbackFor = Exception.class)
public Boolean add(UserInfo userInfo) {
try {
//....业务逻辑处理
if(XXXX){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return false;
}
//....业务逻辑处理
if(xxxxx){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return false;
}
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return false;
}
}
以上都是单个事物方法,理解起来很简单,相信大多数场景大家就这么用一下就没有过多去理会了。
首先,我们通过看 @Transactional 的源码来和大家重新认识一下 @Transactional 的用法。
@Transactional 注解 涉及到的 5大属性
总体来说,事务属性包含了5个方面,如图所示:
@Transactional 源码
transactionManager 和 value 是同一个配置项的两个别名:
大多数项目只需要一个事务管理器,但是在有些项目中为了提高效率、或者有多个完全不同又不相干的数据源,所以会有多个事务管理器,这里填的就是你想用的事务管理器的 Bean 的 id。
propagation属性: Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。
这个后面详细介绍。
isolation属性: 是事务的隔离级别,默认值为 Isolation.DEFAULT。
这里有四个隔离级别,具体这四个级别是什么意思 :
Isolation.DEFAULT:使用底层数据库默认的隔离级别。
Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
在Innodb里面默认用的是 RR 级别,
timeout属性: 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
readOnly属性 : 指定事务是否为只读事务,默认值为false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollbackFor属性 : 用于指定能够触发事务回滚的 异常 类型,可以指定多个异常类型。
第一大属性:@Transactional 注解 的 传播机制
什么叫做事务的传播?
Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。
尼恩给大家举个 生动的例子.
比如,有三个 业务 方法,第一个业务 方法如下:
class testOne
{
@Transactional(rollbackFor = Exception.class)
public Boolean addOne(UserInfo userInfo) {
//... 业务处理
//... 业务处理
//... 业务处理
retrun xxx;
}
}
第二个业务 方法如下:
class testTwo
{
@Transactional(rollbackFor = Exception.class)
public Boolean addTwo(UserInfo userInfo) {
//... 业务处理
//... 业务处理
//... 业务处理
retrun xxx;
}
}
然后第三个 业务 方法如下:
class testThree
{
@Transactional(rollbackFor = Exception.class)
public Boolean testThree(UserInfo userInfo) {
addOne(xxxx);
addTwo(xxxx);
retrun xxx;
}
}
那么, 三个 业务 方法 之间:
- 是每一个 业务方法开启一个 新的独立的事务?
- 还是 第一个 业务 方法、 第二个 业务 方法 加入到 第三个 业务 方法 开启的事务?
- 还是 第一个 业务 方法、 第二个 业务 方法 各自开一个 NESTED 内嵌事务, 以局部事务的 加入到 第三个 业务 方法 开启的整体事务?
Spring定义了七种传播行为
使用spring声明式事务,自动在方法调用之前 (进入一个新的方法),spring会根据事务属性去决定是否开一个事务,并在方法执行之后,决定事务提交或回滚事务。这就是事务的传播。
Spring定义了七种传播行为:
传播行为 | 含义 |
---|---|
PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 |
PROPAGATION_SUPPORTS | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 |
PROPAGATION_MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
PROPAGATION_NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
事务7种传播机制 对应的源码如下:
Spring 事务传播机制分为 3 大类,总共 7 种级别,如下图所示:
当我们不指定的时候, 默认使用的是 Propagation.REQUIRED。
1.1 支持当前事务 的三种传播方式
支持当前事务的传播机制有三种,分别是
- 第一种传播: 加入当前事务 REQUIRED
所谓的加入当前事务,是指如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
所谓 当前事务 ,其实是用词 稍有有点错误, 其实 指的是 上一层方法的事务 。
含义:如果上一层方法 已经存在一个事务中,则加入到这个事务中; 如果上一层 方法没有事务,当前层方法 新建一个事务 。
REQUIRED 加入当前事务 , 这是 默认的 传播机制。
第二种 传播: 支持当前事务 SUPPORTS
支持一下当前 事务,是指如果当前存在事务,则加入该事务;如果当前没有事务, 就以非事务方式执行
所谓 当前事务 ,其实是用词 稍有有点错误, 其实 指的是 上一层方法的事务 。
含义:支持上一层 方法的 事务,如果上一层 方法没有事务, 那么,当前层方法 就以非事务方式执行
- 第三种 传播: MANDATORY 强制当前事务
强制一下当前 事务,是指如果当前存在事务,则加入该事务;如果当前没有事务, 就抛出 异常 。
含义:如果 上一层 方法 没事务,那么,当前层方法 就抛出 异常 。
1.2 不支持当前事务的三种传播方式
- 第4种 传播: REQUIRES_NEW
含义:新建事务,如果当前存在事务,把当前事务挂起。
- 第5种 传播: NOT_SUPPORTED
含义:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- 第6种 传播: NEVER
含义: 以非事务方式执行,如果当前存在事务,则抛出异常。
1.3 NESTED 事务嵌套
- 第7种 传播: NESTED 事务嵌套
含义: 如果当前存在事务,则在嵌套事务内执行。
如果当前没有事务,则执行与 REQUIRED类似的操作, 创建一个新的事务。
NESTED事务嵌套和 加入事务(REQUIRED)的主要区别在于 :
NESTED事务 的特点如下 :
当存在外部事务时,NESTED会创建一个嵌套的子事务,这个子事务有自己的保存点(savepoint)。
如果嵌套事务中发生异常,它只会回滚到自己的保存点(savepoint),而不影响外部事务。
因此, NESTED事务可以实现部分事务的回滚,或者说 子事务部分回滚( 只有嵌套事务内的部分操作会被回滚),而外部事务的其他部分可以继续执行。
加入事务(REQUIRED)的特点如下 :
如果当前存在事务,则REQUIRED会加入到当前事务中,作为当前事务的一部分;
- 如果当前没有事务,则创建一个新的事务。
- 在REQUIRED传播级别下,如果遇到异常,整个事务(外部事务,包括嵌套之前的所有操作)将会回滚。
总结来说:
- NESTED事务允许在当前事务中创建一个新的子事务,这个子事务可以独立于外部事务进行回滚
- 而REQUIRED事务则会与外部事务一形成一个整体,同生共死,一起回滚。
- NESTED事务通过保存点(savepoint)实现部分回滚,而REQUIRED事务则是整个事务的回滚。
默认的传播行为:加入当前事务 REQUIRED
除了Propagation.REQUIRED, 另外两个常用的是 Propagation.REQUIRES_NEW 和 Propagation.NESTED。
除了这个三个, 而另外四种我们基本是不会去使用的,所以小伙伴也没必要去了解。
看代码,默认啥都不指定的时候,我们使用的就是PROPAGATION_REQUIRED这种方式。
那么接下来就是关于 这种默认的事物传播机制 PROPAGATION_REQUIRED 我们需要关心的东西了。
前面介绍了 加入当前事务 REQUIRED 的传播行为:
- 是指如果当前存在事务,则加入该事务
- 如果当前没有事务,则创建一个新的事务。
假设,第一个业务类里面的方法 使用了 声明式事务 :
class testOne
{
@Transactional(rollbackFor = Exception.class)
public Boolean addOne(UserInfo userInfo) {
//... 业务处理
//... 业务处理
//... 业务处理
retrun xxx;
}
}
假设, 第二个业务类里面的方法,也使用了声明式事务:
class testTwo
{
@Transactional(rollbackFor = Exception.class)
public Boolean addTwo(UserInfo userInfo) {
//... 业务处理
//... 业务处理
//... 业务处理
retrun xxx;
}
}
然后第三个业务类里面的方法没有使用声明式事务,去调用第一个和第二个,如:
class testThree
{
@Transactional(rollbackFor = Exception.class)
public Boolean testThree(UserInfo userInfo) {
addOne(xxxx);
addTwo(xxxx);
retrun xxx;
}
}
在testThree方法(对于addOne 和 addTwo 来说是个外部方法)上同样使用声明式事物,且也是默认指定传播机制PROPAGATION_REQUIRED。
默认指定传播机制PROPAGATION_REQUIRED , testThree 让testOne,testTwo 都加入到一个事务里面。
这样addOne事物开启时,发现外部存在指定传播机制PROPAGATION_REQUIRED的事物,那么就会加入该事物;
同样addTwo同理。
第二大属性:@Transactional 注解的 隔离属性
数据库有自己的隔离级别的定义,Spring也有自己的 隔离级别的定义
Spring中的隔离级别
Spring事务由 Transactional 注解实现,隔离级别由它的参数 isolation 控制,Isolation 的 Eum 类中定义了“五个”表示隔离级别的值,如下。
隔离级别 | 含义 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
ISOLATION_READ_UNCOMMITTED | 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 |
ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 |
ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 |
ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 |
Spring中的隔离级别 和数据一致性问题的 关系:
Isolation的值与隔离级别 | 隔离级别的值 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Isolation.DEFAULT | 0 | - | - | - |
Isolation.READ_UNCOMMITTED | 1 | √ | √ | √ |
Isolation.READ_COMMITTED | 2 | × | √ | √ |
Isolation.REPEATABLE_READ | 4 | × | × | √ |
Isolation.SERIALIZABLE | 8 | × | × | × |
数据库隔离级别
隔离级别 | 隔离级别的值 | 导致的问题 |
---|---|---|
Read-Uncommitted | 0 | 导致脏读 |
Read-Committed | 1 | 避免脏读,允许不可重复读和幻读 |
Repeatable-Read | 2 | 避免脏读,不可重复读,允许幻读 |
Serializable | 3 | 串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重 |
MySQL 默认为 RR :PEATABLE_READ;
Oracle,sql server 默认为 RC:READ_COMMITTED;
READ_UNCOMMITTED 由于隔离级别较低,通常不会被使用。
数据库隔离级别 和数据一致性问题 的 关系:
隔离级别 | 隔离级别的值 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read uncommitted(未提交读) | 0 | √ | √ | √ |
Read committed(已提交读) | 1 | × | √ | √ |
Repeatable read(可重复读) | 2 | × | × | √ |
Serializable(可串行化) | 3 | × | × | × |
Spring事务的隔离级别与 数据库隔离级别的关系:
Spring默认的隔离级别, 是 Isolation.DEFAULT。
它的含义是:使用数据库默认的事务隔离级别。
除此之外,另外Spring事务的隔离级别 四个与 JDBC 的隔离级别是相对应的,那个四个 Spring事务隔离级别,其实是在数据库隔离级别之上又进一步进行了封装。
如果 Spring事务的隔离级别与 数据库隔离级别的不一致会怎样?
以Spring事务为准的。
Spring 事务管理涉及到了与数据库的交互 。
JDBC 加载的流程 有四步:注册驱动,建立连接,发起请求,输出结果, 伪代码如下:
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
// 1.注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.创建链接
System.out.println("连接数据库...");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/my_db","root","root");
// 3.发起请求
stmt = conn.createStatement();
String sql = "SELECT id, name, url FROM websites";
rs = stmt.executeQuery(sql);
// 4.输出结果
System.out.print("查询结果:" + rs);
// 关闭资源(演示代码,不要纠结没有写在finally中)
rs.close();
stmt.close();
conn.close();
} catch (SQLException se)
se.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}
在创建连接阶段,JDBC 从数据库获取一个连接 Connection 对象
Connection 对象不仅有连接数据库的方法,还有设置当前连接的事物隔离级别的方法, 源码如下:
*/
public interface Connection extends Wrapper, AutoCloseable {
...
/**
- 尝试将此连接对象的事务隔离级别更改为给定的级别
- 接口连接中定义的常量是可能的事务隔离级别
*/
void setTransactionIsolation(int level) throws SQLException;
...
}
该方法的注释说明:尝试将此连接对象的事务隔离级别更改为给定的级别,如果在事务期间调用此方法,则结果由实现定义。
所以,如果spring与数据库事务隔离级别不一致时,spring 会调用类似的方法, 设置 一下 当前链接的 事务隔离级别。
第三大属性:@Transactional 注解的 readOnly属性
@Transactional
注解的readOnly 属性用于指定事务是否为只读事务。
当readOnly
属性设置为true
时,表示该事务只涉及读取数据, 而不进行任何写操作(如INSERT、UPDATE、DELETE等)。这有助于数据库引擎优化事务处理,因为它知道不需要考虑事务的并发写操作。
当使用 @Transaction 注解时,可以通过设置 readOnly=true
来指定这是一个只读事务,这样在事务执行期间就不会对数据进行修改,只会进行查询操作。
以下是一个使用 @Transaction 只读示例的代码片段:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional(readOnly = true)
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 其他方法...
}
在上面的示例中,getUserById
方法被标记为只读事务,因此在执行期间只会进行查询操作。如果在方法中尝试进行修改操作,将会抛出异常。
从数据库层面来讲,设置readOnly = true
会向数据库发送一个信号,告诉数据库这个事务是只读的。
不同的数据库会根据这个提示进行优化。例如
- 在一些数据库中,对于只读事务,数据库可以避免获取写锁,减少锁竞争,从而提高并发读取性能。
- 同时,数据库也可能会跳过一些与写操作相关的日志记录和事务处理逻辑,提高事务执行的效率。
第四大属性:@Transactional 注解的 rollbackFor 回滚规则属性
事务五边形的rollbackFor 回滚规则属性 , 定义了哪些异常会导致事务回滚,而哪些异常不会。
下面是一个简单的 Java 代码示例,演示了 @Transactional
回滚规则属性。
首先是 不做配置,使用 rollbackFor 的默认值:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser(User user) {
userRepository.save(user);
if (user.getId() == null) {
throw new RuntimeException("Failed to create user");
}
}
}
在上面的示例中, 如果在方法执行过程中发生异常,事务会自动回滚,保证数据的一致性。
如果用户创建失败,createUser
方法会抛出一个 RuntimeException
异常,这会导致事务回滚,用户创建操作会被撤销。
尼恩提示,@Transactional 使用有很多的 约束:
约束1 :
@Transactional
注解只能应用于公共方法,因为只有公共方法才能被代理,从而实现事务管理。约束2 :默认情况下,
@Transactional
注解 只对非受检 异常进行回滚,而对受检查异常不进行回滚。
非检查型异常 (Unchecked Exception/非受检查异常)的是程序在编译时不会提示需要处理该异常,而是在运行时才会出现异常, 如 RuntimeException。
检查型异常(Checked Exception)是指在 Java 中,编译器会强制要求对可能会抛出这些异常的代码进行异常处理,否则代码将无法通过编译。
一般来说,在编写代码时应该尽量避免抛出非检查型异常(如 RuntimeException),因为这些异常的发生通常意味着程序存在严重的逻辑问题。
如果是受检 异常(Checked Exception), 进行回滚,可以在 @Transactional
注解中指定 rollbackFor
属性,例如
@Transactional(rollbackFor = Exception.class)
public void createUser(User user) {
userRepository.save(user);
if (user.getId() == null) {
throw new RuntimeException("Failed to create user");
}
}
掌握了 @Transactional 的几个核心属性, 最后我们来说下 @Transactional 的失效场景。
Spring事务 的10种 失效场景
Spring事务管理 是Java应用中确保数据库操作一致性和完整性的关键机制之一。
然而,在实际开发中,有时候会遇到Spring事务失效的情况,导致期望的事务行为无法正常发生。
本文将深入探讨九种常见的导致Spring事务失效的场景,帮助开发者更好地理解事务管理的细节和注意事项。
场景1:非Spring容器管理的 事务方法
Spring事务是通过AOP(面向切面编程)来实现的,如果一个事务注解被应用到一个普通的Java类的方法上,并且该类不是通过Spring容器进行管理的,那么事务将不会生效。
因为Spring无法拦截并管理这个类的方法调用。
示例:
public class TransactionalService {
@Transactional
public void performTransaction() {
// 事务操作
}
}
在上述示例中,如果TransactionalService
不是通过Spring容器进行管理,那么@Transactional
注解将不会生效。
场景2: 在非公有方法上使用事务
Spring事务默认只对公有方法上的事务注解生效。@Transactional 应用在非 public 修饰的方法上,@Transactional 将会失效。
如果在一个非公有方法上使用事务注解,事务将不会生效。
示例:
@Transactional
private void performTransaction() {
// 非公有方法上使用事务,事务失效
}
在上述示例中,performTransaction
是一个私有方法,事务注解不会生效。
protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。
场景3:异常被捕获 而不是 抛出
有时候,开发者可能选择捕获掉一个异常,而不重新抛出或处理。
这样的做法将导致事务失效,因为Spring事务管理依赖于异常来判断是否需要回滚事务。
示例:
@Transactional
public void handleException() {
try {
// 事务操作
throw new RuntimeException("Simulate Exception");
} catch (Exception e) {
// 异常被忽略,事务失效
}
}
在上述示例中,异常被捕获但未重新抛出或处理,导致事务失效。
场景4: 对 受检查异常进行 异常拦截
默认情况下, Spring事务只对RuntimeException
(非受检查异常)及其子类进行回滚。
如果一个受事务管理的方法抛出了 受检查异常(如Exception), 默认情况下,事务将不会回滚。
示例:
@Transactional
public void performTransaction() throws Exception{
// 事务操作
throw new Exception("Unchecked Exception");
}
在上述示例中,抛出了一个 受检查异常, 导致事务失效。
如果是 对 受检查异常进行捕获, 需要使用 rollbackFor 定制回滚 规则:
@Transactional(rollbackFor = Exception.class)
public void createUser(User user) throws Exception{
// 事务操作
throw new Exception("Unchecked Exception");
}
场景5:方法内部调用导致的事务失效
Spring事务默认只对外部方法调用进行代理,对于同一个类的内部方法调用是无法触发事务的。
如果在一个事务方法内部调用另一个方法,而这个被调用的方法上标注了@Transactional
注解,事务将不会生效。
示例:
@Transactional
public void outerTransaction() {
innerTransaction(); // 内部调用,事务失效
}
@Transactional
public void innerTransaction() {
// 内部事务操作
}
在上述示例中,outerTransaction
方法内部调用了innerTransaction
方法,但由于默认只对外部方法调用进行代理,导致innerTransaction
方法上的事务失效。
场景6: 方法自调用导致的事务失效
类似于内部方法调用,如果一个事务方法内部自己调用自己,事务同样会失效。
这是因为Spring使用代理机制来管理事务,自调用会绕过代理对象,导致事务不生效。
示例:
@Transactional
public void selfInvokingTransaction() {
// 自调用,事务失效
selfInvokingTransaction();
// 事务操作
}
在上述示例中,selfInvokingTransaction
方法内部自己调用了自己,导致事务失效。
场景7: 在同一个类中,一个非事务方法调用另一个事务方法
当在同一个类中,一个非事务方法调用了另一个事务方法时,事务将不会生效。
这是因为Spring默认使用动态代理来管理事务,而动态代理只能拦截外部调用。
示例:
public void nonTransactionMethodA() {
transactionMethodB(); // 在同一个类中调用另一个事务方法,事务失效
}
@Transactional
public void transactionMethodB() {
// 事务操作
}
在上述示例中,nonTransactionMethodA
调用了transactionMethodB
,但事务不会生效。
场景8: 使用错误的事务传播行为
Spring事务提供了不同的传播行为,如REQUIRED
、REQUIRES_NEW
等。
使用错误的传播行为可能导致事务失效,因为传播行为决定了事务如何在方法调用链中传播。
示例:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void performTransaction() {
// 使用错误的传播行为,可能导致事务失效
}
在上述示例中,如果使用了错误的传播行为,可能会导致事务失效。
场景9: 数据库引擎不支持事务
数据库引擎不支持事务,Spring事务 失效。
这一点很简单,myisam 引擎是不支持事务的,innodb 引擎支持事务。
场景10:数据源没有配置事务管理器
数据源没有配置事务管理器,这个也很简单,要使用事务肯定要配事务管理器。
Hibernate 用的是HibernateTransactionManager,
JDBC 和 Mybatis 用的是 DataSourceTransactionManager。
如果数据源没有配置事务管理器 ,Spring事务 失效。
Spring事务 的10种 失效场景总结
开发者应当牢记这些场景,并在开发过程中注意避免出现事务失效的情况,以确保数据的一致性和完整性。
顶奢好文:3W字,穿透Spring事务原理、源码,最少读10遍
高端面试:必须来点 高大上的答案:
尼恩 提示: 要拿到 高薪offer, 或者 要进大厂,必须来点 非常见的、 高大上的答案, 整点技术狠活儿。
如果能讲 到尼恩答案 的 水平 , 面试官一定口水直流, 大厂 offer 就到手啦。
尼恩架构团队,持续为大家 梳理了一系列的 塔尖 面试题,帮助大家 进大厂,拿高薪:
- Java基础
美团面试:String 为什么 不可变 ?(90%答错了,尼恩来一个绝世答案)
- 索引
阿里面试:为什么要索引?什么是MySQL索引?底层结构是什么?
滴滴面试:单表可以存200亿数据吗?单表真的只能存2000W,为什么?
- 索引下推 ?
- 索引失效
美团面试:mysql 索引失效?怎么解决?(重点知识,建议收藏,读10遍+)
- MVCC
- binlog、redolog、undo log
美团面试:binlog、redolog、undo log底层原理是啥?分别实现ACID哪个特性?(尼恩图解,史上最全)
- mysql 事务
京东面试:RR隔离mysql如何实现?什么情况RR不能解决幻读?
- 分布式事务
分布式事务圣经:从入门到精通,架构师尼恩最新、最全详解 (50+图文4万字全面总结 )
说在最后:有问题找45岁老架构取经
只要按照上面的 尼恩团队梳理的 方案去作答, 你的答案不是 100分,而是 120分。 面试官一定是 心满意足, 五体投地。
按照尼恩的梳理,进行 深度回答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会, 可以找尼恩来改简历、做帮扶。前段时间,空窗2年 成为 架构师, 32岁小伙逆天改命, 同学都惊呆了。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
尼恩技术圣经系列PDF
- 《NIO圣经:一次穿透NIO、Selector、Epoll底层原理》
- 《Docker圣经:大白话说Docker底层原理,6W字实现Docker自由》
- 《K8S学习圣经:大白话说K8S底层原理,14W字实现K8S自由》
- 《SpringCloud Alibaba 学习圣经,10万字实现SpringCloud 自由》
- 《大数据HBase学习圣经:一本书实现HBase学习自由》
- 《大数据Flink学习圣经:一本书实现大数据Flink自由》
- 《响应式圣经:10W字,实现Spring响应式编程自由》
- 《Go学习圣经:Go语言实现高并发CRUD业务开发》
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓