6,AOP事务管理
6.1 Spring事务简介
6.1.1 相关概念介绍
-
事务作用:在数据层保障一系列的数据库操作同成功同失败
-
Spring事务作用:在数据层或==业务层==保障一系列的数据库操作同成功同失败
数据层有事务我们可以理解,为什么业务层也需要处理事务呢?
举个简单的例子,
-
转账业务会有两次数据层的调用,一次是加钱一次是减钱
-
把事务放在数据层,加钱和减钱就有两个事务
-
没办法保证加钱和减钱同时成功或者同时失败
-
这个时候就需要将事务放在业务层进行处理。
Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager
commit是用来提交事务,rollback是用来回滚事务。
PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现:
从名称上可以看出,我们只需要给它一个DataSource对象,它就可以帮你去在业务层管理事务。其内部采用的是JDBC的事务。所以说如果你持久层采用的是JDBC相关的技术,就可以采用这个事务管理器来管理你的事务。而Mybatis内部采用的就是JDBC的事务,所以后期我们Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器。
🧠 理论理解
Spring事务解决的问题是:确保多步骤业务操作的原子性、一致性、隔离性、持久性(ACID),即要么都成功、要么都失败。
数据层事务(JDBC、MyBatis)只能保证单表单操作;业务层事务(Service)整合多个 DAO 操作,必须由 Spring 管理。
Spring提供了 PlatformTransactionManager
接口,具体实现(如 DataSourceTransactionManager
)用来统一管理事务提交、回滚。
🏢 企业实战理解
-
阿里巴巴:支付宝钱包的分布式转账、退款等业务,使用 Spring 声明式事务管理,确保资金链路绝对一致。
-
字节跳动:抖音的虚拟道具购买与扣款,业务层用 Spring 事务,统一管理分布在多表、多库的扣费、库存更新。
-
Google:广告竞价系统用 Spring 管理跨多个组件(计费、扣款、日志)的事务,防止部分成功部分失败。
-
英伟达:云GPU租赁中的预扣费、资源分配等场景,用 Spring 事务管理后台数据库操作,确保用户请求全链路一致。
-
OpenAI:早期 GPT API 接入层,使用 Spring 事务控制用户调用次数扣减、付费计账的多步操作,保证账户安全。
面试题
面试官问: 你能解释一下 Spring 中的事务管理机制吗?
回答:
当然可以!Spring 中的事务管理是一套基于 AOP(面向切面编程)的声明式事务机制,它的核心目标是保证业务操作的 ACID(原子性、一致性、隔离性、持久性)特性。
通过 PlatformTransactionManager 接口,Spring 将事务操作(如提交、回滚)抽象出来,使得无论底层是 JDBC、Hibernate 还是 JPA,都可以用统一的方式管理。声明式事务允许我们用 @Transactional
注解声明哪些方法需要事务,而不用显式写代码控制事务边界,大大减少了样板代码并提高了可维护性。这是企业项目中管理复杂业务操作的关键能力。
💬 大厂场景题
场景题:
假设你负责字节跳动某个广告投放系统模块,涉及用户账户扣费、投放记录写入、预算更新,这些操作分布在不同 DAO 层。领导让你保证这些操作要么全部成功,要么全部失败,你会如何设计?
回答:
首先,我会明确这些操作是一个完整的业务事务,需要具备原子性、一致性、隔离性、持久性。单纯在 DAO 层做事务是不够的,因为 DAO 只控制单表或单模块的数据库操作,无法跨越多个模块整合。
因此,我会在 Service 层统一引入 Spring 声明式事务,用 @Transactional
注解包裹住整个扣费、写入、更新的业务方法。这样,Spring 的 AOP 机制会自动在方法调用前后开启、提交或回滚事务。如果其中任何一步失败(如余额不足、数据库异常),整个事务都会回滚,保证系统状态的一致性。这是业界在多模块、多 DAO 协同时的标准做法。
6.1.2 转账案例-需求分析
接下来通过一个案例来学习下Spring是如何来管理事务的。
先来分析下需求:
需求: 实现任意两个账户间转账操作
需求微缩: A账户减钱,B账户加钱
为了实现上述的业务需求,我们可以按照下面步骤来实现下: ①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
②:业务层提供转账操作(transfer),调用减钱与加钱的操作
③:提供2个账号和操作金额执行转账操作
④:基于Spring整合MyBatis环境搭建上述操作
🧠 理论理解
转账业务典型场景:A扣钱、B加钱。单独的 SQL 操作(A账户、B账户)彼此独立,必须由外部业务层用事务包裹,保证整体一致性。
没有事务,异常中断会导致“钱从 A 出去了,却没到 B”,数据错误。
🏢 企业实战理解
-
阿里巴巴:余额宝、花呗的转账、结算操作,业务层将扣款、加款、清分作为一体事务。
-
字节跳动:字节广告投放,账户余额扣费与展示曝光统计写入,用事务包裹,避免数据对不上。
-
Google Pay:用户跨国支付转账时,涉及多方账户、手续费计算,必须保证多步骤一起成功或一起失败。
-
英伟达:用户租用GPU节点时,扣费与资源调度分离,但用事务统一绑定,避免资源分配与扣款不一致。
-
OpenAI:API调用的扣费、额度检查、记录写入由统一事务控制,确保调用量、付费、限额逻辑一体化。
面试官问: 你能用一个具体案例说明为什么业务层需要事务,而不仅仅是 DAO 层?
回答:
好的,以转账为例,A账户要扣钱,B账户要加钱。表面上看,这两个操作分别属于两个 DAO 层,单独看它们各自可以加事务,但实际业务上它们是一个整体,要么一起成功,要么一起失败。
如果仅在 DAO 层加事务,A扣款后成功提交,B加款失败或中断,这就造成了钱从系统里“消失”或者“两边对不上”的数据不一致问题。而业务层事务(通常在 Service 层)是用来将这些分散的 DAO 操作整合成一个整体,统一提交或回滚,保证整个业务操作的原子性。这也是为什么我们总说,事务的“粒度”必须从业务需求角度去划分,而不是只从单个数据库操作去看。
💬 大厂场景题
场景题:
你设计了一个银行转账功能,A账户扣款、B账户加款。有一天测试报告发现:当 A 扣款成功,但 B 因网络问题没加款,系统没有报错也没有回滚,钱凭空消失了。你如何分析并解决这个问题?
回答:
这个问题的根源是事务边界划分不正确。很可能代码里,A 扣款和 B 加款各自是单独的数据库操作,没有用事务把它们包裹起来。当 B 加款失败时,A 扣款已经提交,导致了数据不一致。
我会分析 Service 层代码,检查是否缺少 @Transactional
注解,确保整个转账业务被事务整体控制。其次,我会检查是否有 try-catch 把异常“吃掉”了,因为 Spring 事务需要检测到未捕获异常才能回滚。修复方案是:确保 A 和 B 的 DAO 操作都在同一个 Service 方法中,用 @Transactional
管理,同时让异常抛出,让事务感知到异常并触发回滚机制。
6.1.3 转账案例-环境搭建
步骤1:准备数据库表
之前我们在整合Mybatis的时候已经创建了这个表,可以直接使用
create database spring_db character set utf8;
use spring_db;
create table tbl_account(
id int primary key auto_increment,
name varchar(35),
money double
);
insert into tbl_account values(1,'Tom',1000);
insert into tbl_account values(2,'Jerry',1000);
步骤2:创建项目导入jar包
项目的pom.xml添加相关依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
步骤3:根据表创建模型类
public class Account implements Serializable {
private Integer id;
private String name;
private Double money;
//setter...getter...toString...方法略
}
步骤4:创建Dao接口
public interface AccountDao {
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
步骤5:创建Service接口和实现类
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
public void transfer(String out,String in ,Double money) ;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}
}
步骤6:添加jdbc.properties文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root
步骤7:创建JdbcConfig配置类
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}
步骤8:创建MybatisConfig配置类
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.itheima.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.itheima.dao");
return msc;
}
}
步骤9:创建SpringConfig配置类
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}
步骤10:编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() throws IOException {
accountService.transfer("Tom","Jerry",100D);
}
}
最终创建好的项目结构如下:
🧠 理论理解
搭建事务环境,需要:
1️⃣ 数据库表准备好(账户表、日志表)。
2️⃣ 配置好数据源(Druid)、MyBatis、Spring。
3️⃣ 确保 Spring 的配置类(@EnableTransactionManagement
)开启注解事务。
4️⃣ 用注解 @Transactional
标记业务方法。
这是标准的 Spring 事务搭建流程。
🏢 企业实战理解
-
阿里巴巴:每个新服务上线前,基础框架团队会搭好带事务的 Spring Boot 微服务骨架,供业务方接入。
-
字节跳动:公司级研发平台(飞鱼)提供了内置 MyBatis + Spring 的事务模板,研发只需专注业务逻辑。
-
Google:使用内部微服务框架,但事务概念仍类似,通过注解或DSL声明。
-
英伟达:AI云后台用 Java Spring 系统,按模块划分事务管理,保持配置集中统一。
-
OpenAI:服务初期,内部有基于 Spring Boot 的管理面,利用 Spring 提供的事务支持快速构建和测试。
面试官问: 在 Spring 中搭建事务管理环境需要哪些核心步骤?请详细讲讲。
回答:
这个问题非常好。搭建 Spring 事务环境有几个关键步骤:
第一步,是引入相关依赖,比如 spring-tx
、spring-jdbc
、mybatis-spring
等。
第二步,是在配置中声明数据源(DataSource)、事务管理器(如 DataSourceTransactionManager),并将这些 Bean 交给 Spring 管理。
第三步,是在配置类上添加 @EnableTransactionManagement
注解,启用注解驱动的事务管理。
第四步,是在具体需要事务的方法或类上加上 @Transactional
注解,让 Spring 的 AOP 切面机制在方法调用前后织入事务控制代码。
这一套下来,事务管理环境就基本齐全了。当然,如果用 Spring Boot,大部分可以通过自动配置帮你省去,只要关注事务管理器和注解使用就够了。
场景题:
领导要求你在阿里巴巴的支付系统新模块中引入 Spring 事务,你怎么确保项目的事务配置完整、规范、无缺漏?
回答:
我会从以下几个方面系统性检查:
第一,Maven 依赖是否引入了 spring-tx
、spring-jdbc
、与使用的持久层技术(如 MyBatis、JPA)相关的依赖。
第二,数据源配置是否符合公司统一规范,比如用 Druid 数据源,并确保连接池参数(最大连接数、超时等)合理。
第三,事务管理器是否正确配置(如 DataSourceTransactionManager),并注入 Spring 容器。
第四,是否在 Spring 配置类中加了 @EnableTransactionManagement
,确保注解驱动事务生效。
最后,我会写一两个简单的事务性单元测试,确保 @Transactional
的方法在正常和异常情况下,事务都能按预期提交或回滚。这是保证系统上线稳定的最后防线。
6.1.4 事务管理
上述环境,运行单元测试类,会执行转账操作,Tom
的账户会减少100,Jerry
的账户会加100。
这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}
}
这个时候就模拟了转账过程中出现异常的情况,正确的操作应该是转账出问题了,Tom
应该还是900,Jerry
应该还是1100,但是真正运行后会发现,并没有像我们想象的那样,Tom
账户为800而Jerry
还是1100,100块钱凭空消息了,银行乐疯了。如果把转账换个顺序,银行就该哭了。
不管哪种情况,都是不允许出现的,对刚才的结果我们做一个分析:
①:程序正常执行时,账户金额A减B加,没有问题
②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层上,而Spring的事务管理就是用来解决这类问题的。
Spring事务管理具体的实现步骤为:
步骤1:在需要被事务管理的方法上添加注解
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money) ;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Transactional
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}
}
==注意:==
@Transactional可以写在接口类上、接口方法上、实现类上和实现类方法上
-
写在接口类上,该接口的所有实现类的所有方法都会有事务
-
写在接口方法上,该接口的所有实现类的该方法都会有事务
-
写在实现类上,该类中的所有方法都会有事务
-
写在实现类方法上,该方法上有事务
-
==建议写在实现类或实现类的方法上==
步骤2:在JdbcConfig类中配置事务管理器
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
注意:事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager
步骤3:开启事务注解
在SpringConfig的配置类中开启
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
步骤4:运行测试类
会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性。
知识点1:@EnableTransactionManagement
名称 | @EnableTransactionManagement |
---|---|
类型 | 配置类注解 |
位置 | 配置类定义上方 |
作用 | 设置当前Spring环境中开启注解式事务支持 |
知识点2:@Transactional
名称 | @Transactional |
---|---|
类型 | 接口注解 类注解 方法注解 |
位置 | 业务层接口上方 业务层实现类上方 业务方法上方 |
作用 | 为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务) |
🧠 理论理解
当业务中途出现异常(如除以0、空指针、数据库异常),如果没有事务控制,前面已经成功的操作(如扣钱)无法自动撤销。
通过 Spring 的 @Transactional
注解,方法中一旦出错,所有数据库操作自动回滚,恢复到初始状态。
事务管理器根据具体技术(JDBC、JPA、Hibernate、MyBatis)选用对应实现。
🏢 企业实战理解
-
阿里巴巴:分布式事务场景(如购物车、结算)采用阿里自研的 GTS,底层对接 Spring 事务。
-
字节跳动:用 Spring 声明式事务包裹广告扣款,防止因为中间失败导致资金对账不平。
-
Google:虽然Google大量用自研技术,但小型 Java 微服务仍基于 Spring 事务管理器,快速开发。
-
英伟达:Spring事务用在云控制面板中,确保集群调度、数据库更新和API调用三者一致。
-
OpenAI:用于管理调用量、模型计费、日志写入等多步操作,出错时保证回滚。
面试官问: 如果一个被 @Transactional
注解的方法中途抛出了异常,Spring 是怎么决定要不要回滚事务的?
回答:
这是很多人理解不清楚的细节。Spring 默认的事务回滚策略是:只有遇到未捕获的 RuntimeException
或 Error
时,才会自动回滚事务,而像 Checked Exception
(检查型异常,如 IOException)是不会触发回滚的。
当然,这个行为是可以通过 @Transactional
注解的属性显式配置的,比如用 rollbackFor
或 noRollbackFor
来指定哪些异常类型应该回滚或不回滚。
背后的机制是:Spring 的事务拦截器在捕获到异常后,会根据事务属性中的配置,决定是调用 commit()
还是 rollback()
。这也是为什么企业项目中,异常设计和事务管理是紧密耦合的,不理解默认策略容易踩坑。
场景题:
在 Google 的广告系统中,有一个模块在执行过程中偶尔会因为网络抖动抛出 IOException,导致广告主扣款回滚失败。你如何优化事务管理来防止这种问题?
回答:
首先,我会分析当前 @Transactional
的配置。Spring 默认只回滚 RuntimeException
和 Error
,不会回滚 CheckedException
,而 IOException 属于 CheckedException
。这意味着即使抛出了 IOException,事务也不会自动回滚。
解决方案是,在业务方法上的 @Transactional
注解中显式加上 rollbackFor = IOException.class
,确保遇到这类异常时事务能回滚。
同时,我会评估网络调用的位置是否能重试或熔断,减少外部依赖导致的事务失败。系统性的事务管理优化,必须结合异常策略、网络容错、事务配置等多方面。
6.2 Spring事务角色
这节中我们重点要理解两个概念,分别是事务管理员
和事务协调员
。
-
未开启Spring事务之前:
-
AccountDao的outMoney因为是修改操作,会开启一个事务T1
-
AccountDao的inMoney因为是修改操作,会开启一个事务T2
-
AccountService的transfer没有事务,
-
运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
-
如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行
-
就会导致数据出现错误
-
-
开启Spring的事务管理后
-
transfer上添加了@Transactional注解,在该方法上就会有一个事务T
-
AccountDao的outMoney方法的事务T1加入到transfer的事务T中
-
AccountDao的inMoney方法的事务T2加入到transfer的事务T中
-
这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
通过上面例子的分析,我们就可以得到如下概念:
-
事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
-
事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
==注意:==
目前的事务管理是基于DataSourceTransactionManager
和SqlSessionFactoryBean
使用的是同一个数据源。
🧠 理论理解
-
事务管理员(Transaction Manager):负责具体执行事务操作(提交、回滚)。
-
事务协调员(Transaction Coordinator):负责调度和协调多个事务参与者,确保整体一致性。
在单体应用中,两者经常混在一起(如 DataSourceTransactionManager 直接承担双角色);在分布式系统中,这两者严格分离。
🏢 企业实战理解
-
阿里巴巴:在分布式场景,阿里用分布式事务协调器(如 Seata)协调各业务节点的本地事务。
-
字节跳动:广告系统涉及资金流转,事务协调器保证广告主、广告系统、财务账务间的一致性。
-
Google:云平台部分组件采用 Paxos/Raft 协议在底层实现分布式事务协调。
-
英伟达:大规模 GPU 云资源调度,通过事务协调员控制多个数据中心的事务状态。
-
OpenAI:API调用计费和云后端结合,需要事务协调器保证调用链条一致。
面试官问: Spring 中的事务管理员(Transaction Manager)和事务协调员(Transaction Coordinator)有什么区别?
回答:
非常经典的问题。事务管理员是具体负责事务的接口或组件,比如 DataSourceTransactionManager(基于 JDBC)、JpaTransactionManager(基于 JPA),它负责具体的 begin
、commit
、rollback
操作。
事务协调员则是站在更高层次的角色,尤其在分布式场景下,负责调度多个参与方(每个参与方有自己的事务管理员),保证整体事务一致性。在单体系统里,事务管理员和协调员通常是一体的,但在分布式系统(如基于 Seata、Atomikos)里,事务协调员需要实现两阶段提交(2PC)、补偿机制(TCC)等复杂协议,来协调多个资源。
所以,理解两者关系是理解分布式事务的基础,也是大厂系统设计面试中常考的点。
场景题:
你在英伟达 GPU 云平台设计一个集群调度模块,涉及数据库、缓存、外部资源调度多方操作。领导问:为什么不能只用一个事务管理器,而要引入事务协调器?
回答:
这是因为多方操作涉及不同资源类型,甚至跨越不同物理节点,一个本地事务管理器(如 DataSourceTransactionManager)无法跨数据库、缓存、调度服务统一管理。
事务协调器(如基于分布式事务协议的组件)能够将多个参与者(数据库、缓存、调度服务)纳入一个全局事务中,保证整体一致性。例如用两阶段提交(2PC)协调各方在准备阶段统一确认,再在提交阶段统一提交或回滚。
在单库单表操作场景,事务管理器已经足够,但在多资源场景(特别是分布式环境),事务协调器是必要的。这是架构设计上的关键分界。
6.3 Spring事务属性
上一节我们介绍了两个概念,事务的管理员和事务的协同员,对于这两个概念具体做什么的,我们待会通过案例来使用下。除了这两个概念,还有就是事务的其他相关配置都有哪些,就是我们接下来要学习的内容。
在这一节中,我们主要学习三部分内容事务配置
、转账业务追加日志
、事务传播行为
。
6.3.1 事务配置
上面这些属性都可以在@Transactional
注解的参数上进行设置。
-
readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。
-
timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。
-
rollbackFor:当出现指定异常进行事务回滚
-
noRollbackFor:当出现指定异常不进行事务回滚
-
思考:出现异常事务会自动回滚,这个是我们之前就已经知道的
-
noRollbackFor是设定对于指定的异常不回滚,这个好理解
-
rollbackFor是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定?
-
这块需要更正一个知识点,并不是所有的异常都会回滚事务,比如下面的代码就不会回滚
public interface AccountService { /** * 转账操作 * @param out 传出方 * @param in 转入方 * @param money 金额 */ //配置当前接口方法具有事务 public void transfer(String out,String in ,Double money) throws IOException; } @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Transactional public void transfer(String out,String in ,Double money) throws IOException{ accountDao.outMoney(out,money); //int i = 1/0; //这个异常事务会回滚 if(true){ throw new IOException(); //这个异常事务就不会回滚 } accountDao.inMoney(in,money); } }
-
-
-
出现这个问题的原因是,Spring的事务只会对
Error异常
和RuntimeException异常
及其子类进行事务回顾,其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚-
此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
@Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Transactional(rollbackFor = {IOException.class}) public void transfer(String out,String in ,Double money) throws IOException{ accountDao.outMoney(out,money); //int i = 1/0; //这个异常事务会回滚 if(true){ throw new IOException(); //这个异常事务就不会回滚 } accountDao.inMoney(in,money); } }
-
-
rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
-
noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
-
isolation设置事务的隔离级别
-
DEFAULT :默认隔离级别, 会采用数据库的隔离级别
-
READ_UNCOMMITTED : 读未提交
-
READ_COMMITTED : 读已提交
-
REPEATABLE_READ : 重复读取
-
SERIALIZABLE: 串行化
-
介绍完上述属性后,还有最后一个事务的传播行为,为了讲解该属性的设置,我们需要完成下面的案例。
🧠 理论理解
事务属性包含:
-
回滚策略(rollbackFor)
-
超时时间(timeout)
-
只读设置(readOnly)
-
传播行为(propagation)
这些参数控制事务的执行细节。比如:默认只回滚 RuntimeException,传播行为决定遇到新事务如何处理(复用、挂起、独立开启等)。
🏢 企业实战理解
-
阿里巴巴:秒杀场景严格控制事务超时时间,避免死锁、卡单。
-
字节跳动:分布式微服务间用自定义传播行为,确保跨模块事务隔离。
-
Google:广告投放系统中的事务细粒度调整,用不同传播策略控制主交易和统计写入的隔离性。
-
英伟达:云调度事务配置超时,避免长时间锁定调度资源。
-
OpenAI:调用链中对只读操作明确声明,提升数据库查询性能。
面试官问: 你平时项目中有用到事务的哪些高级属性?能详细说说它们的作用吗?
回答:
在实际项目中,我经常用到 @Transactional
注解的这些高级属性:
-
rollbackFor
:指定哪些异常需要触发回滚,用来覆盖默认只回滚 RuntimeException 的策略。 -
timeout
:设置事务最长允许执行时间,防止长事务拖慢系统。 -
readOnly
:声明只读事务,优化数据库引擎的读性能。 -
propagation
:最核心的传播行为属性,决定当前方法遇到外部事务时,是加入、挂起、独立开启,还是抛出异常。
这些属性的合理配置,能显著提升系统的性能、稳定性和可维护性,也是大厂对候选人要求比较高的地方,因为大厂业务复杂,对事务粒度、超时、隔离性要求更高。
场景题:
你在美团外卖系统发现,有些用户下单接口响应很慢,定位发现部分事务持续时间过长。你怎么调整事务配置优化性能?
回答:
我会分两步走:
第一步,检查 @Transactional
注解是否有设置 timeout
属性。Spring 默认事务没有超时限制,容易造成长事务占用数据库连接,影响整体性能。我会根据业务场景,合理设置超时时间,比如下单接口事务控制在 3-5 秒以内。
第二步,分析业务代码,看哪些操作可以提前提交事务(比如只读查询、缓存更新),哪些必须放在事务中,尽量缩小事务范围。此外,还要排查是否有不必要的事务传播(如方法套用多层事务)导致嵌套开销。
通过精细化调整事务配置,可以有效提升接口响应速度和系统吞吐量。
6.3.2 转账业务追加日志案例
6.3.2.1 需求分析
在前面的转案例的基础上添加新的需求,完成转账后记录日志。
-
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
-
需求微缩:A账户减钱,B账户加钱,数据库记录日志
基于上述的业务需求,我们来分析下该如何实现:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
需要注意一点就是,我们这个案例的预期效果为:
==无论转账操作是否成功,均进行转账操作的日志留痕==
6.3.2.2 环境准备
该环境是基于转账环境来完成的,所以环境的准备可以参考6.1.3的环境搭建步骤
,在其基础上,我们继续往下写
步骤1:创建日志表
create table tbl_log(
id int primary key auto_increment,
info varchar(255),
createDate datetime
)
步骤2:添加LogDao接口
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
步骤3:添加LogService接口与实现类
public interface LogService {
void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
@Transactional
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
步骤4:在转账的业务中添加记录日志
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money)throws IOException ;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
@Transactional
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}
}
步骤5:运行程序
-
当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
-
当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
-
这个结果和我们想要的不一样,什么原因?该如何解决?
-
失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败
-
最终效果:无论转账操作是否成功,日志必须保留
🧠 理论理解
新需求:无论转账成功与否,都要记录日志。
挑战:日志与主业务用同一事务,主业务回滚时日志也被回滚。
解决方案:日志部分用 Propagation.REQUIRES_NEW
,开启独立新事务,保证不受主事务影响。
🏢 企业实战理解
-
阿里巴巴:支付场景,主链路失败,日志和审计必须独立提交,便于后续排查。
-
字节跳动:抖音、头条的埋点、日志服务都用独立事务,防止业务异常导致日志丢失。
-
Google:搜索广告日志单独入库,用独立事务,确保计费与业务操作分离。
-
英伟达:云调度失败时,操作日志仍单独保存,保证完整性。
-
OpenAI:接口调用失败,独立日志写入保存异常信息,便于后续分析。
面试官问: 如果一个主事务失败了,你如何保证日志仍然能被独立记录下来?
回答:
这其实考察的是事务传播行为的理解。主事务失败时,如果日志记录跟主事务是同一个事务上下文,主事务回滚时,日志也会被一起回滚。
为了解决这个问题,我们可以在日志方法上使用 @Transactional(propagation = Propagation.REQUIRES_NEW)
,让它在调用时挂起当前事务,独立开启一个新事务。这样即使主事务失败,日志的事务也能独立提交,确保日志留痕。
这个技巧在大厂中非常常见,比如支付失败时必须保留错误日志、监控埋点,即便主业务链路挂掉,日志和审计记录也不能丢。
场景题:
在字节跳动的直播打赏模块,用户打赏失败时仍要记录日志。你发现主业务失败时日志也没记录下来。请分析并优化。
回答:
这个问题是事务传播策略配置错误导致的。当前代码里,主业务和日志记录可能共用一个事务,当主业务失败回滚时,日志也一起回滚,导致日志丢失。
优化方案是:将日志记录方法的 @Transactional
注解设置 propagation = Propagation.REQUIRES_NEW
,强制日志记录开启一个独立新事务,挂起主事务,保证日志的独立提交。
这样,即使主业务失败,日志事务依然能成功提交,保留下打赏失败的详细信息,便于后续问题排查和统计分析。这个设计模式在大型分布式系统中非常常见。
6.3.3 事务传播行为
对于上述案例的分析:
-
log方法、inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3
-
transfer因为加了@Transactional注解,也开启了事务T
-
前面我们讲过Spring事务会把T1,T2,T3都加入到事务T中
-
所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
-
这和我们的需求不符,这个时候我们就想能不能让log方法单独是一个事务呢?
要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是:
事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
具体如何解决,就需要用到之前我们没有说的propagation属性
。
1.修改logService改变事务的传播行为
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。
2.事务传播行为的可选值
对于我们开发实际中使用的话,因为默认值需要事务是常态的。根据开发过程选择其他的就可以了,例如案例中需要新事务就需要手工配置。其实入账和出账操作上也有事务,采用的就是默认值。
🧠 理论理解
事务传播行为决定当一个事务方法被另一个事务方法调用时,是否使用原事务、挂起原事务、开启新事务等。
典型值:
-
REQUIRED
(默认):加入现有事务或新建一个。 -
REQUIRES_NEW
:挂起当前事务,开启新事务。 -
NESTED
:开启嵌套事务(需要底层支持)。
🏢 企业实战理解
-
阿里巴巴:核心资金链路用
REQUIRED
,日志、监控等非核心模块用REQUIRES_NEW
。 -
字节跳动:推荐系统的主召回、日志埋点模块分开事务,防止相互影响。
-
Google:跨模块调用时,主事务和辅助服务(如监控、限流)的事务独立,确保主业务稳定。
-
英伟达:GPU调度中,任务分配和调度日志使用不同事务传播策略,保证性能。
-
OpenAI:主API调用与后台统计分离,用
REQUIRES_NEW
保证主任务失败不影响统计入库。
面试官问: 你能详细讲讲 Spring 中不同的事务传播行为吗?它们在什么场景下用?
回答:
当然。Spring 的事务传播行为定义了一个事务方法被另一个事务方法调用时的处理方式。
-
REQUIRED
(默认):如果当前有事务就加入,没有就新建。常用于大部分业务。 -
REQUIRES_NEW
:总是挂起当前事务,开启新事务。适用于日志、审计等要独立提交的场景。 -
NESTED
:嵌套事务,需要底层数据库支持,用来做部分回滚。
场景题:
你在 OpenAI API 平台的调用链设计中,需要保证主 API 调用失败时后台统计模块依然能正常写入调用数据。如何设计事务传播行为?
回答:
为了实现这个需求,我会将后台统计模块的方法用 @Transactional(propagation = Propagation.REQUIRES_NEW)
声明,确保它在主事务挂起时独立开启一个新事务。这意味着即使主 API 调用失败、主事务回滚,统计模块的事务仍然能独立提交,把调用数据写入数据库。
这种设计在高并发、复杂调用链中尤为重要,因为它可以确保核心与非核心操作解耦,主业务链路即使挂掉,也不会影响数据埋点、监控、日志等后台分析模块的准确性和完整性。这是分布式系统的关键稳定性保障手段。