一、前言
在日常开发中,事务是保证数据一致性的核心手段。尤其是转账这类业务,必须保证「A减钱」和「B加钱」两个操作同成功、同失败,否则就会出现资金异常。
Spring 提供了一套完整的声明式事务解决方案,基于 AOP 实现,让我们无需手动编写事务开启、提交、回滚代码,只需通过注解就能轻松管理业务层事务。
本文将从事务基础概念 出发,通过转账案例 完整演示 Spring 事务的环境搭建、问题复现、解决方案,再深入讲解事务属性、传播行为等核心知识点,帮你彻底搞懂 Spring 事务管理。
二、Spring事务基础概念
2.1 事务的作用
- 数据库层事务:保障一系列数据库操作同成功同失败。
- Spring事务作用:在业务层保障一系列数据库操作同成功同失败。
举个转账的例子:
- 转账业务需要两次数据库操作:A账户减钱、B账户加钱。
- 如果事务放在数据库层,减钱和加钱是两个独立事务,无法保证同时成功/失败。
- 必须将事务提升到业务层,让两个操作在同一个事务中执行。
2.2 Spring事务核心接口
Spring 提供了统一的事务管理器接口 PlatformTransactionManager,定义了事务的核心操作:
public interface PlatformTransactionManager{
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
commit():提交事务rollback():回滚事务
Spring 为其提供了具体实现 DataSourceTransactionManager,内部基于 JDBC 事务实现,我们只需要给它注入 DataSource,就能在业务层管理事务,这也是 Spring 整合 MyBatis 时的默认事务管理器。
@Transactional 只是声明 → 不干活
transactionManager 才是真正干活的 → 处理事务
Spring 的事务工作流程:
1.看到 @Transactional
2.去找容器中的 事务管理器 Bean
3.用事务管理器开启 / 提交 / 回滚事务
如果没有事务管理器 Bean → @Transactional 直接失效!
三、转账案例:从零搭建事务环境
3.1 需求分析
需求:实现任意两个账户间的转账操作
需求拆解:
- A账户减钱(outMoney)
- B账户加钱(inMoney)
- 保证两个操作同成功同失败
3.2 环境搭建步骤
步骤1:准备数据库表
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:项目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:创建实体类Account
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);
}
补充:MyBatis中@Param注解的使用规则
- 方法参数是多个基本类型/String时,必须加@Param:MyBatis无法自动识别参数名,需通过注解明确绑定SQL占位符。
- 方法参数是单个对象时,无需加@Param:MyBatis会自动通过对象属性名匹配SQL占位符。
- 其他场景:集合/数组、动态SQL中也需要@Param避免歧义。
步骤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);
}
}
3.3 问题复现:无事务的转账异常
正常运行测试,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账户已经减了100,但Jerry账户没有加钱,100块凭空消失了!
原因分析:
- 无事务时,
outMoney和inMoney是两个独立的数据库事务。 outMoney执行成功后事务自动提交,后续异常导致inMoney未执行,最终数据不一致。
四、Spring声明式事务:解决转账异常
4.1 事务配置步骤
步骤1:给业务方法添加@Transactional注解
public interface AccountService {
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可以写在接口、接口方法、实现类、实现类方法上。- 建议写在实现类/实现类方法上,更直观可控。
- @Transactional可以写在接口类上、接口方法上、实现类上和实现类方法上
- 写在接口类上,该接口的所有实现类的所有方法都会有事务
- 写在接口方法上,该接口的所有实现类的该方法都会有事务
- 写在实现类上,该类中的所有方法都会有事务
- 写在实现类方法上,该方法上有事务
- ==建议写在实现类或实现类的方法上==
步骤2:在JdbcConfig中配置事务管理器
public class JdbcConfig {
// 省略DataSource配置...
// 配置事务管理器,MyBatis使用JDBC事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
注意 :
@Transactional事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager
步骤3:在SpringConfig中开启注解式事务驱动
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
// 开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
4.2 核心注解详解
| 注解 | 类型 | 位置 | 作用 |
|---|---|---|---|
@EnableTransactionManagement |
配置类注解 | 配置类上方 | 在Spring环境中开启注解式事务支持 |
@Transactional |
方法/类注解 | 业务层接口/实现类方法上方 | 为当前业务方法添加事务(类上则类中所有方法生效) |
4.3 事务生效原理:事务管理员&事务协调员
未开启事务前
outMoney和inMoney各自开启独立事务T1、T2。transfer方法无事务,中间出现异常时,T1已提交、T2未执行,数据异常。

开启事务后
transfer方法加@Transactional,成为事务管理员,开启全局事务T。outMoney和inMoney作为事务协调员,将各自的事务T1、T2加入到全局事务T中。- 业务层出现异常时,整个事务T回滚,T1、T2全部回滚,保证数据一致性。
关键注意:事务管理器和协调员必须使用同一个DataSource,否则无法统一事务,这是Bean单例默认保证的。
补充:两个核心疑问精准解答
问题一:outMoney(T1)、inMoney(T2) 是谁生成的事务?
答案:它们是【数据层(MyBatis/JDBC)自动生成的事务】,不是业务方法自己生成的。
详细拆解:
- 没有 Spring 事务时
- 调用
accountDao.outMoney()→ 执行一条 SQL - JDBC/MyBatis 自动开启事务 T1 → 执行完自动提交
- 再调用
accountDao.inMoney()→ 自动开启事务 T2 → 自动提交 - 结论:T1、T2 都是数据层自动产生的独立事务
- 有 Spring 事务时
- 业务方法
transfer()加@Transactional - Spring 提前开启一个大事务 T
- 数据层 T1、T2 不再独立提交,而是加入到大事务 T
- 最终由 Spring 统一提交/回滚
一句话总结
outMoney / inMoney 的事务是数据层(JDBC/MyBatis)默认自动提交事务,不是业务层方法创建的。
问题二:LogDao 接口为什么不用加 @Mapper 注解?
答案:完全不用加!因为全局扫描已自动识别所有 Dao 为 Mapper。
关键配置:
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
// 扫描该包下所有接口,自动注册为 MyBatis Mapper
msc.setBasePackage("com.itheima.dao");
return msc;
}
两种写法等价:
写法1(全局扫描,企业推荐):
// 无需 @Mapper,正常生效
public interface LogDao { ... }
写法2(显式声明):
@Mapper // 可加可不加,仅显式标记
public interface LogDao { ... }
最终结论
只要配置了 MapperScannerConfigurer 扫描包,所有 Dao 接口都不需要加 @Mapper!
五、Spring事务属性详解
@Transactional注解支持多个属性配置,用于精细化控制事务行为。
5.1 核心属性表
| 属性 | 作用 | 示例 |
|---|---|---|
readOnly |
设置是否为只读事务 | readOnly=true 只读事务(查询用),false读写事务(增删改用) |
timeout |
设置事务超时时间(秒) | timeout = -1 永不超时 |
rollbackFor |
设置事务回滚异常(Class类型) | rollbackFor = {IOException.class} |
rollbackForClassName |
设置事务回滚异常(String全类名) | 同rollbackFor,参数为字符串 |
noRollbackFor |
设置事务不回滚异常(Class类型) | noRollbackFor = {NullPointerException.class} |
noRollbackForClassName |
设置事务不回滚异常(String全类名) | 同noRollbackFor,参数为字符串 |
isolation |
设置事务隔离级别 | isolation = Isolation.DEFAULT |
propagation |
设置事务传播行为 | propagation = Propagation.REQUIRED(默认) |
5.2 关键属性深度解析
1. rollbackFor:自定义回滚异常
Spring事务默认只对Error和RuntimeException及其子类回滚,受检异常(如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; // RuntimeException,事务会回滚
if(true){
throw new IOException(); // 受检异常,事务不会回滚!
}
accountDao.inMoney(in,money);
}
}
解决方法:用rollbackFor指定回滚异常:
@Transactional(rollbackFor = {IOException.class})
public void transfer(String out,String in ,Double money) throws IOException{
// 业务代码
}
2. isolation:事务隔离级别
| 隔离级别 | 说明 |
|---|---|
DEFAULT |
默认级别,使用数据库的隔离级别 |
READ_UNCOMMITTED |
读未提交(存在脏读、不可重复读、幻读) |
READ_COMMITTED |
读已提交(解决脏读,存在不可重复读、幻读,Oracle默认) |
REPEATABLE_READ |
重复读(解决脏读、不可重复读,存在幻读,MySQL默认) |
SERIALIZABLE |
串行化(解决所有问题,性能差) |
六、事务传播行为:解决转账日志问题
6.1 新需求:转账后记录日志
在转账案例基础上,新增需求:无论转账是否成功,都要记录转账日志到数据库。
需求实现步骤
-
创建日志表
tbl_logcreate table tbl_log(
id int primary key auto_increment,
info varchar(255),
createDate datetime
) -
创建LogDao、LogService
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}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); }}
-
在转账业务中添加日志记录
@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); int i = 1/0; accountDao.inMoney(in,money); }finally { logService.log(out,in,money); } }}
问题复现
转账出现异常时,转账操作回滚了,但日志也没有记录!
原因 :log()方法加了@Transactional,默认传播行为是REQUIRED,会加入transfer的全局事务。当transfer事务回滚时,log的事务也会一起回滚,导致日志丢失。
6.2 事务传播行为核心概念
事务传播行为:事务协调员对事务管理员所携带事务的处理态度,即「是否加入管理员的事务,还是开启新事务」。
解决方案:修改日志方法的传播行为
将log()方法的传播行为设置为REQUIRES_NEW,让它每次都开启新事务,不加入外部事务:
@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);
}
}
运行后,无论转账是否成功,日志都会正常记录,完美满足需求。
6.3 事务传播行为可选值

七、事务生效三要素:缺一不可
很多同学会遇到「加了@Transactional但事务不生效」的问题,核心是没满足三个必要条件:
- 配置事务管理器Bean :向Spring容器注册
PlatformTransactionManager,事务的真正执行者。 - 添加@Transactional注解:给方法/类打标签,声明需要事务。
- 开启@EnableTransactionManagement:开启注解式事务驱动,相当于接通电路。
通俗类比 :
transactionManager= 事务发动机@Transactional= 点火钥匙@EnableTransactionManagement= 电路接通
三者缺一,事务都无法启动!
八、总结
本文从转账案例出发,完整覆盖了Spring AOP事务管理的核心知识点:
- 基础概念:事务的作用、Spring事务管理器的核心原理。
- 实战案例:从零搭建转账环境,复现无事务异常,通过声明式事务解决问题。
- 事务属性 :
readOnly、timeout、rollbackFor、isolation等属性的使用场景。 - 传播行为 :通过日志案例理解
REQUIRED和REQUIRES_NEW的核心区别,解决实际开发中的常见问题。 - 生效条件:事务生效的三要素,帮你排查事务不生效的问题。
- 核心疑问:数据层事务来源、@Mapper注解省略原理,面试高频必懂。
Spring事务是后端开发的核心技能,掌握这些知识点,就能应对绝大多数业务场景的事务管理需求。
九、常见问题排查
Q1:加了@Transactional但事务不生效?
A:检查三个点:
- 是否配置了
PlatformTransactionManagerBean。 - 是否加了
@EnableTransactionManagement。 - 方法是否是
public(@Transactional对private方法不生效)。 - 是否是同类内部调用(AOP代理不生效,需通过ApplicationContext获取代理对象)。
Q2:为什么受检异常不会回滚?
A :Spring默认只对RuntimeException和Error回滚,受检异常需通过rollbackFor手动指定。
Q3:日志方法为什么要设置REQUIRES_NEW?
A :默认REQUIRED会加入外部事务,外部事务回滚时日志也会回滚;REQUIRES_NEW开启独立事务,不受外部事务影响,保证日志一定记录。