Spring boot  事务管理  ----17

 

基于你的 JobPortal 项目「公司 - 岗位」业务场景,覆盖事务原理、只读优化、回滚规则、传播行为、隔离级别、生产避坑、管理员实战功能,全部采用企业级最佳实践写法。

一、前置说明

Spring Boot 引入 spring-boot-starter-data-jpa 后,自动完成事务配置,无需额外依赖,直接使用 @Transactional 注解即可。
  • 底层基于 AOP 动态代理实现
  • 默认使用 DataSourceTransactionManager
  • 仅对 public 方法生效,同类自调用会失效(高频坑点)

二、基础事务:生产级标准用法(175、176 集)

CompanyService.java 核心业务层

java
 
运行
 
 
package com.example.jobportal.service;import com.example.jobportal.dto.CompanyDTO;
import com.example.jobportal.entity.Company;
import com.example.jobportal.entity.Job;
import com.example.jobportal.exception.BusinessException;
import com.example.jobportal.repository.CompanyRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;@Service
@RequiredArgsConstructor
public class CompanyService {private final CompanyRepository companyRepository;/*** 新增公司 + 批量新增岗位* 核心特性:事务原子性,要么全部成功,要么全部回滚* rollbackFor = Exception.class:所有异常都回滚(生产必加)*/@Transactional(rollbackFor = Exception.class)public Company createCompanyWithJobs(CompanyDTO dto) {// 1. 保存公司Company company = new Company();company.setName(dto.getName());company.setAddress(dto.getAddress());Company savedCompany = companyRepository.save(company);// 2. 批量保存岗位for (String jobTitle : dto.getJobTitles()) {Job job = new Job();job.setJobTitle(jobTitle);job.setCompany(savedCompany);savedCompany.getJobs().add(job);}// 3. 模拟异常:测试事务回滚if (dto.getName().contains("测试异常")) {throw new BusinessException("触发异常,事务回滚,公司和岗位都不会存入数据库");}return savedCompany;}/*** 只读事务优化(对应177集)* readOnly = true:告诉框架这是纯查询,不做增删改* 优化点:关闭脏检查、优化连接、提升查询性能* 注意:只读事务里不能执行写操作,否则报错*/@Transactional(readOnly = true)public List<Company> getAllCompanies() {return companyRepository.findAll();}/*** 根据ID查询单个公司*/@Transactional(readOnly = true)public Company getCompanyById(Long id) {return companyRepository.findById(id).orElseThrow(() -> new BusinessException("公司不存在"));}/*** 更新公司信息*/@Transactional(rollbackFor = Exception.class)public Company updateCompany(Long id, CompanyDTO dto) {Company company = getCompanyById(id);company.setName(dto.getName());company.setAddress(dto.getAddress());return companyRepository.save(company);}/*** 删除公司* 级联删除所有关联岗位*/@Transactional(rollbackFor = Exception.class)public void deleteCompany(Long id) {companyRepository.deleteById(id);}
}

 

 

配套 DTO

java
 
运行
 
 
package com.example.jobportal.dto;import lombok.Data;
import java.util.List;@Data
public class CompanyDTO {private String name;private String address;private List<String> jobTitles;
}

 

 

三、@Modifying 与事务(对应 178 集)

JPQL 批量更新 / 删除必须同时加 @Modifying@Transactional,否则会报错。

CompanyRepository.java 新增批量更新

java
 
运行
 
 
package com.example.jobportal.repository;import com.example.jobportal.entity.Company;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;@Repository
public interface CompanyRepository extends JpaRepository<Company, Long> {/*** 批量更新公司地址* 1. @Modifying:声明这是写操作,不是查询* 2. @Transactional:批量操作必须在事务内执行*/@Modifying@Transactional(rollbackFor = Exception.class)@Query("UPDATE Company c SET c.address = :address WHERE c.id IN :ids")int batchUpdateAddress(@Param("ids") List<Long> ids, @Param("address") String address);
}

 

 
最佳实践:批量操作建议在 Service 层加事务,Repository 只负责 SQL,职责更清晰。

四、事务回滚规则(对应 179 集)

核心规则大白话

表格
 
异常类型默认是否回滚说明
RuntimeException 及其子类 ✅ 是 空指针、数组越界、自定义业务异常都属于这一类
Error 及其子类 ✅ 是 系统级错误,如 OOM
Exception 下的 Checked 异常(如 IOException) ❌ 否 受检异常默认不回滚,是最大的坑

生产最佳实践

所有 @Transactional 都加上 rollbackFor = Exception.class,确保任何异常都回滚,避免踩坑。

错误 vs 正确写法对比

java
 
运行
 
 
// ❌ 坑点写法:遇到 Checked 异常不会回滚
@Transactional
public void testCheckedException() throws Exception {// 业务操作throw new Exception("受检异常,默认不回滚!");
}// ✅ 正确写法:所有异常都回滚
@Transactional(rollbackFor = Exception.class)
public void testCheckedExceptionSafe() throws Exception {// 业务操作throw new Exception("所有异常都会回滚");
}

 

 

五、事务传播行为(对应 180 集)

决定「方法 A 调用方法 B 时,事务该怎么处理」,7 种传播行为,重点掌握前 3 种。

演示代码:两个 Service 演示传播行为

1. 主业务 Service

java
 
运行
 
 
package com.example.jobportal.service;import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
@RequiredArgsConstructor
public class TransactionDemoService {private final SubTransactionService subService;/*** REQUIRED(默认值):有事务就加入,没有就新建* 子方法异常,主方法一起回滚*/@Transactional(rollbackFor = Exception.class)public void requiredDemo() {// 主方法操作System.out.println("主方法执行");// 调用子方法,共用同一个事务
        subService.requiredSub();}/*** REQUIRES_NEW:总是新建独立事务* 子方法异常不影响主事务,主方法异常不影响子事务* 适合:日志记录、操作记录等必须独立成功的场景*/@Transactional(rollbackFor = Exception.class)public void requiresNewDemo() {System.out.println("主事务执行");try {subService.requiresNewSub(); // 子方法独立事务} catch (Exception e) {System.out.println("子方法异常,主事务不受影响");}}/*** NESTED:嵌套事务,保存点机制* 子方法回滚只回滚到保存点,不影响主事务* 主事务回滚,子事务一起回滚*/@Transactional(rollbackFor = Exception.class)public void nestedDemo() {System.out.println("主事务执行");try {subService.nestedSub();} catch (Exception e) {System.out.println("子方法回滚,主事务继续");}}
}

 

2. 子事务 Service

java
 
运行
 
 
package com.example.jobportal.service;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;@Service
public class SubTransactionService {// 默认 REQUIRED@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)public void requiredSub() {System.out.println("子方法:加入主事务");}// REQUIRES_NEW:新建独立事务@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)public void requiresNewSub() {System.out.println("子方法:独立新事务");throw new RuntimeException("子事务异常");}// NESTED:嵌套事务@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)public void nestedSub() {System.out.println("子方法:嵌套事务");throw new RuntimeException("嵌套事务回滚");}
}

 

 

常用传播行为总结

表格
 
传播行为通俗解释适用场景
REQUIRED(默认) 有就加入,没有就开新的 绝大多数普通业务
REQUIRES_NEW 不管有没有,都开全新独立事务 操作日志、审计记录、必须独立成功的逻辑
NESTED 嵌套事务,子回滚不影响父 复杂业务分支处理
SUPPORTS 有事务就用,没有也没事 纯查询方法
NOT_SUPPORTED 挂起事务,非事务执行 不重要的查询,不想占用事务资源
MANDATORY 必须有事务,没有就报错 强制要求在事务内执行的方法
NEVER 必须没事务,有就报错 严禁在事务里执行的方法

六、事务隔离级别(对应 181 集)

控制「多个并发事务之间的数据可见性」,解决脏读、不可重复读、幻读问题。

4 种隔离级别对比

表格
 
隔离级别脏读不可重复读幻读性能MySQL 默认
READ_UNCOMMITTED ❌ 会发生 ❌ 会发生 ❌ 会发生 最高 ❌ 不用
READ_COMMITTED ✅ 避免 ❌ 会发生 ❌ 会发生 较高 Oracle 默认
REPEATABLE_READ ✅ 避免 ✅ 避免 ❌ 基本避免 一般 ✅ MySQL 默认
SERIALIZABLE ✅ 避免 ✅ 避免 ✅ 避免 最低 几乎不用

代码指定隔离级别

java
 
运行
 
 
/*** 手动指定隔离级别为读已提交* 一般不用改,用数据库默认即可*/
@Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_COMMITTED
)
public void testIsolation() {// 业务逻辑
}

 

 
最佳实践:99% 的场景用数据库默认隔离级别即可,不要为了炫技乱改,改高了性能差,改低了数据乱。

七、生产常见坑点(对应 182 集)

坑 1:同类自调用,事务失效(最高频)

java
 
运行
 
 
@Service
public class DemoService {// 直接调用本类方法,不走代理,事务失效public void outer() {this.inner(); // ❌ 自调用,@Transactional 不生效
    }@Transactional(rollbackFor = Exception.class)public void inner() {// 写操作
    }
}

 

 
✅ 解决方法:注入自己、用 AopContext、拆成两个 Service
java
 
运行
 
 
@Service
public class DemoService {// 注入自己,通过代理对象调用
    @Autowiredprivate DemoService self;public void outer() {self.inner(); // ✅ 走代理,事务生效
    }
}

 

坑 2:方法不是 public

@Transactional 只对 public 方法生效,private/protected 直接失效。

坑 3:异常被 try-catch 吞掉

java
 
运行
 
 
@Transactional(rollbackFor = Exception.class)
public void test() {try {// 业务操作,抛异常} catch (Exception e) {// ❌ 异常被吞了,事务感知不到,不会回滚
        e.printStackTrace();}
}

 

 
✅ 解决:捕获后手动回滚,或者重新抛出
java
 
运行
 
 
catch (Exception e) {// 手动触发回滚
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

 

 

坑 4:大事务问题

一个事务里包了大量查询、循环操作、远程调用,事务时间过长,占用连接,并发高了直接拖垮数据库。
 
✅ 原则:事务里只做必要的数据库写操作,查询、远程调用放到事务外。

八、管理员公司管理接口(183~186 集 实战)

AdminCompanyController.java

java
 
运行
 
 
package com.example.jobportal.controller;import com.example.jobportal.common.Result;
import com.example.jobportal.dto.CompanyDTO;
import com.example.jobportal.entity.Company;
import com.example.jobportal.service.CompanyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;import java.util.List;@RestController
@RequestMapping("/api/admin/companies")
@RequiredArgsConstructor
@Tag(name = "管理员-公司管理", description = "后台公司增删改查接口")
public class AdminCompanyController {private final CompanyService companyService;@GetMapping@Operation(summary = "查询所有公司")public Result<List<Company>> list() {return Result.success(companyService.getAllCompanies());}@GetMapping("/{id}")@Operation(summary = "查询公司详情")public Result<Company> detail(@PathVariable Long id) {return Result.success(companyService.getCompanyById(id));}@PostMapping@Operation(summary = "新增公司(连带岗位)")public Result<Company> create(@RequestBody CompanyDTO dto) {return Result.success(companyService.createCompanyWithJobs(dto));}@PutMapping("/{id}")@Operation(summary = "更新公司信息")public Result<Company> update(@PathVariable Long id, @RequestBody CompanyDTO dto) {return Result.success(companyService.updateCompany(id, dto));}@DeleteMapping("/{id}")@Operation(summary = "删除公司")public Result<Void> delete(@PathVariable Long id) {companyService.deleteCompany(id);return Result.success(null);}
}

 

 
配合之前的 Security 配置,该接口路径 /api/admin/** 需要 ADMIN 角色才能访问,实现权限 + 事务的完整后台功能。

九、课程对应表

表格
 
集数核心内容对应代码
175 @Transactional 底层原理 前置说明 + AOP 代理原理
176 生产级后端事务实战 CompanyService 基础事务方法
177 readOnly 只读事务优化 getAllCompanies 等查询方法
178 @Modifying 必须性 Repository 批量更新方法
179 回滚规则:Checked vs Runtime 回滚规则对比 + rollbackFor 最佳实践
180 事务传播行为详解 传播行为演示 Service
181 隔离级别选型 隔离级别对比 + 代码示例
182 生产常见坑点避坑 四大坑点 + 解决方法
183~186 管理员公司 / 雇主管理功能 AdminCompanyController 完整后台接口

十、终极最佳实践总结

  1. 事务加在 Service 层,不要加在 Controller、Repository
  2. 必须加 rollbackFor = Exception.class,避免受检异常不回滚
  3. 查询方法统一加 readOnly = true,优化性能
  4. 避免大事务,事务里只做核心写操作,查询、远程调用放外面
  5. 注意自调用失效,同类调用事务不生效
  6. 不要乱改传播行为和隔离级别,默认值能覆盖 95% 场景
  7. 捕获异常后记得手动回滚,不要吞掉异常导致事务不回滚