基于你的 JobPortal 项目「公司 - 岗位」业务场景,覆盖事务原理、只读优化、回滚规则、传播行为、隔离级别、生产避坑、管理员实战功能,全部采用企业级最佳实践写法。
一、前置说明
Spring Boot 引入 spring-boot-starter-data-jpa 后,自动完成事务配置,无需额外依赖,直接使用 @Transactional 注解即可。
- 底层基于 AOP 动态代理实现
- 默认使用
DataSourceTransactionManager
- 仅对
public 方法生效,同类自调用会失效(高频坑点)
二、基础事务:生产级标准用法(175、176 集)
CompanyService.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
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 新增批量更新
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 正确写法对比
// ❌ 坑点写法:遇到 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
运行
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
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 |
✅ 避免 |
✅ 避免 |
✅ 避免 |
最低 |
几乎不用 |
代码指定隔离级别
/*** 手动指定隔离级别为读已提交* 一般不用改,用数据库默认即可*/
@Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_COMMITTED
)
public void testIsolation() {// 业务逻辑
}
最佳实践:99% 的场景用数据库默认隔离级别即可,不要为了炫技乱改,改高了性能差,改低了数据乱。
七、生产常见坑点(对应 182 集)
坑 1:同类自调用,事务失效(最高频)
@Service
public class DemoService {// 直接调用本类方法,不走代理,事务失效public void outer() {this.inner(); // ❌ 自调用,@Transactional 不生效
}@Transactional(rollbackFor = Exception.class)public void inner() {// 写操作
}
}
✅ 解决方法:注入自己、用 AopContext、拆成两个 Service
@Service
public class DemoService {// 注入自己,通过代理对象调用
@Autowiredprivate DemoService self;public void outer() {self.inner(); // ✅ 走代理,事务生效
}
}
坑 2:方法不是 public
@Transactional 只对 public 方法生效,private/protected 直接失效。
坑 3:异常被 try-catch 吞掉
@Transactional(rollbackFor = Exception.class)
public void test() {try {// 业务操作,抛异常} catch (Exception e) {// ❌ 异常被吞了,事务感知不到,不会回滚
e.printStackTrace();}
}
catch (Exception e) {// 手动触发回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
坑 4:大事务问题
一个事务里包了大量查询、循环操作、远程调用,事务时间过长,占用连接,并发高了直接拖垮数据库。
✅ 原则:事务里只做必要的数据库写操作,查询、远程调用放到事务外。
八、管理员公司管理接口(183~186 集 实战)
AdminCompanyController.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% 场景
- 捕获异常后记得手动回滚,不要吞掉异常导致事务不回滚