恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Spring事务陷阱:@Transactional用错竟导致数据不一致?
首页
资讯中心
/
Spring事务陷阱:@Transactional用错竟导致数据不一致?
Spring事务陷阱:@Transactional用错竟导致数据不一致?
发布时间:2026/10/3 12:52:17
引言在日常开发里几乎没有哪个业务离得开事务。转账要保证扣款和加款要么同时成功、要么同时失败下单要保证扣库存和生成订单步调一致批量导入要保证中途出错能整体回滚。如果不用事务一旦某一步抛异常数据就会处于一半成功一半失败的脏状态排查起来极其痛苦。Spring 对事务做了非常优雅的封装让我们不用再写一堆commit/rollback的样板代码一个Transactional注解就能搞定绝大多数场景。但注解用起来容易用对却没那么简单——很多同学遇到的事务不生效明明抛了异常却没回滚往往就栽在几个经典坑上。这篇文章我会带你系统过一遍 Spring 事务的核心概念、注解用法、传播行为、回滚规则以及那些最容易踩的事务失效场景每个知识点都配有可直接运行的代码示例。一、事务到底是什么事务Transaction是数据库操作的一个最小执行单元它由一组操作组成这组操作要么全部成功要么全部失败具有ACID四大特性原子性Atomicity事务内的所有操作是一个整体要么全做要么全不做。一致性Consistency事务执行前后数据都处于合法状态例如转账前后总金额不变。隔离性Isolation多个事务并发执行时互不干扰彼此看不到中间状态。持久性Durability事务一旦提交对数据的修改就是永久的即使宕机也不会丢失。举个最经典的例子——转账sql-- 步骤1从 A 账户扣款 100 元 UPDATE account SET balance balance - 100 WHERE id 1; -- 步骤2给 B 账户加款 100 元 UPDATE account SET balance balance 100 WHERE id 2;如果步骤 1 成功、步骤 2 因为某种原因失败了那么 A 的钱凭空消失、B 却没收到这是绝对不能接受的。事务的作用就是保证这两步同生共死。二、Spring 事务的两种管理方式Spring 提供了两种事务管理方式1. 编程式事务手动写代码控制事务的开启、提交和回滚比较繁琐现在基本不用。java// 编程式事务老式写法不推荐 TransactionStatus status transactionManager.getTransaction(def); try { // 业务逻辑 transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; }2. 声明式事务通过Transactional注解或 XML 配置来声明某个方法需要事务Spring 底层基于 AOP 帮你自动完成开启事务 → 执行业务 → 提交/回滚这一整套动作。这也是我们平时用的主流方式下面重点讲它。三、声明式事务 Transactional 快速上手3.1 准备环境先引入 SpringBoot JDBC 相关依赖以 SpringBoot 2.x/3.x 为例xmldependencies !-- web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JDBC 事务 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok可选简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies配置文件application.ymlyamlspring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver建一张account表sqlCREATE TABLE account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, balance DECIMAL(10, 2) NOT NULL ); INSERT INTO account (name, balance) VALUES (张三, 1000.00), (李四, 1000.00);3.2 最简事务示例下面实现一个转账业务张三给李四转 100 元。javaService public class AccountService { Autowired private JdbcTemplate jdbcTemplate; Transactional public void transfer(Long fromId, Long toId, BigDecimal amount) { // 1. 扣款 jdbcTemplate.update(UPDATE account SET balance balance - ? WHERE id ?, amount, fromId); // 2. 加款 jdbcTemplate.update(UPDATE account SET balance balance ? WHERE id ?, amount, toId); } }只要在transfer方法上标注Transactional这个方法内的两次update就被纳入同一个事务。任何一步抛出运行时异常RuntimeException整个事务都会回滚只有两步都成功事务才会提交。测试javaSpringBootTest class AccountServiceTest { Autowired private AccountService accountService; Test void testTransfer() { accountService.transfer(1L, 2L, new BigDecimal(100.00)); // 转账成功张三 900李四 1100 } }3.3 手动模拟异常回滚为了看清楚回滚效果我们故意在两步中间抛一个异常javaTransactional public void transferWithError(Long fromId, Long toId, BigDecimal amount) { jdbcTemplate.update(UPDATE account SET balance balance - ? WHERE id ?, amount, fromId); // 模拟业务异常 int i 1 / 0; // 抛出 ArithmeticException运行时异常 jdbcTemplate.update(UPDATE account SET balance balance ? WHERE id ?, amount, toId); }执行后你会发现张三的钱没有被扣因为扣款那一步也被回滚了。这就是事务的原子性。四、事务传播行为 propagation事务传播行为Propagation解决的是当前方法已经有事务时新开的事务该怎么处理这个问题。比如 A 方法有事务A 内部又调用了 B 方法B 也有事务那么 B 是加入 A 的事务还是自己新开一个Transactional提供了 7 种传播行为其中最常用的是前两种传播行为含义REQUIRED默认有事务就加入没有就新建。最常用REQUIRES_NEW无论如何都新建一个独立事务并挂起当前事务SUPPORTS有事务就加入没有就以非事务方式执行NOT_SUPPORTED以非事务方式执行有事务则挂起MANDATORY必须在事务中执行否则抛异常NEVER必须在非事务中执行否则抛异常NESTED如果存在事务则以嵌套事务方式执行4.1 REQUIRED默认—— 加入已有事务javaService public class OrderService { Autowired private LogService logService; Transactional // 默认 REQUIRED public void createOrder() { // 下单核心逻辑 saveOrder(); // 记录日志LogService.log 也是 REQUIRED会加入当前事务 logService.log(创建订单); } } Service public class LogService { Transactional public void log(String msg) { // 这里如果抛异常会把 createOrder 整个事务一起回滚 saveLog(msg); } }特点REQUIRED下被调用方法抛异常会导致整个事务回滚因为大家共用同一个事务。4.2 REQUIRES_NEW —— 独立新事务有些场景我们不想让日志这种旁路操作影响主业务。比如主业务成功了但记日志失败我们不希望因为日志失败而让下单也回滚。这时就可以用REQUIRES_NEWjavaService public class LogService { Transactional(propagation Propagation.REQUIRES_NEW) public void log(String msg) { // 独立事务这里出错不会影响外层事务 saveLog(msg); } }特点REQUIRES_NEW会挂起外层事务自己开一个新事务。即使内层抛异常只要外层捕获了它外层事务照样能提交。javaService public class OrderService { Autowired private LogService logService; Transactional public void createOrder() { saveOrder(); try { logService.log(创建订单); // 独立事务失败也不影响 } catch (Exception e) { // 忽略日志失败 } } }常见坑REQUIRES_NEW必须通过另一个 Bean的方法调用才能生效跨方法、跨 Bean。如果在同一个类里直接this.log()AOP 代理不生效传播行为也失效。这一点和下一节事务失效密切相关。五、回滚规则 rollbackForTransactional默认只在抛出 RuntimeException 和 Error时回滚抛出受检异常Checked Exception时不会回滚。这是很多人踩坑的地方。5.1 默认回滚规则演示javaTransactional public void transfer() throws Exception { jdbcTemplate.update(UPDATE account SET balance balance - 100 WHERE id 1); // 抛出一个受检异常Checked Exception throw new Exception(业务失败); // 默认【不会】回滚 }上面代码里Exception是受检异常默认情况下 Spring不会回滚事务扣款那步会被提交。这显然不符合预期。5.2 用 rollbackFor 显式指定javaTransactional(rollbackFor Exception.class) // 任何 Exception 都回滚 public void transfer() throws Exception { jdbcTemplate.update(UPDATE account SET balance balance - 100 WHERE id 1); throw new Exception(业务失败); // 现在会回滚 }rollbackFor Exception.class表示遇到Exception及其子类都回滚这是实际项目里非常推荐的标准写法。5.3 noRollbackFor —— 指定某些异常不回滚反过来如果某个运行时异常你希望不要回滚可以用noRollbackForjavaTransactional(rollbackFor Exception.class, noRollbackFor BusinessException.class) public void transfer() { jdbcTemplate.update(UPDATE account SET balance balance - 100 WHERE id 1); throw new BusinessException(业务提示不触发回滚); }最佳实践建议写业务代码时养成显式写Transactional(rollbackFor Exception.class)的习惯避免受检异常漏回滚。六、事务失效的常见场景与原因这一节是重头戏。Transactional是基于 AOP 代理实现的一旦绕过了代理或违反了它的生效条件注解就会静默失效这是生产环境最常见的坑。场景 1方法非 publicSpring 事务要求被代理的方法必须是public。如果是private/protected/ 包级私有代理无法拦截事务不生效。javaTransactional private void doSave() { // ❌ 不生效方法不是 public jdbcTemplate.update(INSERT INTO ...); }场景 2同类内部调用this 调用事务是通过动态代理实现的只有经过代理对象的调用才会被拦截。如果在同一个类内部用this.xxx()调用另一个带Transactional的方法走的是原始对象而不是代理事务不会生效。javaService public class UserService { public void register() { // ❌ 内部直接调用绕过代理事务失效 this.doSave(); } Transactional public void doSave() { jdbcTemplate.update(INSERT INTO ...); } }解决方案把被调用的方法抽到另一个 Bean或通过Autowired注入自身用代理或用AopContext.currentProxy()调用。javaService public class UserService { Autowired private UserService self; // 注入代理对象 public void register() { self.doSave(); // ✅ 走代理事务生效 } Transactional public void doSave() { jdbcTemplate.update(INSERT INTO ...); } }场景 3异常被 catch 吞掉事务回滚的触发点是异常被抛到代理层。如果你在方法内部try-catch把异常捕获了、没再抛出去Spring 感知不到异常自然不会回滚。javaTransactional public void transfer() { try { jdbcTemplate.update(UPDATE account SET balance balance - 100 WHERE id 1); int i 1 / 0; // 抛出异常 } catch (Exception e) { // ❌ 异常被吞掉事务提交了扣款生效了 } }解决方案要么不捕获、让异常往上抛要么在 catch 里手动回滚javaTransactional public void transfer() { try { jdbcTemplate.update(UPDATE account SET balance balance - 100 WHERE id 1); int i 1 / 0; } catch (Exception e) { // ✅ 手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); throw new RuntimeException(e); } }场景 4抛的是受检异常且未配置 rollbackFor如上一节所说默认只回滚 RuntimeException 和 Error。抛Exception受检异常却不配rollbackFor事务不会回滚。javaTransactional // ❌ 没配 rollbackFor public void transfer() throws Exception { jdbcTemplate.update(UPDATE account SET balance balance - 100 WHERE id 1); throw new Exception(受检异常默认不回滚); }场景 5数据库存储引擎不支持事务MySQL 的MyISAM引擎不支持事务。如果表用的是 MyISAM即使代码完全正确Transactional也不会生效数据照常写入。sql-- 查看表引擎 SHOW TABLE STATUS LIKE account; -- 应使用 InnoDB CREATE TABLE account (...) ENGINE InnoDB;场景 6没有被 Spring 管理Transactional依赖 Spring 容器生成的代理 Bean。如果你的类是通过new UserService()手动创建的而不是Service注册到容器里的代理不存在事务自然失效。java// ❌ 手动 new不在容器中没有代理事务失效 UserService service new UserService(); service.transfer();场景 7多线程调用事务和线程是绑定的事务信息存在ThreadLocal里。如果在事务方法里新开一个线程去执行数据库操作新线程拿不到当前事务也就不会纳入同一个事务。javaTransactional public void transfer() { jdbcTemplate.update(UPDATE account SET balance balance - 100 WHERE id 1); // ❌ 新线程里的事务与当前事务无关 new Thread(() - { jdbcTemplate.update(UPDATE account SET balance balance 100 WHERE id 2); }).start(); }场景 8事务管理器配置错误如果项目里存在多个数据源或多个事务管理器而没有指定Transactional使用哪一个可能出现事务没作用到目标数据源的情况。多数据源场景下需要显式指定javaTransactional(transactionManager orderTransactionManager) public void transfer() { // ... }场景 9方法上注解被类注解覆盖注意顺序注解属性遵循就近覆盖原则。类上有Transactional(readOnly true)方法上又写了Transactional方法级注解会覆盖类级注解但要注意不要以为类上的配置一定会合并生效。javaService Transactional(readOnly true) // 类级别默认只读 public class UserService { Transactional(readOnly false) // 方法级覆盖可写 public void save() { // 写操作 } }总结这篇文章我们系统地梳理了 Spring 事务的核心知识事务的 ACID 特性是理解一切事务行为的基础。声明式事务Transactional借助 AOP 帮我们自动完成事务的开启、提交与回滚是日常开发的首选。传播行为中REQUIRED默认加入已有事务和REQUIRES_NEW独立新事务最常用二者在一个方法调用另一个带事务的方法时行为差异很大务必分清。回滚规则默认只对 RuntimeException 和 Error 生效受检异常需要显式配置rollbackFor Exception.class才会回滚。事务失效的根源几乎都集中在绕过了 AOP 代理或异常没被正确抛到代理层这两类具体包括方法非 public、同类 this 调用、异常被吞、受检异常未配 rollbackFor、引擎不支持事务MyISAM、类未被 Spring 管理、多线程调用、多数据源未指定事务管理器等。一句话记住想让事务生效就必须保证调用走的是 Spring 代理对象 异常能正常抛到代理层 用的是支持事务的存储引擎。