根本原因是oracle存储过程异常被when others then静默吞掉,未透传至java层,导致spring无法感知错误而触发回滚;同时jndi数据源autocommit默认为true及存储过程中显式commit/rollback也会破坏事务边界。
spring事务在oracle存储过程抛出异常后不回滚,根本原因不是spring没起作用,而是异常根本没“传出来”——它被oracle pl/sql的异常处理机制吞掉了,spring压根不知道发生了错误。
Oracle存储过程里用了EXCEPTION WHEN OTHERS THEN ... 之后事务就失效了
这是最典型的陷阱。你在存储过程中写了类似这样的代码:
CREATE OR REPLACE PROCEDURE insert_user(p_name VARCHAR2) AS
BEGIN
INSERT INTO users(name) VALUES (p_name);
RAISE_APPLICATION_ERROR(-20001, '业务校验失败');
EXCEPTION
WHEN OTHERS THEN
NULL; -- 或者只写 DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
问题就出在 EXCEPTION WHEN OTHERS THEN 块里:只要没显式 RAISE 或 RAISE_APPLICATION_ERROR,异常就被静默吞掉。Spring JDBC调用完这个过程,返回的是“执行成功”,SQLException 根本没抛出,@Transactional 自然不会触发回滚。
- Oracle默认不把PL/SQL异常透传给JDBC客户端,除非你主动
RAISE或用RAISE_APPLICATION_ERROR重新抛出 -
WHEN OTHERS THEN NULL是“事务黑洞”,连日志都不打,Spring完全无感知 - 即使存储过程里写了
ROLLBACK,那也只是当前PL/SQL上下文的回滚,和Spring管理的外部事务无关
Spring调用存储过程时没配置throws SQLException
如果你用的是 JdbcTemplate 或 SimpleJdbcCall,但方法签名没声明抛出 SQLException,或者在调用处做了空 catch,那就等于主动切断了异常链:
public void callInsertUser(String name) {
try {
jdbcCall.execute(Collections.singletonMap("p_name", name));
} catch (DataAccessException e) { // 这里吞了所有底层SQLException
log.error("调用失败", e);
// 没有 re-throw,Spring事务代理收不到信号
}
}
-
DataAccessException是Spring封装的异常,但如果你在 service 方法里 catch 住它又不 re-throw,事务就断了 - 确保 service 层方法签名包含
throws Exception(或至少throws SQLException),且不自己捕获 - 如果必须捕获,结尾一定要
throw new RuntimeException(e)或手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
Oracle数据源开启了autoCommit=true(尤其JNDI场景)
你用的是JNDI数据源,而Oracle JDBC驱动默认 autoCommit=true。哪怕Spring开了事务,只要连接本身是自动提交模式,存储过程里的DML就会立即生效,不受Spring事务控制:
- 检查Tomcat
context.xml中的defaultAutoCommit="false"是否设置(不是所有版本都支持该属性) - 更可靠的做法:在Spring配置中显式禁用自动提交,例如用
HikariCP时设auto-commit=false;若坚持用JNDI,可在DataSource初始化后通过setAutoCommit(false)强制关闭(需包装为TransactionAwareDataSourceProxy) - 验证方式:在事务方法内打印
connection.getAutoCommit(),必须为false
存储过程里显式COMMIT或ROLLBACK破坏了Spring事务边界
Oracle存储过程内部如果写了 COMMIT 或 ROLLBACK,会强行结束当前事务,导致Spring无法统一控制:
- 哪怕你只在存储过程里写了一行
COMMIT;,后续所有DML(包括Spring事务里的其他操作)都会脱离事务管理 - 解决方案:存储过程必须设计为“无事务控制”——只做DML,不 commit/rollback,把事务权完全交给调用方(即Spring)
- 如果历史原因必须保留内部commit,那就不能把它放在Spring事务方法里,得单独拆出来、用
@Transactional(propagation = Propagation.NOT_SUPPORTED)隔离
真正卡住人的地方,往往不是Spring配置错了,而是Oracle侧的异常流没打通——它像一堵墙,把错误挡在数据库里面。你得先确认异常是否真的到达了Java层,再谈Spring回滚。否则所有@Transactional设置都是摆设。











