SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
文章摘要
本文深入讲解 SpringBoot 中 @Transactional 注解的使用方法、事务传播机制、回滚规则以及常见的坑。通过实战案例(用户注册 + 初始化默认数据)演示如何正确使用事务,避免数据不一致问题。适合有一定 SpringBoot 基础的开发者阅读。
一、为什么需要事务?
1.1 什么是事务?
事务(Transaction)是数据库操作的最小工作单元,要么全部成功,要么全部失败。事务必须满足 ACID 四大特性:
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成
- 一致性(Consistency):事务前后数据的完整性必须保持一致
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务
- 持久性(Durability):事务一旦提交,对数据库的改变是永久性的
1.2 实际场景:用户注册
假设我们有一个用户注册功能,需要完成两件事:
- 插入用户信息到
user表 - 为用户初始化默认的账单类型到
bill_type表(餐饮、交通、工资等)
如果没有事务会怎样?
java
// 没有事务的代码
public User register(User user) {
// 1. 插入用户(成功)
userMapper.insert(user);
// 2. 初始化默认类型(失败!比如数据库连接断开)
initDefaultBillType(user.getUserId()); // 抛出异常
return user;
}
问题:用户数据已经插入成功,但默认类型初始化失败,导致数据不一致!用户登录后发现没有任何账单类型可用。
有了事务:
java
// 使用事务
@Transactional(rollbackFor = Exception.class)
public User register(User user) {
// 1. 插入用户
userMapper.insert(user);
// 2. 初始化默认类型(如果失败,用户数据也会回滚)
initDefaultBillType(user.getUserId());
return user;
}
效果:任何一步失败,所有操作都会回滚,保证数据一致性!
二、@Transactional 基础用法
2.1 添加依赖
SpringBoot 项目中,spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 已经包含了事务支持,无需额外添加依赖。
2.2 启用事务
SpringBoot 默认已启用事务,无需手动配置 @EnableTransactionManagement。
2.3 基本使用
java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private BillTypeMapper billTypeMapper;
// 方法级别的事务
@Transactional(rollbackFor = Exception.class)
public User register(User user) {
// 1. 插入用户
userMapper.insert(user);
// 2. 初始化默认类型
initDefaultBillType(user.getUserId());
return user;
}
// 私有方法,不需要事务注解
private void initDefaultBillType(Long userId) {
List<BillType> defaultTypes = new ArrayList<>();
defaultTypes.add(new BillType(null, "餐饮", 0, userId, 0, 0));
defaultTypes.add(new BillType(null, "交通", 0, userId, 0, 0));
defaultTypes.add(new BillType(null, "工资", 1, userId, 0, 0));
for (BillType type : defaultTypes) {
billTypeMapper.insert(type);
}
}
}
关键点:
@Transactional加在 public 方法 上rollbackFor = Exception.class表示所有异常都回滚(推荐)- 私有方法
initDefaultBillType不需要加@Transactional,它会在register的事务中执行
三、事务传播机制(重点!)
事务传播机制定义了当一个事务方法调用另一个事务方法时,事务应该如何传播。
3.1 七种传播机制
| 传播机制 | 说明 |
|---|---|
| REQUIRED(默认) | 如果当前存在事务,则加入该事务;如果不存在,则创建新事务 |
| REQUIRES_NEW | 创建新事务,如果当前存在事务,则挂起当前事务 |
| NESTED | 如果当前存在事务,则在嵌套事务内执行;如果不存在,则创建新事务 |
| SUPPORTS | 如果当前存在事务,则加入该事务;如果不存在,则以非事务方式执行 |
| NOT_SUPPORTED | 以非事务方式执行,如果当前存在事务,则挂起当前事务 |
| MANDATORY | 如果当前存在事务,则加入该事务;如果不存在,则抛出异常 |
| NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常 |
最常用的是前三种,下面重点讲解。
3.2 REQUIRED(默认)- 加入当前事务
java
@Service
public class UserServiceImpl {
@Autowired
private OrderService orderService;
// 外层事务
@Transactional(rollbackFor = Exception.class)
public void createUserAndOrder() {
// 1. 创建用户
userMapper.insert(user);
// 2. 创建订单(加入当前事务)
orderService.createOrder(order); // REQUIRED
// 如果这里抛异常,用户和订单都会回滚
}
}
@Service
public class OrderServiceImpl {
// 默认 REQUIRED
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
orderMapper.insert(order);
// 如果这里抛异常,用户和订单都会回滚
}
}
效果:
createOrder加入createUserAndOrder的事务- 任何一个方法抛异常,所有操作都回滚
- 只有一个数据库事务
使用场景: 大部分情况下使用默认的 REQUIRED 即可。
3.3 REQUIRES_NEW - 创建新事务
java
@Service
public class UserServiceImpl {
@Autowired
private LogService logService;
// 外层事务
@Transactional(rollbackFor = Exception.class)
public void createUser() {
// 1. 创建用户
userMapper.insert(user);
// 2. 记录日志(新事务,独立提交)
logService.saveLog(log); // REQUIRES_NEW
// 3. 如果这里抛异常,用户回滚,但日志已提交
throw new RuntimeException("测试异常");
}
}
@Service
public class LogServiceImpl {
// 创建新事务
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveLog(Log log) {
logMapper.insert(log);
// 这个方法执行完立即提交,不受外层事务影响
}
}
效果:
saveLog创建新事务,独立于createUser的事务saveLog执行完立即提交- 即使
createUser回滚,日志也已经保存成功
使用场景:
- 记录操作日志(无论业务成功失败,日志都要保存)
- 发送通知、消息(不影响主业务)
3.4 NESTED - 嵌套事务
java
@Service
public class UserServiceImpl {
@Autowired
private PointService pointService;
// 外层事务
@Transactional(rollbackFor = Exception.class)
public void createUser() {
// 1. 创建用户
userMapper.insert(user);
try {
// 2. 赠送积分(嵌套事务)
pointService.givePoints(userId, 100); // NESTED
} catch (Exception e) {
// 积分赠送失败,不影响用户创建
log.error("赠送积分失败", e);
}
// 用户创建成功,积分可能失败
}
}
@Service
public class PointServiceImpl {
// 嵌套事务
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void givePoints(Long userId, Integer points) {
pointMapper.insert(new Point(userId, points));
// 如果这里抛异常,只回滚积分,不影响用户创建
}
}
效果:
givePoints在createUser的事务内创建一个保存点(Savepoint)- 如果
givePoints失败且被 catch,只回滚到保存点,不影响外层事务 - 如果外层事务回滚,嵌套事务也会回滚
使用场景:
- 可选的子操作(失败不影响主流程)
- 部分回滚场景
NESTED vs REQUIRES_NEW 对比:
| 特性 | NESTED | REQUIRES_NEW |
|---|---|---|
| 是否创建新事务 | 否,创建保存点 | 是,完全独立的事务 |
| 外层回滚 | 嵌套事务也回滚 | 新事务不受影响 |
| 内层回滚 | 可以只回滚内层 | 新事务独立回滚 |
| 数据库支持 | 需要数据库支持保存点 | 所有数据库都支持 |
四、事务回滚规则
4.1 默认回滚规则
Spring 默认只回滚 RuntimeException 和 Error,不回滚 Checked Exception(编译时异常)。
java
// 错误示例:Checked Exception 不会回滚
@Transactional
public void createUser() throws Exception {
userMapper.insert(user);
// 抛出 Checked Exception,事务不会回滚!
throw new Exception("业务异常");
}
问题: 用户数据已经插入,但抛出了异常,数据不一致!
4.2 正确做法:指定 rollbackFor
java
// 正确示例:所有异常都回滚
@Transactional(rollbackFor = Exception.class)
public void createUser() throws Exception {
userMapper.insert(user);
// 抛出任何异常,事务都会回滚
throw new Exception("业务异常");
}
推荐: 始终使用 rollbackFor = Exception.class,确保所有异常都回滚。
4.3 noRollbackFor - 指定不回滚的异常
java
// 特定异常不回滚
@Transactional(rollbackFor = Exception.class, noRollbackFor = BusinessException.class)
public void createUser() {
userMapper.insert(user);
// 抛出 BusinessException,事务不会回滚
throw new BusinessException("业务提示");
}
使用场景: 某些业务异常只是提示信息,不需要回滚数据。
五、常见的坑(重点!)
5.1 坑一:同类方法调用,事务不生效
java
@Service
public class UserServiceImpl {
// 没有事务
public void register(User user) {
// 调用同类的事务方法
this.saveUser(user); // 事务不生效!
}
@Transactional(rollbackFor = Exception.class)
public void saveUser(User user) {
userMapper.insert(user);
throw new RuntimeException("测试异常");
}
}
问题: saveUser 的事务不生效,数据不会回滚!
原因: Spring 事务是基于 AOP 代理实现的,this.saveUser() 调用的是原始对象,不是代理对象,所以事务不生效。
解决方案一: 拆分到不同的 Service
java
@Service
public class UserServiceImpl {
@Autowired
private UserDataService userDataService;
public void register(User user) {
// 调用另一个 Service 的事务方法
userDataService.saveUser(user); // 事务生效
}
}
@Service
public class UserDataServiceImpl {
@Transactional(rollbackFor = Exception.class)
public void saveUser(User user) {
userMapper.insert(user);
throw new RuntimeException("测试异常");
}
}
解决方案二: 注入自己(不推荐)
java
@Service
public class UserServiceImpl {
@Autowired
private UserServiceImpl self; // 注入自己
public void register(User user) {
// 通过代理对象调用
self.saveUser(user); // 事务生效
}
@Transactional(rollbackFor = Exception.class)
public void saveUser(User user) {
userMapper.insert(user);
throw new RuntimeException("测试异常");
}
}
5.2 坑二:异常被 catch,事务不回滚
java
@Transactional(rollbackFor = Exception.class)
public void createUser() {
try {
userMapper.insert(user);
throw new RuntimeException("测试异常");
} catch (Exception e) {
// 异常被 catch,事务不会回滚!
log.error("插入用户失败", e);
}
}
问题: 异常被 catch 了,Spring 认为方法正常执行,不会回滚事务。
解决方案一: 重新抛出异常
java
@Transactional(rollbackFor = Exception.class)
public void createUser() {
try {
userMapper.insert(user);
throw new RuntimeException("测试异常");
} catch (Exception e) {
log.error("插入用户失败", e);
throw e; // 重新抛出,事务回滚
}
}
解决方案二: 手动回滚
java
@Transactional(rollbackFor = Exception.class)
public void createUser() {
try {
userMapper.insert(user);
throw new RuntimeException("测试异常");
} catch (Exception e) {
log.error("插入用户失败", e);
// 手动标记回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
5.3 坑三:事务方法必须是 public
java
@Service
public class UserServiceImpl {
// private 方法,事务不生效
@Transactional(rollbackFor = Exception.class)
private void saveUser(User user) {
userMapper.insert(user);
}
// protected 方法,事务不生效
@Transactional(rollbackFor = Exception.class)
protected void updateUser(User user) {
userMapper.updateById(user);
}
// public 方法,事务生效
@Transactional(rollbackFor = Exception.class)
public void deleteUser(Long userId) {
userMapper.deleteById(userId);
}
}
原因: Spring AOP 默认只代理 public 方法。
解决方案: 事务方法必须是 public。
5.4 坑四:数据库引擎不支持事务
sql
-- MyISAM 引擎不支持事务
CREATE TABLE user (
user_id BIGINT PRIMARY KEY,
username VARCHAR(50)
) ENGINE=MyISAM;
-- InnoDB 引擎支持事务
CREATE TABLE user (
user_id BIGINT PRIMARY KEY,
username VARCHAR(50)
) ENGINE=InnoDB;
问题: 如果使用 MyISAM 引擎,事务不会生效。
解决方案: 使用 InnoDB 引擎(MySQL 5.5+ 默认)。
5.5 坑五:多线程中的事务
java
@Transactional(rollbackFor = Exception.class)
public void createUser() {
userMapper.insert(user);
// 新线程中的操作不在事务中
new Thread(() -> {
billTypeMapper.insert(billType); // 不受事务控制
}).start();
}
问题: 事务是基于 ThreadLocal 的,新线程中的操作不在事务中。
解决方案: 不要在事务方法中开启新线程,或者使用消息队列异步处理。
六、实战案例:用户注册
6.1 需求
用户注册时需要完成:
- 插入用户信息到
user表 - 为用户初始化 9 个默认账单类型到
bill_type表
要求:要么全部成功,要么全部失败。
6.2 完整代码
java
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private BillTypeMapper billTypeMapper;
/**
* 用户注册(带事务)
* 1. 插入用户
* 2. 初始化默认账单类型
* 任何一步失败,全部回滚
*/
@Override
@Transactional(rollbackFor = Exception.class)
public User register(User user) {
log.info("新用户注册:{}", user.getUsername());
// 1. 校验用户名是否重复
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, user.getUsername());
if (userMapper.exists(wrapper)) {
log.warn("用户注册失败:用户名 {} 已存在", user.getUsername());
throw new RuntimeException("用户名已存在");
}
// 2. 插入用户
userMapper.insert(user);
log.info("用户 {} 注册成功,userId:{}", user.getUsername(), user.getUserId());
// 3. 初始化默认收支类型(在同一个事务中)
initDefaultBillType(user.getUserId());
// 4. 返回注册成功的用户对象(包含自动生成的userId)
return user;
}
/**
* 初始化默认收支类型(私有方法,不需要事务注解)
* 这个方法会在 register 的事务中执行
*/
private void initDefaultBillType(Long userId) {
log.debug("为用户 {} 初始化默认收支类型", userId);
List<BillType> defaultTypes = new ArrayList<>();
// 支出类型
defaultTypes.add(new BillType(null, "餐饮", 0, userId, 0, 0));
defaultTypes.add(new BillType(null, "水果", 0, userId, 0, 0));
defaultTypes.add(new BillType(null, "零食", 0, userId, 0, 0));
defaultTypes.add(new BillType(null, "买菜", 0, userId, 0, 0));
defaultTypes.add(new BillType(null, "交通", 0, userId, 0, 0));
// 收入类型
defaultTypes.add(new BillType(null, "工资", 1, userId, 0, 0));
defaultTypes.add(new BillType(null, "兼职", 1, userId, 0, 0));
defaultTypes.add(new BillType(null, "生活费", 1, userId, 0, 0));
defaultTypes.add(new BillType(null, "收红包", 1, userId, 0, 0));
// 循环单条插入
for (BillType type : defaultTypes) {
billTypeMapper.insert(type);
}
log.info("用户 {} 默认收支类型初始化完成,共 {} 个类型", userId, defaultTypes.size());
}
}
6.3 测试事务回滚
java
// 模拟初始化类型时失败
private void initDefaultBillType(Long userId) {
List<BillType> defaultTypes = new ArrayList<>();
defaultTypes.add(new BillType(null, "餐饮", 0, userId, 0, 0));
for (BillType type : defaultTypes) {
billTypeMapper.insert(type);
}
// 模拟异常
throw new RuntimeException("初始化类型失败");
}
测试结果:
- 用户数据不会插入(事务回滚)
- 账单类型数据也不会插入(事务回滚)
- 数据库保持一致性
七、性能优化建议
7.1 控制事务粒度
java
// 事务粒度太大
@Transactional(rollbackFor = Exception.class)
public void processOrder() {
// 1. 查询用户信息(耗时操作)
User user = userService.getById(userId);
// 2. 调用第三方支付接口(耗时操作)
paymentService.pay(order);
// 3. 更新订单状态(需要事务)
orderMapper.updateStatus(orderId, "PAID");
}
// 事务粒度合理
public void processOrder() {
// 1. 查询用户信息(不需要事务)
User user = userService.getById(userId);
// 2. 调用第三方支付接口(不需要事务)
paymentService.pay(order);
// 3. 更新订单状态(只在这里开启事务)
updateOrderStatus(orderId, "PAID");
}
@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus(Long orderId, String status) {
orderMapper.updateStatus(orderId, status);
}
原则: 事务范围越小越好,只在需要保证原子性的操作上使用事务。
7.2 避免长事务
java
// 长事务
@Transactional(rollbackFor = Exception.class)
public void batchProcess() {
List<Order> orders = orderMapper.selectAll(); // 查询 10000 条数据
for (Order order : orders) {
// 处理每个订单(耗时操作)
processOrder(order);
}
}
// 分批处理
public void batchProcess() {
int pageSize = 100;
int pageNum = 1;
while (true) {
List<Order> orders = orderMapper.selectPage(pageNum, pageSize);
if (orders.isEmpty()) {
break;
}
// 每批开启一个事务
processBatch(orders);
pageNum++;
}
}
@Transactional(rollbackFor = Exception.class)
public void processBatch(List<Order> orders) {
for (Order order : orders) {
processOrder(order);
}
}
原则: 长事务会占用数据库连接,影响并发性能,应该分批处理。
7.3 只读事务优化
java
// 只读事务,提升性能
@Transactional(readOnly = true)
public List<User> listUsers() {
return userMapper.selectList(null);
}
好处:
- 数据库可以进行优化(如不加锁)
- 提升查询性能
八、总结
8.1 核心要点
- 始终使用
rollbackFor = Exception.class,确保所有异常都回滚 - 事务方法必须是 public,否则事务不生效
- 避免同类方法调用,会导致事务失效
- 不要 catch 异常后不处理,要么重新抛出,要么手动回滚
- 控制事务粒度,事务范围越小越好
- 避免长事务,影响并发性能
8.2 事务传播机制选择
- REQUIRED(默认):大部分场景使用
- REQUIRES_NEW:记录日志、发送通知等独立操作
- NESTED:可选的子操作,失败不影响主流程
8.3 最佳实践
java
@Service
public class UserServiceImpl {
// 推荐写法
@Transactional(rollbackFor = Exception.class)
public User register(User user) {
// 1. 参数校验
validateUser(user);
// 2. 业务逻辑
userMapper.insert(user);
initDefaultBillType(user.getUserId());
// 3. 返回结果
return user;
}
// 私有方法,不需要事务注解
private void validateUser(User user) {
if (user.getUsername() == null) {
throw new IllegalArgumentException("用户名不能为空");
}
}
// 私有方法,在 register 的事务中执行
private void initDefaultBillType(Long userId) {
// ...
}
}
九、参考资料
如果这篇文章对你有帮助,请点赞、收藏、关注!有问题欢迎在评论区讨论。
作者:[识君啊]
不要做API的搬运工,要做原理的探索者!