首页 / 资讯中心 / 文章详情

Oracle 19c JDBC连接实战:驱动选型、URL配置与连接池调优

Oracle 19c JDBC连接实战:驱动选型、URL配置与连接池调优 ★ FEATURED ARTICLE
去年年底帮朋友看一个老项目升级数据库从 11g 迁移到 19c应用层用的还是几年前封装的 JDBC 工具类。结果第一轮联调测试环境直接抛了一堆异常从ORA-12514到Unsupported major.minor version再到ORA-28040一晚上全碰了一遍。JDBC 连接 Oracle 19c 这件事表面上看是个老掉牙的基础操作但这几年 Oracle 的版本节奏变化很快驱动、URL、权限模型都跟着变了如果还拿十年前的习惯去写踩坑几乎是必然的。这篇文章就把我在多个项目里连接 Oracle 19c 时沉淀下来的完整方案和踩坑记录整理出来。包括驱动怎么选、URL 怎么写最稳、连接池参数怎么调、常见的 ORA 报错怎么排查每一块我都会给出可以直接抄作业的配置和代码顺便解释一下背后的原因。适合刚接手 Oracle 19c 项目的 Java 开发也适合那些从 11g/12c 升级过来的老项目团队做参考。1. 连接前的三个关键认知动手写代码之前我强烈建议先把三件事搞清楚。这三件事决定了你后面能不能顺利连上以及项目上线后会不会出幺蛾子。1.1 驱动选型ojdbc8 还是 ojdbc11很多人下意识觉得“数据库是 19c那驱动也拿 19.x 的 ojdbc 就好了”这个方向没错但你得再往下分一层Oracle 官方从 19c 开始对驱动 jar 的命名做了调整不再像以前那样一个ojdbc14.jar走天下而是按 JDK 兼容性拆成了多个版本。我列一下实际使用中最常见的两个驱动 jar兼容 JDK说明ojdbc8.jarJDK 8绝大多数 Java 8 老项目首选JDBC 4.2 规范ojdbc11.jarJDK 11对应 21c 及以后版本但连接 19c 数据库完全没问题JDK 17 项目建议用它如果你的项目跑在 JDK 8 上就用ojdbc8。如果是 JDK 11 或 17优先ojdbc11。这里有个易错点ojdbc11这个命名容易让人误以为“只支持 JDK 11”实际上它支持 JDK 11 及以上包括 17、21所以 JDK 17 的项目不要回头去找什么ojdbc17没有那个东西。还有一个更老的ojdbc7.jar只兼容 JDK 7除非你是那种实在没法升级的老系统否则直接忽略。Oracle 19c 对应的驱动版本号一般以19.x开头比如19.3.0.0、19.14.0.0这点在 Maven 坐标里能看到。1.2 连接 URLSID 和 Service Name 别再搞混JDBC 连 Oracle 的 URL 有两种主流写法# 旧式 SID 写法18c 之前常见 jdbc:oracle:thin:192.168.10.20:1521:orcl # 新式 Service Name 写法官方推荐 jdbc:oracle:thin://192.168.10.20:1521/ORCLPDB1注意SID写法是host:port:sid一个冒号而Service Name写法是//host:port/service_name两条斜杠加一个斜杠。很多从 11g 迁过来的项目代码里还是旧写法连到 19c 上就报ORA-12505。根本原因是 19c 默认安装的是容器数据库架构CDB/PDB业务账号通常建在 PDB 里而你用 SID 去连SID 对应的是 CDB 的实例名PDB 并不是一个独立的 SID。所以我的习惯是19c 一律用 Service Name 写法。如果你不确定 PDB 的 service name登录数据库执行SELECT value FROM v$parameter WHERE name service_names;或者直接问 DBA 要连接字符串。你自己本地写代码联调时也可以用简单写法。1.3 账号密码和权限模型19c 的默认安全策略比 11g 严了不少。你拿到一个业务账号先确认它建在哪个容器里。如果应用要连ORCLPDB1账号必须是在这个 PDB 里创建的或者至少有访问权限。用 CDB 的公共账号跨 PDB 去连有时候能连上但查询业务表时各种ORA-00942排查起来很折腾不如一开始就理清楚。另外19c 默认对SYSTEM密码过期策略、用户锁定策略都更严格。如果应用账号连续失败几次被锁了代码里只会看到ORA-28000 the account is locked这不是连接代码的问题是账号策略的问题。2. 最小可用的连接代码与依赖配置理论说再多不如先跑通一个最简单的连接。这一节给你一套完整的最小代码从 Maven 依赖到 JDBC 连接你直接复制就能跑。2.1 Maven 依赖坐标如果你用 Maven 管理项目在pom.xml里加上dependency groupIdcom.oracle.database.jdbc/groupId artifactIdojdbc8/artifactId version19.14.0.0/version /dependency如果项目走的是 Spring Boot 2.x 或者旧一点的结构这个坐标完全够用。JDK 17 的项目把ojdbc8换成ojdbc11就行dependency groupIdcom.oracle.database.jdbc/groupId artifactIdojdbc11/artifactId version21.5.0.0/version /dependency有个细节如果你没配公司私服直接访问 Maven 中央仓库下载这些坐标没问题Oracle 官方的驱动坐标从 19c 开始已经同步推送到中央仓库了不用再手动去 Oracle 官网下载 jar 放进本地 lib省了很多事。不过如果团队里有老项目还是用 lib 方式管理 jar 包那手动下载时注意选择ojdbc8.jar而不是那些名字带-g的调试版带-g的包包含额外调试信息体积更大生产环境没必要用。2.2 写一个最简单的连接测试类直接看代码package demo; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class Oracle19cConnectDemo { // 用 Service Name不要用 SID 写法 private static final String URL jdbc:oracle:thin://192.168.10.20:1521/ORCLPDB1; private static final String USER app_user; private static final String PASSWORD your_password; public static void main(String[] args) { // 这行其实可省因为 DriverManager 会自动加载 META-INF 里的驱动 // 但保留让你更明确地知道用的是哪个驱动 try { Class.forName(oracle.jdbc.driver.OracleDriver); } catch (ClassNotFoundException e) { System.err.println(找不到驱动类请确认 ojdbc jar 是否在 classpath 中); e.printStackTrace(); return; } try (Connection conn DriverManager.getConnection(URL, USER, PASSWORD); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT 连接成功 AS INFO, 1 AS V FROM dual)) { if (rs.next()) { System.out.println(rs.getString(INFO)); System.out.println(数据库产品版本: conn.getMetaData().getDatabaseProductVersion()); System.out.println(驱动版本: conn.getMetaData().getDriverVersion()); } } catch (Exception e) { e.printStackTrace(); } } }dual是 Oracle 特有的单行单列表用来做这种不带真实表名的简单查询特别方便。这段代码能跑通说明你的驱动、URL、账号三要素都没问题。2.3 为什么推荐 try-with-resources上面代码里用的是 Java 7 之后的try-with-resources写法Connection、Statement、ResultSet都会自动关闭。这比老代码里的finally里手动conn.close()更安全。我之前接手过一个老项目他们的封装工具类里close()写得乱七八糟有的地方resultSet.close()之后又去connection.close()异常路径下连接一直不释放最后连接池被打满。如果你还在写手动的try-catch-finally关资源我真心建议趁这次迁移改掉。尤其是 Oracle连接是很贵的资源释放不及时DB 端会积累一堆INACTIVE会话DBA 查过来第一眼就怀疑是应用连池没配好。3. 连接池配置与参数调优实战JDBC 直连只适合做测试和工具类真实项目必须走连接池。目前 Java 生态里我用得最多的是 HikariCP 和 Druid这里结合 Oracle 19c 把关键参数拆开讲。3.1 HikariCP 连接 Oracle 19c 的推荐配置如果你用的是 Spring Boot 2.x 及以上默认连接池就是 HikariCP。在application.yml里可以这样调spring: datasource: url: jdbc:oracle:thin://192.168.10.20:1521/ORCLPDB1 username: app_user password: your_password driver-class-name: oracle.jdbc.OracleDriver hikari: minimum-idle: 5 maximum-pool-size: 15 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 connection-test-query: SELECT 1 FROM DUAL如果是纯 Java 项目手动创建 HikariDataSource代码类似HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:oracle:thin://192.168.10.20:1521/ORCLPDB1); config.setUsername(app_user); config.setPassword(your_password); config.setDriverClassName(oracle.jdbc.OracleDriver); config.setMaximumPoolSize(15); config.setMinimumIdle(5); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); config.setConnectionTestQuery(SELECT 1 FROM DUAL); HikariDataSource dataSource new HikariDataSource(config);这里有几个参数值得你细品。max-lifetime默认值是 1800000 毫秒30分钟但 Oracle 的会话可能被数据库侧的 profile 限制为更短的 idle 时间如果你的idle-timeout比数据库的空闲超时还大连接池认为连接还活着实际上已经被数据库杀了这就埋下了一个“用的时候突然报连接关闭”的隐患。所以我个人的经验是连接池的max-lifetime要小于数据库会话空闲回收时间通常 30 分钟问题不大但如果 DBA 那边静态参数改过你就得同步调。3.2 连接池参数背后的逻辑为什么maximum-pool-size不能拍脑袋随便填Oracle 的进程/会话资源是有限度的。一个 19c 实例默认processes参数可能只支持几百个进程。如果每个微服务实例都配maximum-pool-size: 100三个服务一启动直接把数据库进程数占满其他业务全部卡死。常见的容量估算方式是这样的最大活跃请求数 服务实例数 × 单实例最大并发请求数 数据库承受能力 数据库后台进程 每个连接一个会话进程 DBA/运维预留进程你不需要把这个公式背下来核心思想是连接池大小 (业务并发峰值 × 平均每个请求持有连接的时间) / 数据库可用会话数。大部分中小型系统单实例 10-20 就够了。配 5 个太低会导致请求排队配 100 个错觉是“性能好”实际上数据库和网络层都背着隐形的连接开销。connection-timeout这个参数也容易让新手踩坑。它是指从连接池借一个连接的超时时间不是 TCP 建连超时。如果数据库真的挂了你指望 HikariCP 在 30 秒内报错但实际上 TCP 层可能已经在排队了所以连接池层面通常还会配合一个oracle.net.CONNECT_TIMEOUT的 URL 属性来限制网络建连超时。这个后面在 URL 扩展参数里讲。3.3 Druid 连接池的 Oracle 专项配置国内很多老项目用的是 Druid。Druid 连 Oracle 时除了基础配置还推荐开testWhileIdle和timeBetweenEvictionRunsMillisspring: datasource: druid: driver-class-name: oracle.jdbc.OracleDriver url: jdbc:oracle:thin://192.168.10.20:1521/ORCLPDB1 username: app_user password: your_password initial-size: 5 min-idle: 5 max-active: 20 validation-query: SELECT 1 FROM DUAL test-while-idle: true time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000Druid 的test-while-idle开启后会定期检测空闲连接但检测也是有代价的如果池子小time-between-eviction-runs-millis设成 60 秒足够。别把time-between-eviction-runs-millis设到没谱的 10 秒那样数据库会被一堆探活 SQL 骚扰尤其是SELECT 1 FROM DUAL在高峰期高频执行DBA 看 AWR 报告会疯掉。我遇到过最典型的一个案例某系统从 11g 迁到 19c 后Druid 连接池每隔几秒就报一次GetConnectionTimeoutException排查到最后发现是连接在池子里被数据库侧提前回收了而 Druid 的min-evictable-idle-time-millis设得比数据库的空闲限制还长探活 SQL 又只在申请连接时才执行导致池子以为连接没事实际已经断了。解决办法就是把探活频率提高或者缩短min-evictable-idle-time-millis。4. 核心环节实现从 URL 扩展到批量操作前面还只是把“连上”跑通实际项目里你会发现连上只是开始。这一节说说我在 Oracle 19c 上遇到的最有代表性的几个核心环节URL 的扩展参数、批量写入优化、以及分页查询的典型写法。这几个点对系统体验的影响往往比连接本身还大。4.1 URL 扩展参数一分钟定位连接超时很多项目的 JDBC URL 就是裸的jdbc:oracle:thin://host:port/service一旦数据库负载高或者网络抖动应用层可能出现长达几十秒的卡顿因为 TCP 默认超时时间很长。Oracle JDBC 驱动支持在 URL 后面拼接特定参数jdbc:oracle:thin://192.168.10.20:1521/ORCLPDB1?oracle.net.CONNECT_TIMEOUT5000oracle.jdbc.ReadTimeout30000参数说明参数作用建议值oracle.net.CONNECT_TIMEOUTTCP 建连超时毫秒5000-10000oracle.jdbc.ReadTimeout读取 Socket 超时毫秒30000-60000oracle.jdbc.defaultRowPrefetch预取行数默认 10分页查询可以调大这个思路和 HikariCP 的connectionTimeout不冲突前者控制的是“从连接池借连接”后者控制的是“数据库 TCP 建连”。两个都配上才稳妥。有朋友问过我oracle.jdbc.ReadTimeout能不能设很大避免大查询被中途掐断我劝你换个思路。如果 SQL 本身要跑几分钟就算 socket 不超时应用线程也挂死在那里连接池会被占满。更合理的做法是去优化 SQL 执行计划让单次查询控制在秒级ReadTimeout 只是兜底不是救命稻草。4.2 批量写入优化调整 BatchSizeOracle 的 JDBC 驱动和 MySQL 的rewriteBatchedStatements行为不太一样MySQL 只要在 URL 上加个参数就能把批量 SQL 重写成多值插入Oracle 没有这个开关。但 Oracle 也有自己的批量优化参数。举个例子假设你要向一张订单表插入一万条数据普通的写法是循环preparedStatement.executeUpdate()性能惨不忍睹。改成addBatch()后如果不做任何参数调整默认每次网络往返处理一批批大小由内部参数控制。Oracle JDBC 支持在连接属性里设置Properties props new Properties(); props.put(user, app_user); props.put(password, your_password); // 连接级批量阈值默认 10 props.put(oracle.jdbc.defaultBatchValue, 100); Connection conn DriverManager.getConnection(url, props); PreparedStatement ps conn.prepareStatement(INSERT INTO T_ORDER(ID, AMOUNT, CREATE_TIME) VALUES (?, ?, ?)); for (int i 0; i 10000; i) { ps.setLong(1, i); ps.setBigDecimal(2, new BigDecimal(100.50)); ps.setTimestamp(3, new Timestamp(System.currentTimeMillis())); ps.addBatch(); if (i % 100 0) { ps.executeBatch(); ps.clearBatch(); } } ps.executeBatch();这里的oracle.jdbc.defaultBatchValue设置为 100意思是驱动攒够 100 条后再走一次网络批量发送。你可以对比测试相比默认配置一万条数据插入时间通常能缩短一半以上。需要提醒的是addBatch()的 burst 频率不是越高越好。JDBC 批处理的花费主要在解析 SQL、绑定参数、网络传输、数据库执行。如果 batch 太大单次批量执行的时间太长事务要等很久才能提交锁的范围也会变大。我用下来的经验是 50 到 200 比较合适小表 100大表 50具体还是结合行长度和网络延迟来调。4.3 分页查询ROWNUM 和 FETCH FIRST 的取舍说到 JDBC 操作 Oracle分页是绕不开的。老项目里最常见的写法是基于ROWNUM的三层嵌套SELECT * FROM ( SELECT TMP.*, ROWNUM RN FROM ( SELECT ID, NAME FROM T_USER ORDER BY ID ) TMP WHERE ROWNUM ? ) WHERE RN ?这种写法在很多系统里跑得好好的但也有个问题随着页码越翻越深ROWNUM要扫描并丢弃的记录越多。Oracle 12c 之后引入了行限制子句19c 自然是支持的可以写得非常简洁SELECT ID, NAME FROM T_USER ORDER BY ID OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;看上去很美但我要泼一盆冷水这个语法在底层实现上和ROWNUM并没有本质区别如果表本身没有合适索引深度翻页照样慢。真正治本的方法是做“键集分页”也就是记住上一页最后一条记录的排序键然后WHERE ID ?加FETCH NEXT 10 ROWS ONLY。如果你的项目是给后台管理系统用的数据量在百万以内用OFFSET FETCH完全没问题代码可读性好太多。如果是千万级以上的大表建议别偷懒老老实实用排序键 游标式分页这也是 JDBC 连接 Oracle 时最常见的性能优化方向之一。5. 连接池与 SQL 层的常见坑点排查实际运行系统时报错往往不是连接建立那一下而是用着用着突然出问题。所以专门写一节排查记录方便你以后对照自查。5.1 数据库端连接数暴涨怎么处理典型场景应用连的是 PDB连接池大小没变但数据库的会话数一直在涨甚至把processes打满。首先看是不是连接池泄露。Druid 和 HikariCP 都提供监控接口HikariCP 可以通过 health check 注册指标Druid 有自带的监控页面。排查思路是先查数据库的 v$session按 machine 和 username 分组看是不是某一台应用服务器的连接数异常。再到应用侧看连接池当时的 active 数量是否匹配。如果 DB 侧多出来的连接全部是INACTIVE多半是应用侧没有正常归还连接或者连接池的idleTimeout太长导致连接一直不释放。顺手看last_call_et如果某些 session 有个很大的值说明可能有慢 SQL 卡住把连接长期占用导致池子被迫新建更多连接。如果你用 HikariCP可以在application.yml里开启leak-detection-threshold这个参数用来检测连接泄露spring: datasource: hikari: leak-detection-threshold: 30000超过 30 秒未归还的连接会被打印出来非常好用。5.2 Oracle 监听无法启动与 JDBC 报错的关系有时候 JDBC 报IO Error: The Network Adapter could not establish the connection根本原因是 Oracle 监听服务挂了。很多从 11g 迁移上来的运维团队会遇到甲方环境不熟、监听日志路径不对、端口占用哪些问题。我只说一个和 JDBC 相关的点监听服务和 19c 的 PDB 注册关系。19c 安装后如果 PDB 是在监听启动之后才创建的监听可能没有自动注册这个 PDB 的 service name。这时候你在应用侧用 PDB 的 service name 去连报错往往是ORA-12514但用 SID 去连又可能通。排查方式很简单用lsnrctl status看监听暴露了哪些 service如果 PDB 的 service name 不在列表里就手动执行alter system register强制注册。这个操作 DBA 一般都会但如果你自己开发环境遇到了这行命令能救你一次。还有一个高频坑19c 默认使用动态端口还是 1521看你装的时候怎么选。如果选的是非标准端口JDBC URL 里的端口写错是必然的。我的建议是开发环境的端口统一写成 1521理由不是 1521 有多好而是团队成员多少一个“我明明用的是对的主机为什么连不上”的排查成本。5.3 时区问题导致时间差 8 小时Oracle 19c 的TIMESTAMP WITH TIME ZONE类型和 JDBC 驱动之间有时区处理的细节。我碰到过一次特别隐蔽的坑通过 JDBC 向TIMESTAMP列插入java.time.LocalDateTime查出来时间一致但换到TIMESTAMP WITH TIME ZONE列查出来总是差 8 小时。原因在于驱动默认使用 JVM 的时区去解释参数的时区。如果应用服务器时区是 Asia/Shanghai数据库的 dbtimezone 设成了 UTC那一进一出时差就出来了。解决办法有两个方向简单粗暴统一所有环境的 JVM 时区、数据库时区再加上jdbc:oracle:thin://...的oracle.jdbc.timezoneAsRegion参数oracle.jdbc.timezoneAsRegionfalse规范写法所有时间字段在应用里都用OffsetDateTime或ZonedDateTime不要一边用LocalDateTime一边期望数据库帮你换算时区。如果你的系统只在国内跑而且历史表全是DATE或TIMESTAMP不带时区那 90% 的场景可以忽略这个问题。但如果你接了跨境业务或者需要和不同时区的系统对接这个坑一定要提前知道。5.4 JDK 模块化与 ojdbc 的兼容最后说一个 Java 17 用户特别容易踩的坑。JDK 9 之后推出了模块化系统旧版的 ojdbc8/ojdbc11 在 Java 17 上运行启动时可能出现java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter这是因为 JDK 高版本移除了 Java EE 相关的模块。解决方式有两种。一种是最省事的换用新版驱动比如 Oracle 19.15.0.0 及之后的 ojdbc11里面已经适配了 Java 17 的模块限制。另一种是如果你还用老驱动必须在 JVM 启动参数里补上旧的 JAXB 依赖 jar。我个人强烈建议走第一条路不要在 2024 年了还跟 JVM 模块较劲。顺带一提如果你在 JDK 17 下用 Spring Boot 3.x那么ojdbc11的兼容性是经过 Spring Boot 官方认可的直接配没问题。Spring Boot 2.x 用的还是 javax 命名空间驱动是ojdbc8还是ojdbc11都能跑但更推荐ojdbc8因为ojdbc8本身在 Java 8 和 Java 11 上都稳。6. 常见报错速查表这一节是纯干货以后连不上时直接翻。报错信息可能原因解决方案ORA-12514: TNS:listener does not currently know of service用了 Service Name 但监听器没注册这个服务或服务名写错检查 PDB 的 service name执行alter system register注册ORA-12505: TNS:listener does not currently know of SID用了 SID 但连接的库是容器库PDB 不算 SID改用jdbc:oracle:thin://host:port/service_nameORA-28040: No matching authentication protocol客户端驱动太老19c 数据库的认证协议不兼容升级驱动至少用 19c 或 21c 的 ojdbcORA-01017: invalid username/password; logon denied用户名或密码错误或账号在错误的 PDB 中核对账号名确认在目标 PDB 里创建ORA-28000: the account is locked账号连续试错被锁DBA 执行ALTER USER xxx ACCOUNT UNLOCKListener refused the connection with the following error: ORA-12560监听服务没起来或协议配置错误启动监听检查sqlnet.oraUnsupported major.minor version 52.0JDK 版本低于驱动要求升级 JDK 或换低版本驱动java.sql.SQLException: Io 异常: Got minus one from a read call数据库连接被强制关闭或网络异常检查数据库会话超时、防火墙、socket timeoutORA-00942: table or view does not exist账号没有该表的权限或连接的 PDB 不对授权GRANT SELECT ON xxx TO user或者确认连接到正确的 PDB这张表里的内容都是我实际遇到过、帮别人排查过的不是从文档里抄的。特别提醒那条ORA-12514它出现的频率在 19c 环境里远高于旧版因为 PDB 架构把“实例”和“服务”拆开了很多老 DBA 第一次接触也懵。7. 最后的实操经验分享写到这里我把自己这几年的体会再沉淀几句。第一不要把 JDBC 连接 Oracle 19c 当成一个“能连上就完事”的任务。连接成功只是起点真正影响项目下线的往往是连接池参数、时区、批量提交、网络超时这些细节。我见过太多项目“开发环境跑得好好一上生产就报连接池爆满”一查都是参数没调或者连接没归还导致的。第二排查问题时要有顺序意识。遇到连接异常先确认驱动和 JDK 版本匹配再确认 URL 用的是 Service Name再确认账号建在正确的 PDB最后才考虑应用代码和连接池。按照这个顺序来大多数情况下能在十分钟内定位问题。如果一上来就翻代码找 SQL只会浪费更多时间。第三也是最实用的一点在正式联调前先把最小的 JDBC 连接测试代码跑通再用到框架里。这个过程看起来多一步实际能省很多事。因为你用框架接的时候报错信息会被 Spring、MyBatis、Druid 层层包装核心原因容易被藏起来。直接写一个main方法去连反而最干净。我每次到一个新环境第一件事就是拿这段测试代码跑一遍通了再接业务这个习惯帮我少踩了无数坑。
阅读完成 · 觉得有帮助?
咨询建站