oracle只读事务必须在事务第一条语句显式声明,仅对当前事务生效,且不支持jdbc setreadonly(true);其基于scn快照提供一致性读,需避免长时间持有以防ora-01555。

只读事务必须在事务开始前显式声明
Oracle 的 SET TRANSACTION READ ONLY 不是连接级开关,也不是会话级默认行为——它只对**当前事务生效**,且必须是该事务的第一条语句。一旦执行了任何 DML(哪怕只是 SELECT FOR UPDATE),再执行该语句就会报错 ORA-01453: SET TRANSACTION must be first statement of transaction。
常见错误现象:
- 在 Spring @Transactional 方法里先查数据、再调
setReadOnly(true),结果被忽略或抛异常 - 用 HikariCP 连接池配置
connection-init-sql=SET TRANSACTION READ ONLY,但因连接复用导致后续写事务也继承只读状态
正确做法是:在获取连接后、执行任何 SQL 前,立即执行 SET TRANSACTION READ ONLY NAME 'xxx'。例如:
Connection conn = dataSource.getConnection();
conn.createStatement().execute("SET TRANSACTION READ ONLY NAME 'report_txn'");
// 后续所有 SELECT 都基于同一快照
ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM sales WHERE dt = SYSDATE - 1");
JDBC 中 setReadOnly(true) 在 Oracle 上不生效
Java Connection.setReadOnly(true) 是 JDBC 标准接口,但 Oracle JDBC 驱动(ojdbc8+)**不会将其翻译为 SET TRANSACTION READ ONLY**,而是仅设置连接的只读标志位,对 Oracle 数据库内核无实际影响。这意味着:
- 事务仍可执行 INSERT/UPDATE/DELETE(只要权限允许)
- 两次相同查询可能看到不同结果(非一致性读)
- 无法获得只读事务的快照隔离保证
对比 OceanBase Oracle 模式(OBOracle):它确实将 setReadOnly(true) 映射为 SET SESSION TRANSACTION READ ONLY,但这是 OB 特有行为,**不能套用到原生 Oracle**。
所以别依赖 setReadOnly(true) 实现只读语义——直接发 SQL。
读写分离路由需配合应用层逻辑判断
JPA 或 MyBatis 本身不解析 SQL 语义,无法自动把 SET TRANSACTION READ ONLY 后的查询路由到只读库。真正可行的路由方式只有两种:
- 按方法命名约定:如
findReportData()→ 走只读数据源;updateOrderStatus()→ 走主库 - 用注解 + AOP:自定义
@ReadOnly注解,在切面中切换DataSource或设置事务属性
注意:即使你手动在只读事务中执行了 INSERT,Oracle 也会立刻报 ORA-12081: UPDATE not allowed on table,但此时路由已发生,写操作失败反而暴露了架构缺陷。因此路由决策必须前置,不能靠数据库报错兜底。
只读事务的快照生命周期与资源开销
Oracle 只读事务启动时会固定一个 SCN(系统变更号),后续所有查询都基于该 SCN 的数据版本。这意味着:
- 事务持续越久,undo 数据保留时间越长,可能引发
ORA-01555: snapshot too old - 长时间运行的报表查询若未及时
COMMIT,会阻塞其他会话的 undo 清理 - 不建议在连接池中长期持有只读事务连接——应“即用即开即关”
典型误用:在 Web 请求中开启只读事务,整个 HTTP 请求周期都不提交。正确做法是限定只读事务作用域,比如封装成 reportService.executeInReadOnly(() -> {...}),内部自动 COMMIT。











