Java 事务失效 10 大场景 + 新手避坑!从 public 修饰符到内部调用全解析
·
前言:从 2 个新手灵魂提问说起
今天开发时踩了个大坑:在ServiceImpl方法上加了@Transactional,数据插入失败却没回滚?带着疑问查了半天,发现居然是方法没加public!顺着这个问题,我又延伸出新困惑:内部提炼的方法要不要加public?同一类里方法自调用为啥事务失效?
结合查资料和实战验证,我把 Java(Spring)事务失效的核心场景、解决方案和关键疑问整理成这篇指南,新手看完直接避坑,再也不用为事务 “失灵” 头秃~
一、先解答 2 个核心疑问(新手最关心)
疑问 1:事务方法必须加 public 吗?
必须加!否则事务 100% 失效原因:Spring 事务默认基于「AOP 动态代理」实现,不管是 JDK 动态代理还是 CGLIB 代理,都只能拦截public修饰的方法。非 public 方法(private、protected、default)无法被代理拦截,@Transactional注解相当于 “摆设”。
疑问 2:内部提炼的方法需要加 public 吗?
分场景判断,不用一刀切:
- 仅内部自调用(不对外暴露):不用加 public,用
private/protected即可(符合封装原则)。事务是否生效,只看「发起调用的外层方法」是否为public+@Transactional;@Service public class UserServiceImpl { // 外层方法:public+@Transactional(事务生效) @Override @Transactional public void saveUser(User user) { this.validateUser(user); // 内部private方法,无需public userMapper.insert(user); } // 内部校验方法:仅内部调用,private足够 private void validateUser(User user) { if (user.getName() == null) { throw new IllegalArgumentException("用户名不能为空"); } } } - 需被其他类调用 / 单独触发事务:必须加
public!若需要独立事务,还要单独加@Transactional(其他类调用时会走 Spring 代理,事务才生效)。
二、事务失效 10 大场景(带原因 + 示例 + 解决方案)
场景 1:方法非 public 修饰(最基础坑)
- 原因:AOP 动态代理无法拦截非 public 方法,
@Transactional失效; - 错误示例:
private void saveData() { ... }加@Transactional; - 解决方案:事务方法统一加
public修饰。
场景 2:同一类方法内部自调用(高频坑)
- 原因:A 方法(无事务)调用本类 B 方法(有
@Transactional)时,用this调用的是 “原始对象”,未经过 Spring 代理,事务无法触发; - 错误示例:
@Service public class UserServiceImpl { public void methodA() { this.methodB(); // 自调用,事务失效 } @Transactional public void methodB() { userMapper.insert(user); } } - 解决方案(3 种,优先选前 2 种):
- 拆分类(最推荐):把 B 方法抽到新的 Service 类,通过
@Autowired注入调用;// 新Service类 @Service public class UserHelperService { @Transactional public void methodB(User user) { userMapper.insert(user); } } // 原Service类 @Service public class UserServiceImpl { @Autowired private UserHelperService helperService; public void methodA(User user) { helperService.methodB(user); // 代理调用,事务生效 } } - 从 Spring 上下文拿代理对象:实现
ApplicationContextAware接口,获取自身代理对象;@Service public class UserServiceImpl implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext ctx) { this.applicationContext = ctx; } public void methodA(User user) { // 获取代理对象调用 UserServiceImpl proxy = applicationContext.getBean(UserServiceImpl.class); proxy.methodB(user); } @Transactional public void methodB(User user) { userMapper.insert(user); } } - 自注入(懒人写法):
@Autowired注入自身代理对象(谨慎使用,可读性稍差)。
- 拆分类(最推荐):把 B 方法抽到新的 Service 类,通过
场景 3:事务传播机制配置错误
- 原因:传播机制决定事务嵌套规则,以下配置会主动关闭事务;
- 失效传播机制:
Propagation.NOT_SUPPORTED:挂起当前事务,以非事务方式执行;Propagation.NEVER:存在事务则抛异常,拒绝执行;Propagation.SUPPORTS:无事务时以非事务执行(不主动开事务);
- 解决方案:默认用
Propagation.REQUIRED(存在事务则加入,无则新建,无需手动配置)。
场景 4:异常被捕获未抛出(异常 “被吞”)
- 原因:Spring 事务默认仅在 “未处理的 RuntimeException/Error” 时回滚;若用
try-catch捕获异常且未重新抛出,事务无法感知; - 错误示例:
@Transactional public void saveUser(User user) { try { userMapper.insert(user); int i = 1 / 0; // 模拟异常 } catch (Exception e) { e.printStackTrace(); // 仅打印,未抛出 } } - 解决方案:捕获后重新抛出异常(
throw new RuntimeException(e)),或用TransactionStatus.setRollbackOnly()手动回滚。
场景 5:捕获了非指定异常类型
- 原因:默认不处理 “受检异常”(如 IOException、SQLException);若
@Transactional指定rollbackFor,实际异常不在范围内则不回滚; - 错误示例:
@Transactional(rollbackFor = RuntimeException.class) public void saveUser() throws IOException { throw new IOException("文件读取失败"); // 事务不回滚 } - 解决方案:
rollbackFor = Exception.class(兜底所有异常,新手推荐)。
场景 6:未配置事务管理器
- 原因:Spring 事务需要绑定
PlatformTransactionManager(如DataSourceTransactionManager),未配置则注解 “空转”; - 必备配置:
@Configuration public class TransactionConfig { @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } } - 注意:SpringBoot 项目若引入
spring-boot-starter-jdbc,会自动配置,无需手动写。
场景 7:类未被 Spring 管理(非 Bean)
- 原因:只有加
@Service/@Component等注解、被 Spring 扫描注册为 Bean 的类,@Transactional才会被 AOP 代理; - 错误示例:
UserService service = new UserServiceImpl(); service.saveUser(user);(手动 new 对象,非 Spring 管理); - 解决方案:通过
@Autowired注入对象,禁止手动new。
场景 8:数据库存储引擎不支持事务
- 原因:事务最终依赖数据库支持,MySQL 的
MyISAM引擎不支持事务(已废弃),InnoDB才支持; - 检查方式:执行
show table status like '表名',查看Engine字段是否为InnoDB; - 解决方案:建表时指定引擎
ENGINE=InnoDB。
场景 9:多线程调用(事务上下文不共享)
- 原因:Spring 事务上下文存在
ThreadLocal中,仅对当前线程生效;子线程的数据库操作不纳入主线程事务; - 错误示例:
@Transactional public void saveUser(User user) { new Thread(() -> userMapper.insert(user)).start(); // 子线程操作不回滚 } - 解决方案:避免多线程中执行事务操作,若必须用,需用分布式事务(如 Seata)。
场景 10:事务方法被 final/static 修饰
- 原因:final 方法无法被 AOP 动态代理重写,static 方法不属于对象实例(代理无法拦截);
- 错误示例:
public final void saveUser(...) { ... }加@Transactional; - 解决方案:事务方法避免用 final/static 修饰。
三、新手避坑 3 大核心原则(记牢少踩坑)
- 修饰符原则:事务方法必加
public,内部自调用方法用private; - 异常原则:要么不捕获异常,要么捕获后重新抛出,
rollbackFor = Exception.class兜底; - 代理原则:调用事务方法必须走 Spring 代理(
@Autowired注入 / 上下文获取),禁止this自调用、手动new对象。
结语
Spring 事务失效的本质就两类:事务未被正确代理(非 public、自调用、非 Bean 等)、执行逻辑跳出事务控制(异常被吞、多线程、传播机制错误等)。
更多推荐
所有评论(0)