SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑

SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑

文章摘要

本文深入讲解 SpringBoot 中 @Transactional 注解的使用方法、事务传播机制、回滚规则以及常见的坑。通过实战案例(用户注册 + 初始化默认数据)演示如何正确使用事务,避免数据不一致问题。适合有一定 SpringBoot 基础的开发者阅读。


一、为什么需要事务?

1.1 什么是事务?

事务(Transaction)是数据库操作的最小工作单元,要么全部成功,要么全部失败。事务必须满足 ACID 四大特性:

  • 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成
  • 一致性(Consistency):事务前后数据的完整性必须保持一致
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务
  • 持久性(Durability):事务一旦提交,对数据库的改变是永久性的

1.2 实际场景:用户注册

假设我们有一个用户注册功能,需要完成两件事:

  1. 插入用户信息到 user
  2. 为用户初始化默认的账单类型到 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-jdbcspring-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));
        // 如果这里抛异常,只回滚积分,不影响用户创建
    }
}

效果:

  • givePointscreateUser 的事务内创建一个保存点(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 需求

用户注册时需要完成:

  1. 插入用户信息到 user
  2. 为用户初始化 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 核心要点

  1. 始终使用 rollbackFor = Exception.class,确保所有异常都回滚
  2. 事务方法必须是 public,否则事务不生效
  3. 避免同类方法调用,会导致事务失效
  4. 不要 catch 异常后不处理,要么重新抛出,要么手动回滚
  5. 控制事务粒度,事务范围越小越好
  6. 避免长事务,影响并发性能

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的搬运工,要做原理的探索者!

相关推荐
小高不会迪斯科5 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle
e***8905 小时前
MySQL 8.0版本JDBC驱动Jar包
数据库·mysql·jar
l1t5 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划6 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿6 小时前
Jsoniter(java版本)使用介绍
java·开发语言
Victor3566 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor3566 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
探路者继续奋斗7 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
失忆爆表症7 小时前
03_数据库配置指南:PostgreSQL 17 + pgvector 向量存储
数据库·postgresql
AI_56787 小时前
Excel数据透视表提速:Power Query预处理百万数据
数据库·excel