1. 为什么还在折腾老版本数据库1.1 存量系统的现实困境屠龙刀法这个系列写到第十四篇手里压着这个话题很久了。说句实在话做数据库相关的活儿干久了谁手里没有几个还跑在老旧版本上的系统可能是财务那边用了十年的MIS可能是医疗行业某个HIS模块也可能是制造业车间MES系统里那台只能跑老Oracle的工控机。这些系统业务逻辑复杂到没人敢动数据库版本却停在十几年前数据量大、字段怪、存储过程多得吓人但就是不能下线也没法轻易升级。我接过不少这种活儿新开发的数据平台要对接老系统需要把老库里的数据同步出来做分析或者老库要做数据迁移。这时候第一道槛就是驱动连接。你用新版数据库驱动去连老版本库十有八九会翻车。轻则报版本不兼容重则驱动加载直接Crash连ClassNotFound都能给你演一遍。更气人的是这些问题在本地环境往往复现不出来一到生产环境就出幺蛾子。写这一篇的初衷很简单就是把我用通用JDBC驱动连老版本数据库那套路子整理出来。没有高深的理论全是实打实的版本匹配经验、URL写法、驱动选型和踩坑记录希望能给同样在跟老库搏斗的同学省点时间。1.2 通用JDBC驱动到底解决什么问题先厘清一个概念所谓通用JDBC驱动并不是某个厂商出的万能驱动而是指在版本选择上采用向下兼容策略的那一类JDBC驱动包。常见的有MySQL官方那个mysql-connector-java的5.x系列Oracle的ojdbc14、ojdbc6这些老驱动以及PostgreSQL的老驱动。它们最核心的价值就是开发者只需要用标准JDBC API写一套连接代码不用关心底层数据库版本差异驱动负责把协议协商、版本握手、SQL执行这些脏活累活接走。为什么需要单独讨论老版本数据库因为JDBC驱动和数据库服务端之间是有一个版本协商过程的。新驱动可能默认使用新的协议、新的字符集编码方式或者新的认证插件而老数据库不认识这些新东西两边握不上手。反过来老驱动连新数据库也可能有问题但相对少一些。所以连接老库时用同时代或略新一点的驱动是铁律这就是通用JDBC驱动能发挥作用的原因。2. 驱动选型不同年代数据库的兼容地图2.1 驱动版本与数据库版本的匹配逻辑先说匹配逻辑。JDBC驱动对数据库版本的支持不是线性的越新越好而是有一个明确的兼容区间。拿MySQL来举例mysql-connector-java 5.1.x这个系列连接3.23、4.x、5.0、5.5、5.6、5.7基本都能干活甚至连早期8.0也可以连只是官方没保证。而到了8.0.20以后新版驱动com.mysql.cj.jdbc.Driver默认走新的认证协议并且把com.mysql.jdbc.Driver这个老类名给废弃掉了你再拿它去连MySQL 4.x那基本是痴人说梦。Oracle的情况类似。ojdbc14这个古董驱动对应的是Java 1.4和JDK 1.4时代可以连Oracle 8i、9i、10g。ojdbc6对应JDK 6主攻Oracle 10g、11g。但你要是把ojdbc8这种新驱动拿去连Oracle 9i它直接给你抛一个ORA-28040: No matching authentication protocol异常因为新版驱动默认使用新的密码校验机制老库根本不认。这种例子我遇到太多本质上就是驱动与服务端的版本握手协议没对齐。所以选驱动之前先确认三件事数据库是什么版本、服务端用的字符集、JDK版本是多少。这三者确定之后驱动版本的选择范围基本就锁死了。不要指望一个驱动包打通所有场景现实里不存在这种东西。通用的含义不是万能而是覆盖面足够宽且在已知版本区间内稳定。2.2 MySQL老库的驱动选择清单既然标题提的是通用JDBC我就把最常见的几个库的驱动选型整理一下。MySQL这边老版本5.1.x的mysql-connector-java是性价比最高的选择。Maven坐标用的是这个dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency连接老库的核心配置项如下驱动类名com.mysql.jdbc.Driver注意不是新的com.mysql.cj.jdbc.DriverURL格式jdbc:mysql://host:port/数据库名?useUnicodetruecharacterEncodinggbk关键参数useUnicode和characterEncoding必须显式指定否则老库的GBK数据读出来全是乱码MySQL 3.x、4.x的库常见字符集是latin1或者GBK新版驱动默认用UTF-8做字符集协商两边对不上就会出乱码。所以哪怕业务代码什么都不改连接串里这两个参数也不能省。2.3 Oracle老库的驱动选择与URL差异Oracle那边要分情况。Oracle 8i和9i时代标准的瘦驱动是jdbc:oracle:thin:host:1521:实例名驱动类名用oracle.jdbc.driver.OracleDriver。这里有个细节从Oracle 10g开始官方建议使用oracle.jdbc.OracleDriver但老类名oracle.jdbc.driver.OracleDriver还能用。如果你连的是Oracle 8i或者9i新版驱动基本没戏老老实实用ojdbc14。Maven引用一般是手动加SystemPath因为仓库里的ojdbc14并不是正规Maven源发布的。SQL Server 2000/2005这类老库微软官方的坑在于驱动类名一直变。早期用com.microsoft.jdbc.sqlserver.SQLServerDriver后来换成com.microsoft.sqlserver.jdbc.SQLServerDriver。中间有个版本还搞出了JTDS这个第三方驱动反而比官方驱动更稳定。连2000这种古董用JTDS反而顺手。数据库类型推荐老驱动驱动类名URL前缀MySQL 3.x~5.7mysql-connector-java 5.1.xcom.mysql.jdbc.Driverjdbc:mysql://Oracle 8i/9i/10gojdbc14 / ojdbc6oracle.jdbc.driver.OracleDriverjdbc:oracle:thin:SQL Server 2000/2005JTDS 1.2.xnet.sourceforge.jtds.jdbc.Driverjdbc:jtds:sqlserver://PostgreSQL 8.xpostgresql 42.2.x 以下org.postgresql.Driverjdbc:postgresql://这张表是我多次实操后整理的常用组合。真要遇到极端版本比如MySQL 3.235.1.49连不上时退到5.0.x版本驱动反而能通这种逆向操作就得靠现场多试几版。建议手头常备一个驱动包小仓库把5.1.49、5.0.8、ojdbc14、ojdbc6、JTDS 1.2.8这几个经典版本都留着用到的时候三分钟就能切换。3. 实操手写一个兼容老库的连接层3.1 项目依赖与基础设施准备说再多理论都不如直接上手。这一节我按完整实操步骤来写参考一个我实际做过的数据同步小项目要把一个老版本MySQL库里的订单数据读出来写入另一个新库。整个流程你可以直接照抄改改就行。第一步先建一个普通的Maven项目Java 8即可因为老驱动对JDK版本很敏感JDK 11以上跑这些古董驱动多多少少有些模块化的问题。核心依赖就一个dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency如果你连的是Oracle而不是MySQL把依赖换成ojdbc14并指定本地jar包路径代码逻辑几乎不用改JDBC接口统一的好处就在这。第二步是准备一个配置文件把连接串、用户名、密码从代码里抽出来。这里有个经验值老库的密码强度往往不高但字符集配置一定不能马虎。我在配置文件里放的是带时区参数的连接串因为老库的timestamp字段经常不存时区读出来会有偏差。source.drivercom.mysql.jdbc.Driver source.urljdbc:mysql://192.168.1.108:3306/orderdb?useUnicodetruecharacterEncodinggbkuseSSLfalse source.usernamereadonly source.passwordorderdb2024注意这里的useSSLfalse老库基本都没有配置SSL证书新版驱动默认会尝试SSL握手加上这个参数能省掉一堆无谓的告警和性能损耗。3.2 核心连接代码接下来写一个最精简的连接工具类原则是能用就好不引入Spring、不用连接池框架因为老库环境经常是内网隔离的额外依赖越少越不容易出问题。也方便你在命令行断开网络时单独调试连接串。import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class LegacyDbConnector { public static Connection getConnection(String driver, String url, String username, String password) { try { Class.forName(driver); Properties props new Properties(); props.setProperty(user, username); props.setProperty(password, password); // 老库连接的关键避免驱动默认属性与库端配置冲突 props.setProperty(useUnicode, true); props.setProperty(characterEncoding, gbk); props.setProperty(useSSL, false); return DriverManager.getConnection(url, props); } catch (ClassNotFoundException | SQLException e) { throw new RuntimeException(数据库连接失败: e.getMessage(), e); } } }这里有几个点需要单独强调一下。第一Class.forName(driver)这一步在现代JDBC驱动里可以省略因为SPI机制会自动加载。但老驱动没有SPI文件所以这行不能省。第二连接参数最好通过Properties传入而不是拼在URL里这样做的好处是密码等敏感信息不容易被日志打印出来。第三老库的timeout设置务必显式加上我用过不少老库连接建立极慢若不设置TRIGGER超时整个线程卡死在等待上。3.3 增删改查与元数据读取连接拿到了剩下就是增删改查。用标准JDBC的Statement和ResultSet就够不需要ORM。这部分我给一个示例读取老库中的订单表public static void queryOrder(Connection conn) throws SQLException { String sql SELECT order_id, order_amt, create_time FROM t_order WHERE order_status 1; try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { String orderId rs.getString(order_id); BigDecimal amt rs.getBigDecimal(order_amt); Timestamp createTime rs.getTimestamp(create_time); System.out.println(orderId | amt | createTime); } } }这段代码看似普通但有个非常容易踩的雷老库的create_time字段类型可能是datetime而驱动映射到的Timestamp是有时区概念的。如果你在连接时没加serverTimezone参数5.1.x驱动默认按JVM时区解析结果就是数据凭空多了8小时或者少了8小时。这个我在外汇交易系统的历史数据迁移里吃过不小的亏后来老老实实在连接串里显式写上时区参数才解决。往老库里写数据则是另一套逻辑。老库对保留字、特殊字符的处理和新库差异很大最典型的就是表名字段名大小写问题。MySQL在Linux下区分大小写Windows下不区分同步数据时SQL必须改成库里的实际大小写否则一执行就报table doesnt exist。另外老库的字段类型偏旧比如int(11)这种显示宽度Java端对应Integer即可但要注意金额字段如果是float类型精度会丢建议在SQL里先做一次CAST。读取元数据这块也是高价值功能。很多老系统压根没有文档全靠从数据库反向推断表结构。用JDBC的DatabaseMetaData可以拿到表名、列名、列类型、主键信息DatabaseMetaData meta conn.getMetaData(); ResultSet tables meta.getTables(null, null, %, new String[]{TABLE}); while (tables.next()) { System.out.println(tables.getString(TABLE_NAME)); }这套API从JDBC 1.0就存在了老驱动都支持得很完整。我做个数据库字典导出工具的时候就是靠它把老库的表清单、字段清单全量拉出来再生成Markdown文档给业务方核对字段含义。实测下来效率很高比翻老代码靠谱多了很多老系统的代码早就找不全了反而是数据库结构一直完整地留在那里。4. 常见问题与排查技巧实录4.1 典型报错速查表这一节把我在现场遇到的报错整理成一个速查表方便读者对号入座。前面说到的ORA-28040: No matching authentication protocol是Oracle老库连接最常见的报错根本原因就是新驱动默认加密协议老库不支持。解决办法分两步要么换ojdbc6或ojdbc14驱动要么在数据库上折中调整参数允许老协议显然后者不合适生产环境所以核心还是换驱动。报错信息可能原因解决方向ClassNotFoundException: com.mysql.jdbc.Driver驱动jar没引入或引入错误坐标检查依赖换5.1.x并确认包路径Unknown system variable query_cache_size驱动太老库端参数不存在升级驱动到5.1.x以上版本Communications link failure网络不通或URL端口错误先telnet端口确认防火墙策略ORA-28040Oracle密码协议不兼容换ojdbc14/ojdbc6Public Key Retrieval is not allowedURL缺allowPublicKeyRetrieval参数新版驱动连接老库时添加该参数Illegal mix of collations字段集不一致检查库、表、连接三个层级的字符集第二个报错值得展开说。MySQL老库如果本身是从3.x迁数据上来的表结构里可能遗留一些老参数新驱动一连接的时候会去读系统变量读不到就抛异常。这种情况反而需要升级驱动版本跟前面的逻辑正好相反。4.2 字符集问题的本质字符集问题在连接老库时几乎必现值得专门讲透。老库在设计的时候字符集用latin1、GBK、GB2312都很常见根本不是现在默认的UTF-8。JDBC连接时有三层字符集需要对齐数据库端字符集、连接字符串指定的字符集、Java端JVM默认字符集。任何一层对不上读写就是乱码。我之前接的一个总账系统同步项目老库是GBK新库是UTF-8。连接串里配置了characterEncodinggbk之后读出来的中文一切正常。但往新库导入的时候我把String直接拼接成SQL结果入库的是乱码。问题出在Java编译器默认用UTF-8读源码而jdbc驱动按GBK发送两边不一致。解决方式是固定地通过new String(value.getBytes(GBK), UTF-8)转换或者干脆把所有环节统一成UTF-8在数据读取阶段就做转换绝不把乱码状态传递到下一层。一个实用的自查方法连上老库后执行show variables like character_set%把服务端、数据库、连接三个地方的值全部列出来然后对着连接串里的参数逐个核对。只要有一项显示latin1而其他是gbk或utf8就一定会有问题。4.3 一次真实问题的排查过程讲一个真实案例。上个月帮一个电商老系统做订单数据校验他们那边是MySQL 5.0的老库运维反馈用新版工具连不上程序报连接超时。我直接先用命令行mysql客户端连了一下发现能连上说明网络和端口没问题。然后我用JDBC去连报的却是Communications link failure。排查过程大概是这样的第一步抓jdbc连接URL发现写的是jdbc:mysql://IP:3306端口没问题。第二步Telnet测试IP的3306端口通了。既然网络通问题就锁定在协议协商环节。第三步用Wireshark抓包发现驱动发出握手包后服务器直接RST断开。进一步看细节客户端用的是caching_sha2_password认证而5.0的MySQL根本不认识这个插件。解决方案很简单把驱动从mysql-connector-java 8.0.33换回5.1.49连接串里的useSSL参数一并去掉。换完瞬间就通了。这个案例说明报连接超时不一定真的是网络问题很多时候是认证被服务端直接拒绝后客户端还在傻等。以后看到连接超时先确认一下驱动版本再判断网络。4.4 驱动包管理的几个实操心得最后分享几个我积累的驱动包管理经验。第一建议把所有兼容老库的驱动jar统一放到一个libs目录里用文件名的日期做区分比如mysql-connector-java-5.1.49.jar、ojdbc14-10.2.0.4.0.jar。因为你在多个项目里辗转之后很容易忘记哪个版本在哪个项目里验证过文档和脚本里记录的版本号必须和实际用的jar完全一致一个字符都不能差。第二把验证过的连接管理脚本也留存下来千行级的Java代码太长其实一个几十行的Shell脚本就够了。Java JDBC驱动支持从命令行直接跑写一个独立的小Main方法就可以做连接测试。脚本里把连接的URL、驱动类名、用户名、密码全输出成参数三分钟内就能验证一个库能不能连通大大降低联调等待时间。我在新接手一个老系统时绝不会先读业务代码而是先用这套脚本把所有相关老库全部打一遍电话确认哪些可用哪些不可用心里有底再动手。第三使用连接池时不要配置得过于激进。老库往往撑不住高并发的连接数连接池最大连接数一定要保守比如10个左右就够了。同时连接池的testOnBorrow要打开因为老库的连接空闲超时非常短。如果你用的是Druid把validationQuery配置成select 1这是所有老库都支持的探测SQL。我见过一个项目把连接池最大连接数配到50结果老库直接内存爆掉连SSH都进不去得不偿失。用通用JDBC驱动连接老版本数据库说难不难说简单也没那么简单。版本匹配、字符集、时区、连接参数每一个环节都是一块绊脚石。希望这篇整理能让你从一脸懵直接跳到心中有数见到老库不再慌。最后再多说一句碰到稀奇古怪的报错第一时间去查驱动版本大部分诡异问题的源头其实都在版本上。