在后端开发中,只要涉及多表联动操作(新增员工+新增工作经历、银行转账、下单扣库存),就一定会用到事务。如果没有事务控制,一旦中间步骤报错,就会出现一半数据入库、一半丢失的数据错乱问题。
一、基础认知:事务是什么?四大ACID特性
1. 事务定义
事务是一组数据库操作的集合,是不可分割的最小工作单元,这一组SQL要么全部执行成功提交,要么全部失败回滚 ,不存在中间状态。
典型业务场景:
- 银行转账:A扣钱、B加钱,任意一步失败,两边数据都恢复原样
- 新增员工:主表emp插入员工、子表emp_expr批量插入工作经历,报错则两张表都不存数据
- 下单流程:创建订单、扣减库存、生成物流单
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事务指令:
- 方法执行前:自动开启事务
- 方法正常执行完毕:自动
commit提交事务 - 方法抛出异常:自动
rollback回滚事务
2. 注解可以标注的位置(推荐优先级:方法 > 类 > 接口)
-
Service层方法(生产环境首选)
精准控制只有当前业务方法开启事务,灵活性最高。java@Service public class EmpServiceImpl implements EmpService{ @Override @Transactional // 仅save方法开启事务 public void save(Emp emp){ // 新增员工+批量新增经历 } } -
Service实现类上
当前类所有public方法都会开启事务,适合整个类所有操作都需要事务的场景。 -
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的方法,调用另一个带事务的方法时,子方法该复用外层事务、还是新建独立事务,由传播行为控制。
最常用两种场景
- REQUIRED(默认值,绝大多数业务使用)
- 逻辑:外层已有事务,子方法加入同一个事务;外层无事务,新建事务
- 案例:新增员工、保存经历共用一个事务,要么一起成功,要么一起回滚
- REQUIRES_NEW(独立事务,互不干扰)
- 逻辑:无论外层是否存在事务,子方法新建完全独立的事务,外层回滚不会影响子方法提交的数据
- 经典业务场景:操作日志永久保存
需求:新增员工无论成功/失败,操作日志必须入库,不能跟着员工事务回滚。
日志落地完整代码
- 日志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);
}
}
- 新增员工主业务,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执行、事务提交/回滚完整日志,快速定位事务失效问题。
六、总结与面试核心考点梳理
- 事务ACID四大特性:原子性是核心,一致性是最终目标,隔离性解决并发冲突,持久性保证数据落地。
- 原生事务三步 :begin开启、commit提交、rollback回滚,Spring
@Transactional自动封装这三步。 - @Transactional基础 :推荐标注在Service层public方法;默认仅RuntimeException回滚,建议统一配置
rollbackFor=Exception.class。 - 事务传播行为
- REQUIRED默认:多表联动新增、修改,共用一个事务;
- REQUIRES_NEW:日志、消息记录等需要独立持久化,不受外层事务回滚影响。
- 实战重点场景
- 多表CRUD必须加事务,防止数据不一致;
- 操作日志、异步记录等场景使用REQUIRES_NEW隔离事务;
- 批量插入使用MyBatis
<foreach>动态SQL简化代码; - 自增主键回填依靠
@Options(useGeneratedKeys = true)。
@Transactional只对public方法生效,private/protected方法标注无效;- 同类中方法自调用(A方法调用本类带事务的B方法)事务会失效,需通过注入自身代理对象调用;
- 传播行为REQUIRES_NEW会创建独立连接,频繁使用会消耗数据库连接池资源,按需使用。