SpringBoot 事务管理完整详解

在后端开发中,只要涉及多表联动操作(新增员工+新增工作经历、银行转账、下单扣库存),就一定会用到事务。如果没有事务控制,一旦中间步骤报错,就会出现一半数据入库、一半丢失的数据错乱问题。

一、基础认知:事务是什么?四大ACID特性

1. 事务定义

事务是一组数据库操作的集合,是不可分割的最小工作单元,这一组SQL要么全部执行成功提交,要么全部失败回滚 ,不存在中间状态。

典型业务场景:

  1. 银行转账:A扣钱、B加钱,任意一步失败,两边数据都恢复原样
  2. 新增员工:主表emp插入员工、子表emp_expr批量插入工作经历,报错则两张表都不存数据
  3. 下单流程:创建订单、扣减库存、生成物流单

2. 事务四大ACID特性

特性 全称 核心解释
原子性 Atomicity 事务不可拆分,全成功/全失败(事务最核心特性)
一致性 Consistency 事务执行前后,数据库数据整体合法、状态统一(转账前后总金额不变)
隔离性 Isolation 多个并发事务之间互相隔离,互不干扰,避免脏读、不可重复读等问题
持久性 Durability 事务提交后,数据修改永久保存到磁盘,宕机也不会丢失

二、底层基础:MySQL原生手动事务控制

在Spring封装事务之前,数据库原生提供三条指令手动管控事务,这是Spring事务的底层原理。

1. 三条核心SQL

sql 复制代码
-- 1. 开启事务
start transaction;
-- 等价写法
begin;

-- 业务SQL(多条增删改)
insert into emp(...) values(...);
insert into emp_expr(...) values(...);

-- 2. 全部执行成功,提交事务,数据永久生效
commit;

-- 3. 任意步骤异常,回滚事务,撤销所有操作
rollback;

2. 实战案例:新增员工双表操作

sql 复制代码
begin;
-- 插入员工主表
insert into emp(username,name,gender,phone,dept_id) values ('zhangsan','张三',1,'13300001111',1);
-- 插入员工工作经历子表
insert into emp_expr(emp_id,begin,end,company,job) values (39,'2019-01-01','2022-01-01','百度','开发');
-- 无异常提交,两张表数据同时入库
commit;
-- 若第二条insert报错,执行rollback,主表数据也会消失
rollback;

原生事务痛点

每次业务都要手动写开启、提交、回滚,代码冗余;异常捕获逻辑重复,Spring提供声明式事务@Transactional简化开发。

三、SpringBoot核心:@Transactional 声明式事务

1. 注解作用

将被标注的方法交给Spring事务管理器自动管控,完全替代手动写MySQL事务指令:

  1. 方法执行前:自动开启事务
  2. 方法正常执行完毕:自动commit提交事务
  3. 方法抛出异常:自动rollback回滚事务

2. 注解可以标注的位置(推荐优先级:方法 > 类 > 接口)

  1. Service层方法(生产环境首选)
    精准控制只有当前业务方法开启事务,灵活性最高。

    java 复制代码
    @Service
    public class EmpServiceImpl implements EmpService{
        @Override
        @Transactional // 仅save方法开启事务
        public void save(Emp emp){
            // 新增员工+批量新增经历
        }
    }
  2. Service实现类上
    当前类所有public方法都会开启事务,适合整个类所有操作都需要事务的场景。

  3. Service接口上(不推荐)
    仅JDK动态代理生效,CGLIB代理场景事务失效,兼容性差,企业开发极少使用。

3. 基础实战:新增员工多表事务案例

需求:新增员工时,同时保存员工主表、批量保存员工多条工作经历,中间报错全部回滚。

① Mapper层铺垫:插入后回填自增主键

新增员工后需要拿到数据库自增的id,作为子表emp_id外键,使用@Options注解自动回填主键:

java 复制代码
@Mapper
public interface EmpMapper {
    // useGeneratedKeys:开启自增主键获取
    // keyProperty:主键值回填到Emp实体的id字段
    @Options(useGeneratedKeys = true, keyProperty = "id")
    @Insert("insert into emp(username,name,gender,phone,job,salary,image,entry_date,dept_id,create_time,update_time) values(#{username},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
    void insert(Emp emp);
}

调用empMapper.insert(emp)后,直接emp.getId()即可拿到数据库生成的主键。

② 批量插入动态SQL

批量保存多条工作经历,MyBatis动态SQL<foreach>遍历集合拼接SQL:

xml 复制代码
<insert id="insertBatch">
    insert into emp_expr(emp_id,begin,end,company,job)
    values
    <foreach collection="exprList" item="item" separator=",">
        (#{item.empId},#{item.begin},#{item.end},#{item.company},#{item.job})
    </foreach>
</insert>

<foreach>核心属性说明:

  • collection:待遍历集合名
  • item:遍历循环中的单个对象
  • separator:每条数据之间的分隔符(逗号)
  • open/close:循环前后拼接片段(批量in查询常用)
③ Service层事务完整代码
java 复制代码
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;
    @Autowired
    private EmpExprMapper empExprMapper;

    @Override
    @Transactional // 当前方法整体受事务管控
    public void save(Emp emp) {
        // 1. 补全创建、更新时间
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        // 2. 插入员工主表,自动回填id
        empMapper.insert(emp);
        // 3. 获取自增主键,给工作经历设置外键
        Integer empId = emp.getId();
        List<EmpExpr> exprList = emp.getExprList();
        if(!CollectionUtils.isEmpty(exprList)){
            exprList.forEach(item -> item.setEmpId(empId));
            // 4. 批量插入工作经历子表
            empExprMapper.insertBatch(exprList);
        }
        // 模拟异常:int i = 1/0; 报错后两张表数据全部回滚
    }
}

测试:代码中添加除零异常,执行后emp、emp_expr两张表都不会新增任何数据,事务生效。

四、@Transactional 进阶核心属性(面试高频)

1. rollbackFor:控制什么异常触发回滚

默认规则

Spring事务只会在运行时异常RuntimeException、错误Error发生时自动回滚;编译时异常(Exception子类)不会触发回滚。

业务痛点

如果方法抛出文件不存在、IO异常等编译异常,数据不会回滚,造成脏数据。

全局解决方案:捕获所有Exception都回滚
java 复制代码
// 任意Exception都触发事务回滚
@Transactional(rollbackFor = Exception.class)
public void save(Emp emp){
    // 业务代码
}

2. propagation:事务传播行为(嵌套事务核心)

定义

当一个加了@Transactional的方法,调用另一个带事务的方法时,子方法该复用外层事务、还是新建独立事务,由传播行为控制。

最常用两种场景
  1. REQUIRED(默认值,绝大多数业务使用)
    • 逻辑:外层已有事务,子方法加入同一个事务;外层无事务,新建事务
    • 案例:新增员工、保存经历共用一个事务,要么一起成功,要么一起回滚
  2. REQUIRES_NEW(独立事务,互不干扰)
    • 逻辑:无论外层是否存在事务,子方法新建完全独立的事务,外层回滚不会影响子方法提交的数据
    • 经典业务场景:操作日志永久保存
      需求:新增员工无论成功/失败,操作日志必须入库,不能跟着员工事务回滚。
日志落地完整代码
  1. 日志Service,使用REQUIRES_NEW新建独立事务
java 复制代码
@Service
public class EmpLogServiceImpl implements EmpLogService {
    @Autowired
    private EmpLogMapper empLogMapper;

    // 独立事务,不受外层员工事务影响
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insertLog(EmpLog empLog) {
        empLogMapper.insert(empLog);
    }
}
  1. 新增员工主业务,finally中强制记录日志
java 复制代码
@Override
@Transactional(rollbackFor = Exception.class)
public void save(Emp emp) {
    try {
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.insert(emp);
        Integer empId = emp.getId();
        List<EmpExpr> exprList = emp.getExprList();
        if(!CollectionUtils.isEmpty(exprList)){
            exprList.forEach(item -> item.setEmpId(empId));
            empExprMapper.insertBatch(exprList);
        }
        // int i = 1/0; // 模拟业务异常
    } finally {
        // 无论try成功/报错,finally一定执行,日志独立入库
        EmpLog log = new EmpLog(null, LocalDateTime.now(), "新增员工:"+emp.getName());
        empLogService.insertLog(log);
    }
}

测试:即使员工新增代码抛出异常,员工数据回滚,但操作日志依然保存到数据库,两个事务完全隔离。

其余传播行为简要说明
传播属性 核心逻辑 使用场景
SUPPORTS 外层有事务就共用,无事务则无事务执行 查询类方法,不需要强制事务
NOT_SUPPORTED 挂起外层事务,子方法无事务执行 纯查询、不需要事务的操作
MANDATORY 必须存在外层事务,否则直接抛异常 子方法依赖外层事务,禁止单独调用
NEVER 禁止存在外层事务,有外层事务直接报错 明确要求无事务执行的方法

五、事务调试:开启事务日志打印

开发排查事务提交/回滚异常时,可在application.yml配置事务管理器debug日志,控制台打印完整事务执行流程:

yaml 复制代码
logging:
  level:
    # Spring事务管理器日志
    org.springframework.jdbc.support.JdbcTransactionManager: debug

配置后可以清晰看到:事务开启、SQL执行、事务提交/回滚完整日志,快速定位事务失效问题。

六、总结与面试核心考点梳理

  1. 事务ACID四大特性:原子性是核心,一致性是最终目标,隔离性解决并发冲突,持久性保证数据落地。
  2. 原生事务三步 :begin开启、commit提交、rollback回滚,Spring @Transactional自动封装这三步。
  3. @Transactional基础 :推荐标注在Service层public方法;默认仅RuntimeException回滚,建议统一配置rollbackFor=Exception.class
  4. 事务传播行为
    • REQUIRED默认:多表联动新增、修改,共用一个事务;
    • REQUIRES_NEW:日志、消息记录等需要独立持久化,不受外层事务回滚影响。
  5. 实战重点场景
    • 多表CRUD必须加事务,防止数据不一致;
    • 操作日志、异步记录等场景使用REQUIRES_NEW隔离事务;
    • 批量插入使用MyBatis <foreach>动态SQL简化代码;
    • 自增主键回填依靠@Options(useGeneratedKeys = true)
  1. @Transactional只对public方法生效,private/protected方法标注无效;
  2. 同类中方法自调用(A方法调用本类带事务的B方法)事务会失效,需通过注入自身代理对象调用;
  3. 传播行为REQUIRES_NEW会创建独立连接,频繁使用会消耗数据库连接池资源,按需使用。