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

相关推荐
Seven971 分钟前
BIO详解:解锁阻塞IO的使用方式
java
摸鱼的春哥17 分钟前
Agent教程17:LangChain的持久化和人工干预
前端·javascript·后端
风象南33 分钟前
OpenClaw 登顶 GitHub Star 榜首:一个程序员 13 年后的"重新点火"故事
人工智能·后端
Victor35640 分钟前
MongoDB(25)什么是单字段索引?
后端
Victor35644 分钟前
MongoDB(26)什么是复合索引?
后端
程序员爱钓鱼2 小时前
Go操作Excel实战详解:github.com/xuri/excelize/v2
前端·后端·go
oak隔壁找我9 小时前
MySQL中 SHOW FULL PROCESSLIST` 输出中 `State` 列的所有可能值
后端
上进小菜猪10 小时前
基于 YOLOv8 的面向文档智能处理的表格区域检测系统 [目标检测完整源码]
后端
oak隔壁找我10 小时前
JVM常用调优参数
java·后端
IT_陈寒14 小时前
React状态管理终极对决:Redux vs Context API谁更胜一筹?
前端·人工智能·后端