实现Oracle高可用数据源切换必须借助AbstractRoutingDataSource自定义路由,因为Spring Boot原生并不支持RAC或Data Guard的自动故障转移;要确保可靠切换,需要结合健康检查、线程绑定、驱动版本匹配以及事务隔离等多种机制。
Oracle高可用数据源切换必须使用 AbstractRoutingDataSource 自定义路由
Spring Boot 本身并未为 Oracle 高可用架构(例如 RAC、Data Guard 切换)提供自动故障转移功能。如果 spring.datasource.url 中写死了一个固定地址,一旦实例宕机,就会直接抛出 SQLException: IO Error: Connection reset。因此,必须自行实现路由逻辑,使连接能够在多个 Oracle 实例之间动态选择。

这里的关键点在于:不能仅仅依赖 JDBC URL 中添加 failover=true&loadBalance=true 参数。Oracle 官方驱动对 RAC 的透明应用故障转移(TAF)仅支持 OCI 连接,Thin Driver 无法触发 TAF 回调;而 Data Guard 的角色切换(primary ↔ standby)更需要应用层主动感知状态并执行重连。
- 首先,继承
AbstractRoutingDataSource,重写determineCurrentLookupKey()方法 - 使用
ThreadLocal绑定当前会话期望的数据源标识(例如"rac-node1"或"dg-standby"),而不是全局轮询 - 配合健康检查线程定期 ping 各个实例(执行
SELECT 1 FROM DUAL),将不可用的节点暂时从候选列表中移除 - 通过切面拦截
@Transactional方法,在事务开始前验证目标数据源是否存活,若不存活则抛出自定义异常或 fallback 到备用数据源
ojdbc8 驱动版本与连接参数必须与 Oracle 版本匹配
驱动版本或参数配置不当,经常会出现一种诡异现象:连接能成功,查询 DUAL 也没问题,但一旦执行涉及序列、LOB 或高级队列的 SQL,就会报 ORA-00904: invalid identifier 或 ORA-22922: nonexistent LOB value。
举个例子,在 Oracle 19c RAC 环境下,如果使用 ojdbc6,即使配置了 oracle.net.TNS_ADMIN 指向 tnsnames.ora,也无法解析 SCAN 地址中的负载均衡信息;而 ojdbc8(对应 JDK 8+)才能完整支持 12c 以后的新特性,包括 Application Continuity 和 Transparent Application Failover 的基础协议字段。
- Oracle 12.1–19c → 使用
com.oracle.database.jdbc:ojdbc8:21.10.0.0(Maven 中央库最新稳定版) - JDBC URL 必须显式启用 RAC 支持:
jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=node1)(PORT=1521))(ADDRESS=(PROTOCOL=TCP)(HOST=node2)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=orcl)(FAILOVER_MODE=(TYPE=SELECT)(METHOD=BASIC)(RETRIES=3)(DELAY=5))) - 禁用
oracle.jdbc.useFetchSizeWithLongColumn(默认 true),否则大字段查询可能导致内存溢出
事务方法内切换数据源会破坏 ACID,必须禁止
在同一个 @Transactional 方法中,如果先查询主库再写入备库,或者中间调用 DataSourceContextHolder.set("dg-standby"),会导致 Spring 的 DataSourceTransactionManager 获取到错误的物理连接——事务已经绑定初始数据源,后续切换只会让非事务操作“看似”走了新库,但实际上会被回滚或隔离失效。
真实场景中常见错误:用户服务调用订单服务,订单服务内部使用了 @DS("dg-standby") 注解,结果事务提交后,主库未更新、备库却写入了脏数据,最终 DG 同步失败且无法回退。
- 所有数据源切换注解(如
@DataSource或自定义@DS)必须标注在 非事务方法 上,或者确保该方法本身不参与外层事务(@Transactional(propagation = Propagation.NOT_SUPPORTED)) - 读写分离场景下,写操作只能走主库,且必须由独立事务方法承载;读操作可走备库,但需明确声明
@Transactional(readOnly = true)并配置专用数据源 - 如果业务强依赖跨库一致性(例如双写校验),应改用消息队列 + 最终一致性,不要在事务内硬切换
Druid 连接池的 validationQuery 对 Oracle 要设置为 SELECT 1 FROM DUAL
许多团队沿用 MySQL 的 SELECT 1,但在 Oracle 下 Druid 会静默忽略校验,导致连接池长期持有已经断开的连接,直到下次真正使用时才暴露 SQLException: Closed Connection,拖慢整个请求链路。
Druid 默认的 testWhileIdle + timeBetweenEvictionRunsMillis 机制依赖 validationQuery 执行结果来判断连接有效性,Oracle 没有 SELECT 1 语法,必须显式指定 DUAL 表。
- 在 application.yml 中务必配置:
spring.datasource.druid.validation-query: SELECT 1 FROM DUAL - 同时设置
spring.datasource.druid.test-on-borrow: false(避免每次取连接都查询一次,影响性能),依靠空闲检测兜底即可 - 如果使用了
dynamic-datasource-spring-boot-starter,它底层也基于 Druid,同样需要覆盖该配置,否则多数据源场景下某个库的连接池会持续泄漏
最容易忽略的是:RAC 节点间状态不同步时,健康检查可能误判。比如一个节点刚重启、SMON 尚未完成实例恢复,此时 SELECT 1 FROM DUAL 能通,但 INSERT INTO ... 仍会报 ORA-03113: end-of-file on communication channel。建议在生产环境将健康检查升级为执行轻量级业务语句(例如查询一个带索引的小表主键),而不是仅依赖 DUAL。
