基于你的 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、拆成两个 Servicejava
运行
@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 完整后台接口 |
十、终极最佳实践总结
- 事务加在 Service 层,不要加在 Controller、Repository
- 必须加
rollbackFor = Exception.class,避免受检异常不回滚 - 查询方法统一加
readOnly = true,优化性能 - 避免大事务,事务里只做核心写操作,查询、远程调用放外面
- 注意自调用失效,同类调用事务不生效
- 不要乱改传播行为和隔离级别,默认值能覆盖 95% 场景
- 捕获异常后记得手动回滚,不要吞掉异常导致事务不回滚