
做Java后端的朋友基本都绕不开MySQL。我见过太多新手第一次写连接代码对着几千行的控制台报错一脸茫然。翻来覆去无非就是几类驱动类找不到、连接被拒绝、SSL握手失败、认证插件不兼容。这些报错信息往往长得吓人但根因就那么几个排查顺序对了几分钟就能定位。这篇文章不打算讲高深理论就把Java连接MySQL的完整链路拆开从驱动配置讲到连接池从报错定位讲到事务边界最后落到Spring Boot MyBatis场景下的实用配置。适合刚上岗的Java开发读也适合长期用框架、没认真写过原生JDBC的同事回头补课。我自己在项目里接过不少连接相关的故障单最离谱的一次MySQL 8.0.21的实例、Connector/J 8.0.11的驱动到特定网络环境就报“Public Key Retrieval is not allowed”查了大半天才发现是驱动版本和认证插件之间的兼容关系。类似的坑网上零散答案很多但能把因果关系讲明白的文章很少所以我把实际经验整理成一条线你照着走完基本不会再被连接问题卡住。1. 驱动与连接串写对这一步后面省一半时间1.1 Connector/J 版本带来的历史包袱Java连MySQL官方驱动叫MySQL Connector/JMaven坐标是这样dependency groupIdcom.mysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency版本号是第一个坑。5.x的驱动类名是com.mysql.jdbc.Driver8.x之后迁移成了com.mysql.cj.jdbc.Driver。很多人项目里引入的JAR是8.x代码里却还在写老类名执行Class.forName时直接抛ClassNotFoundException。JDBC 4.0之后DriverManager可以通过SPI机制自动发现驱动其实不再强制写Class.forName。但为了兼容老代码、也让读代码的人知道用的是哪家数据库保留这行也无妨前提是类名必须写对。我见过最隐蔽的一种情况pom.xml里写的是mysql:mysql-connector-java看起来没问题但间接依赖把版本冲掉了最终加载了一份额外旧的5.x驱动连8.0的库时报java.sql.SQLException: Unsupported character encoding utf8mb4这类问题表面报错跟驱动版本毫无关系排查起来头大。建议在Maven里用dependencyManagement统一锁定版本或者直接在dependency中写死版本号不要放任传递依赖替你做决定。生产环境尤其如此多个中间件项目拼装时谁也不知道哪个库里偷偷带了一个老驱动。1.2 连接URL里的每个参数都要知道在干嘛基本连接URL长这样jdbc:mysql://127.0.0.1:3306/mydb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8主机、端口、库名就不赘述了。真正容易踩坑的是query参数下面这几个是高发区。useSSLMySQL服务端和客户端的默认行为在不同版本之间变过很多次。8.0.26之后服务端默认ssl_modeREQUIRED如果客户端连接时不启用SSL会直接提示要求SSL。而5.7老实例默认并不强制加密驱动这边默认又是useSSLtrue两端一碰就报SSL握手失败。拿到“mysql ssl连接错误”这类报错第一步不是改代码而是先确认服务端和JAR分别是什么版本别盲目复制网上的答案。serverTimezone老驱动在连接时会把时区信息发给服务端两边时区不对齐时会报一串乱码The server time zone value йʱ is unrecognized根因就是没设时区。直接给serverTimezoneAsia/Shanghai就能消掉。characterEncoding数据库建表用了utf8mb4连接串里也要对应设置characterEncodingutf8。JDBC驱动协议里一般写utf8不用纠结是不是少了个mb4。常见配合是useUnicodetruecharacterEncodingutf8少了任何一边中文往库里写就可能变问号。allowPublicKeyRetrieval这是MySQL 8.0引入caching_sha2_password认证插件后的典型问题。如果你的账号用的就是这个认证方式并且连接没走SSL通道服务端没法安全地把加密公钥发给客户端于是报出Public Key Retrieval is not allowed网上很多教程直接建议把这个参数设成true并关掉SSL确实能通但只适合本地开发。生产环境千万别这么干。正确的做法有三条配置受信证书走SSL、把账号认证插件改为mysql_native_password、或者配好完整的sha2证书体系。图省事裸连等于把密码当明文在网络上跑安全上得不偿失。提示不要在公网链路上用useSSLfalse。开发环境图方便没问题测试环境和生产环境请老老实实配SSL。从MySQL 8.0.26开始服务端默认强制SSL这方面根本绕不过去。2. 最小JDBC连接骨架从DriverManager到try-with-resources2.1 一个能跑的Connection示例不管后面用什么框架底层都是那套JDBC流程。最小可用代码长这样String url jdbc:mysql://127.0.0.1:3306/mydb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; String user root; String password your_password; try (Connection conn DriverManager.getConnection(url, user, password); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT id, name FROM user LIMIT 10)) { while (rs.next()) { System.out.println(rs.getInt(id) - rs.getString(name)); } }用try-with-resources的好处是无论执行成功还是抛异常Connection、Statement、ResultSet都会自动关闭。JDK 7之后都建议这么写不建议再用手动finally块既啰嗦又容易漏关。上面这段代码里驱动加载不写了因为SPI自动加载已经接管。如果你想明确加载放在连接之前也行Class.forName(com.mysql.cj.jdbc.Driver);2.2 资源关闭是纪律问题不是技术问题连接能通不算完能不能稳定复用才是关键。很多新手以为关闭连接就是写一行conn.close()的事真相是忘关连接不会立刻报错而是在某个不可预测的时间点集中爆发。MySQL服务端有一个wait_timeout参数默认8小时。连接池或手动创建的连接若长期不活动服务端会主动断开。客户端这边还没感知下次执行SQL时才会报Communications link failure. The last packet successfully received from the server was ...这就是典型的“半开连接”问题。客户端以为自己还握着连接服务端那边早把它清了。所以有两个基本纪律每次连接都必须按“创建-使用-关闭”的完整生命周期管理关闭放在finally或try-with-resources里。不用裸DriverManager.getConnection做高频数据库操作要交给连接池统一管理。手动管理连接还容易把连接泄漏带到生产环境。线程一多、异常一多漏掉一个close就多占一条连接MySQL的max_connections默认151条几分钟就能被打满后续所有请求都卡在获取连接上。这属于数据库最经典的故障模式之一。2.3 别把autoReconnect当救命稻草老版本的Connector/J提供了一个autoReconnecttrue参数连接断掉后驱动会自动重连。听着很美好实际在生产环境埋了不少雷。自动重连发生在驱动内部但中间层的Statement、事务状态很可能已经作废。比如你正在一个事务里执行两条SQL第一条成功、服务端超时断连驱动此时悄悄重建连接第二条SQL在一个全新连接上执行事务边界全乱。对金融、订单这类强一致性场景来说这是不能接受的。所以我的建议很简单不使用autoReconnect而是把“连接生命周期”交给连接池连接池负责测试连接有效性哪条断了就淘汰哪条应用从池子里拿到的永远是健康的连接。这个思路在第五章展开。3. 高频连接报错的完整排查链路3.1 “Connection refused”先查端口和监听地址别急着改代码报错长这样java.net.ConnectException: Connection refused (Connection refused)这个问题出现在TCP层。意思是你的Java进程确实发了一个网络包到目标地址和端口但对方没有进程在监听或者中间路由器直接回了拒绝。排查顺序我一般固定为目标MySQL端口能否telnet通。先看服务端是否在监听Linuxss -an | grep 3306Windowsnetstat -an | findstr 3306接下来查bind-address。MySQL默认可能只绑定了127.0.0.1这时从另一台机器连接必然拒绝。配置文件my.cnf或my.ini里改成bind-address 0.0.0.0或者干脆注释掉这行然后重启MySQL服务。最后才是防火墙。Linux的firewalld或iptables、Windows的入站规则都得放行3306端口。很多云服务器还在安全组层面卡了一道别只查本机防火墙。提示检查顺序从左到右按“服务是否监听 → 是否只绑回环地址 → 本机防火墙 → 云安全组”来。绝大多数Connection refused不是代码问题。3.2 用户密码正确却提示认证失败另一种常见报错java.sql.SQLException: Access denied for user rootlocalhost这种别慌不是连接层问题是认证失败。可能原因密码确实错了、账号只能从某个host登录而你从别的host连、服务端该用户用的认证插件与客户端不兼容、用户名里带了空格。mysql.user表里一个账号是userhost的组合rootlocalhost和root%是两个完全不同的账号。连接时如果没匹配上对应的host就会被当成匿名用户或不存在的用户拒绝。排查时可以往URL后面加?allowPublicKeyRetrievaltrue看报错是否变成公钥问题进而判断认证插件类型。3.3 SSL报错的几种脸谱以及“证书受害者”怎么自救这类报错出现的频率极高。我按症状拆成了三类方便你直接对照。第一类两端SSL策略不匹配。服务端要求SSL客户端却传了useSSLfalse或者反过来服务端没开SSL客户端默认要求SSL。解决思路确认两边的SSL模式统一成一致策略。第二类证书不可信。报错里出现PKIX path building failed或unable to find valid certification path说明客户端校验服务端证书时找不到可信CA。要么拿服务端证书导入客户端的信任库要么就把verifyServerCertificatefalse加上并接受不校验证书仅限内部环境。第三类网络中间设备干扰。某些网络环境下SSL握手会莫名失败报错提示“发送的响应无效”或“Connection reset”。这不是Java代码问题而是握手包在中间链路被破坏。先用命令行客户端mysql -h host -P port直接连一下看能否连上。命令行能连而JDBC不能就聚焦客户端配置命令行也连不上得从网络链路查起。SSL相关参数里组合拳常见用法是这几种场景参数组合本地开发图省事useSSLfalseallowPublicKeyRetrievaltrue内网环境可接受自签名useSSLtrueverifyServerCertificatefalse生产环境要真正加密导入CA证书requireSSLtrue3.4 超时类报错三个timeout别混在一起MySQL连接相关的时间有三个很容易混淆connectTimeout、socketTimeout、服务端wait_timeout。connectTimeoutTCP连接建立的超时时间单位毫秒。默认0表示无限等待网络不通时会卡很久。建议设成connectTimeout50005秒连不上就报错日志里快速暴露问题。socketTimeout一次SQL读写的等待超时默认0也是无限。这参数在裸JDBC里如果不设置一个慢查询能把线程挂半天。连接池场景请在池配置里设置。wait_timeout服务端主动断开空闲连接的阈值默认28800秒8小时。这个跟客户端没关系是MySQL自己的清理策略。调整它治标不治本真正应该做的是让连接池定期ping连接。超时报错还有个容易被忽视的场景连接池空闲时间超过了wait_timeout把连接拿了就报“连接已被关闭”或connection is not available。解决办法是配置连接池的connectionTestInterval或每次获取时做testOnBorrow让池子把过期连接提前筛掉。3.5 连接串引发的“奇怪报错”时区、字符集、Unknown database有些报错看代码简直莫名其妙其实全是连接串参数问题。比如时区乱码、中文插入变问号、Unknown database。Unknown database xxx最无语因为库里明明有这个库。常见原因URL里的库名拼错字符、大小写不一致MySQL在Linux下表名区分大小写、连接串里库名带了空格。这种问题用MySQL命令行客户端连一次把连接串原样敲进去一验便知。4. 从“连得上”到“用得好”事务与批量写入4.1 手动管理事务别让自动提交替你拍板JDBC默认是自动提交即每条SQL独立成一个事务。单条查询没问题但到了多表更新、订单扣库存这类操作自动提交是灾难。用事务的代码骨架Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 业务SQL1 // 业务SQL2 conn.commit(); } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) conn.close(); }核心点是setAutoCommit(false)要及早调用两条业务SQL之间不要插别的远程调用。IO等待会让事务长时间持有连接降低吞吐。相关设置中隔离级别默认是TRANSACTION_REPEATABLE_READMySQL默认框架层也经常配成READ_COMMITTED这个看业务一致性要求别乱调。4.2 rewriteBatchedStatements与批量写入的提速跑批任务里逐条INSERT很亏。JDBC的批量提交一点也不复杂String sql INSERT INTO t_order (id, user_id, amount) VALUES (?, ?, ?); try (PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (int i 0; i 10000; i) { ps.setInt(1, i); ps.setInt(2, userArr[i]); ps.setBigDecimal(3, amountArr[i]); ps.addBatch(); if (i % 1000 0) { ps.executeBatch(); } } ps.executeBatch(); conn.commit(); }有个参数很多人不知道rewriteBatchedStatementstrue。没加这个参数时一批语句会被当作多条独立的INSERT逐条发给MySQL网络往返次数完全没省。加了之后驱动会把一批同构INSERT重写成一条多VALUES语句性能几倍甚至一个数量级的提升。这个参数放在连接URL里jdbc:mysql://127.0.0.1:3306/mydb?rewriteBatchedStatementstrueuseSSLfalseserverTimezoneAsia/Shanghai注意批量插入用PreparedStatement别用Statement拼字符串否则既慢又有注入风险。SQL注入利用的恰恰就是拼接这个环节。4.3 连接复用时小心事务状态残留连接池会把用过的Connection回收复用。如果上一段代码忘了commit或者代码走了异常分支没回滚连接归还到池子里时仍处于事务中。下一个请求拿到这条连接直接读到上一段未提交的数据。这是特别隐蔽的“脏连接”问题。解决办法每次归还连接前连接池会调用rollback()或重置自动提交状态。HikariCP默认配置会自动做这个清理但前提是业务代码别把setAutoCommit改得乱七八糟。我在项目里见过一个同事在查询方法里写了conn.setAutoCommit(false)但根本没提交也不在finally里恢复结果整个服务间歇性数据错乱排查了两天才定位到。5. 现实项目中的连接配置HikariCP 与 Spring Boot MyBatis5.1 HikariCP 连接池参数不是默认就行刚接触连接池时很多人以为默认配置够用。HikariCP确实快但默认值并不适合每个业务的连接质量要求。重点看这几个参数maximumPoolSize池里最大连接数。不是越大越好。默认10对于中等并发够用但如果单SQL执行时间很长就得结合maxLifetime和connectionTimeout一起算。计算公式依赖具体场景简单经验值是“并发高峰期同时执行的SQL数少量冗余”。connectionTimeout客户端从池拿连接的等待时间默认30000毫秒。设置太小池空时很快抛connection is not available设置太大故障时线程集体挂起。maxLifetime连接最大存活时间。必须小于MySQL的wait_timeout否则连接被服务端断了以后池子里还愣愣地干活。默认1800000毫秒30分钟对比MySQL默认8小时是合理的但如果你调过wait_timeout这里要联动改。connectionTestQueryHikariCP默认通过JDBC4的isValid()检测连接一般不用额外配。老版本驱动才需要SELECT 1。minimumIdle池里保留的最小空闲连接数。高并发服务可以设小让池弹性伸缩低并发服务建议等于最大连接数避免频繁创建连接。HikariCP的一个常见配置案例HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://127.0.0.1:3306/mydb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8rewriteBatchedStatementstrue); config.setUsername(root); config.setPassword(password); config.setMaximumPoolSize(20); config.setConnectionTimeout(5000); config.setMaxLifetime(1200000); config.setMinimumIdle(5); config.setValidationTimeout(3000); HikariDataSource dataSource new HikariDataSource(config);5.2 Spring Boot MyBatis 场景的一体化配置现在的Java项目基本都是Spring Boot MyBatis。连接这块配置集中在application.ymlspring: datasource: url: jdbc:mysql://127.0.0.1:3306/mydb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: connection-timeout: 5000 maximum-pool-size: 20 idle-timeout: 600000 max-lifetime: 1200000 minimum-idle: 5经常有人在这报错明明用Navicat或命令行能连上数据库Spring Boot一启动就报连接失败。原因多半是Spring Boot内嵌的连接池HikariCP在启动时做连接检查如果你的URL少了allowPublicKeyRetrievaltrue或serverTimezoneHikari一探活就炸。这跟“代码逻辑”没关系纯粹是连接参数不完整。MyBatis这边常见注意点MyBatis操作的是DataSource不是Connection所以连接生命周期全在池子里。事务由Spring的Transactional管理本质是绑定当前线程的Connection多数据源场景要指定DataSource切换逻辑。在Mapper接口里写Options(useGeneratedKeys true)自增主键回填时底层依然依赖Connection的PreparedStatement能力。5.3 连接池的自愈机制为什么Spring Boot应用能扛住MySQL重启说个真实场景运维半夜把MySQL主库重启了一下服务端所有连接断掉。裸连代码这时候全废了新请求全部失败。但Spring Boot HikariCP的服务通常不需要重启过几十秒自己恢复。原因在于HikariCP会主动淘汰失效连接。同一时间它还会开一个后台线程做keepalive检测把新建的、健康的连接放回池中。应用拿到的连接如果是坏的执行SQL会出现异常异常重试逻辑或连接检测机制会触发淘汰最终池里全是新连接。但这里有个坑如果一个连接在“恰好坏掉”的临界点被应用拿到SQL执行失败后HikariCP只是把这个Connection标记为失效并不回滚业务代码里已经提交一半的事务。所以事务边界仍然要程序自己保证连接池解决不了事务一致性。我在实际工作中还遇到过一种情况数据库负载高连接建立本身很慢connectionTimeout设得又短应用频繁报“无法从连接池获取连接”。这种不是连接数量不够是单条连接建立耗时超过了等待上限。解法是先把connectionTimeout适当调大同时排查连接为什么建得慢别一上来就盲目调大maximumPoolSize。最后再分享一个小技巧。排查任何Java连接MySQL的问题都可以从数据库侧直接开通用日志SET GLOBAL general_log ON; SET GLOBAL general_log_file /tmp/mysql_general.log;日志里会记录每一条连接请求的来源IP、使用的账号、执行的SQL。如果你的应用报连接失败去看general log里有没有对应的握手请求被拒绝一翻就知道到底是服务端拒绝了IP还是密码不匹配还是从来没到达MySQL。这一招在排查复杂网络环境下的连接问题时比看Java堆栈高效得多。查完记得关掉SET GLOBAL general_log OFF;Java连MySQL这条路门槛不高但坑都是实打实的。驱动版本、连接参数、资源释放、连接池配置、事务边界每一个环节都能在深夜里给你惊喜。核心就一句话搞清楚每个参数的实际作用再做决策遇到报错先分网络层和协议层再对症下药。把这一套走熟了数据库连接这块基本也就做到心中有数了。