尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

JDBC连接MySQL全攻略:从环境配置到高频报错排查

发布时间:2026/9/29 15:25:47

资讯中心
01
ARTICLE

JDBC连接MySQL全攻略:从环境配置到高频报错排查

JDBC连接MySQL全攻略:从环境配置到高频报错排查
最近一段时间陆续有几位读者和同事给我发了同一类报错截图Communications link failure、Public Key Retrieval is not allowed、Unknown database。十个人里七八个都卡在同一个地方——JDBC连不上MySQL。代码看起来没毛病数据库服务也在跑为什么Java程序就是连不上去这篇文章就是把我从入门到现在用JDBC连接MySQL的全过程整理了一遍。从环境准备、驱动加载、最小连接代码、增删改查、事务、连接池到几种高频报错的完整排查链路都属于日常开发里能直接抄作业的东西。适合JavaWeb刚起步的同学也适合后端开发中间踩过连接坑但没有系统梳理过的人。内容全部来自我实际跑过的环境没跑通的方案我不会写在这里。1. 环境准备JDBC的第一步不是写代码1.1 MySQL 8 安装和初始账号设置很多教程上来就给代码但代码跑不起来往往不是代码的问题而是环境没对齐。JDBC连接MySQL首先得有一个能正常登录的MySQL服务。我这边开发机用的是MySQL 8.0.33Windows下推荐直接下ZIP压缩包解压然后用mysqld --initialize-insecure初始化再启动服务。Linux下则用系统的包管理器安装例如CentOS系的yum install mysql-server、Debian系的apt install mysql-server。初始化完成后MySQL 8在Windows下会让你在安装时设置root密码Linux下则可能把临时密码写进日志文件里。这里有个初学者经常卡住的点MySQL 8默认的认证插件是caching_sha2_password老版本的驱动和客户端连接时很容易报认证错误。如果你用Navicat、DataGrip这些GUI工具连接没问题但Java连接报错很大概率就是这个认证插件导致的。安装完MySQL之后我建议不要直接用root账号连接业务应用。原因很简单应用一旦被SQL注入root权限的破坏力是毁灭性的。所以我会先建一个专用库和专用账号CREATE DATABASE jdbc_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER demo% IDENTIFIED BY Demo_123456; GRANT ALL PRIVILEGES ON jdbc_demo.* TO demo%; FLUSH PRIVILEGES;%表示允许从任意主机连接如果只在本地开发改成localhost更安全。字符集一定要用utf8mb4因为MySQL 8默认就是它能存emoji和大部分特殊字符比老的utf8其实是utf8mb3兼容性好太多。很多乱码问题根子就在建库时用了错误的字符集。1.2 JDBC驱动包选型与加载原理JDBC本身是Java定义的一套接口规范真正干活的是MySQL官方提供的驱动实现。现在主流用法是在Maven里引入坐标dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency如果你在网络上查资料会看到很多老教程写的是mysql:mysql-connector-java。这个坐标在8.0版本之后改成了com.mysql:mysql-connector-j但老坐标依然有对应版本。不用Maven的话就去MySQL官网下载mysql-connector-j-8.0.33.jar丢到项目的lib目录并加入classpath。我见过不少同学在IDEA里配置了驱动包但运行时报ClassNotFoundException八成是jar没有真正打进classpath或者Maven依赖下载失败。遇到download from maven failed这种问题先检查本地仓库的jar是否完整再检查镜像配置是否正确不要急着怀疑代码。驱动包的加载机制是这样的JDBC 4.0之后驱动包内部有个META-INF/services/java.sql.Driver文件里面写了驱动实现类的完整类名com.mysql.cj.jdbc.Driver。当DriverManager初始化时会通过ServiceLoader自动扫描classpath下所有jar包中的这个文件把驱动类注册进来。所以新版本里那行经典的Class.forName(com.mysql.jdbc.Driver)其实可以不写了。1.3 JDBC URL连接参数的经典组合JDBC连接MySQL的URL格式是jdbc:mysql://主机地址:端口号/数据库名?参数1值1参数2值2本地开发最常见的一个组合是这样jdbc:mysql://127.0.0.1:3306/jdbc_demo?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingUTF-8参数逐个解释参数作用不加会怎样useSSLfalse关闭SSL加密连接用老版本配置或MySQL 8连接时会报SSL握手失败allowPublicKeyRetrievaltrue允许客户端从服务端获取公钥使用caching_sha2_password认证时报Public Key Retrieval is not allowedserverTimezoneAsia/Shanghai指定服务器时区报Server returns invalid timezone或者时间字段差8小时useUnicodetruecharacterEncodingUTF-8使用Unicode和UTF-8编码中文乱码关于SSL我多说一句。生产环境如果数据库和应用服务器在同一内网通常useSSLfalse问题不大毕竟内网流量相对可信。如果要走公网必须开启SSL或者用SSH隧道。MySQL 8默认其实支持SSL但用自签名证书的话JDBC客户端会有证书校验问题很多开发环境图省事直接关闭了SSL。我这里写出来是让大家理解这行的含义不是建议所有场景都关闭。真正上线前你需要和DBA确认连接策略。很多同学觉得这些参数是抄来的出了问题还是一头雾水。实际上理解URL参数背后对应的握手过程排查问题会快得多。JDBC建立连接时会经历TCP建连、协议握手、认证校验、参数协商这几个阶段URL参数本质上就是在控制协商过程中的行为。2. 从DriverManager到Connection最小可运行连接2.1 Class.forName到底是干嘛的网上90%的JDBC教程都会写这句话Class.forName(com.mysql.jdbc.Driver);有的写com.mysql.cj.jdbc.Driver有的写com.mysql.jdbc.Driver。作为一个从老版本走过来的人我可以负责任地说MySQL 8的驱动包中com.mysql.jdbc.Driver只是一个兼容占位类真正干活的是com.mysql.cj.jdbc.Driver。而由于SPI机制的存在这两行Class.forName在新版本驱动里都可以不写。但我不建议新手直接无视它。原因有两点第一如果项目里使用了特别老的驱动SPI文件可能不存在不写Class.forName就会报No suitable driver found。第二显式加载驱动类能让你在程序启动早期就发现驱动包没放对的问题而不是等到getConnection阶段才一脸懵。Class.forName的作用是触发类的静态初始化驱动类里有静态代码块会调用DriverManager.registerDriver(new Driver())把驱动注册进去。理解了这块你看到驱动加载相关报错时就能快速定位问题方向。2.2 第一个能跑起来的Java连接程序写一个纯JDBC的最小程序完整跑通连接和查询比什么都重要。我每次在新环境搭JDBC都会先跑这个最基本的例子确认环境和依赖没问题后再写业务代码import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class JdbcDemo { public static void main(String[] args) { String url jdbc:mysql://127.0.0.1:3306/jdbc_demo ?useSSLfalse allowPublicKeyRetrievaltrue serverTimezoneAsia/Shanghai useUnicodetrue characterEncodingUTF-8; String username demo; String password Demo_123456; try (Connection conn DriverManager.getConnection(url, username, password); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT 1)) { if (rs.next()) { System.out.println(连接成功结果 rs.getInt(1)); } } catch (Exception e) { e.printStackTrace(); } } }这段代码的输出是连接成功结果1。看起来简单但它验证了五件事驱动能被加载、TCP端口通、认证信息正确、目标库存在、网络参数配置没有阻塞连接建立。我推荐把url、username、password提炼到配置类或者Properties文件里别直接散落在业务代码中。虽然入门阶段图方便写死很常见但要尽早养成配置与代码分离的习惯。2.3 try-with-resources与资源关闭注意上面的代码用的是try-with-resources这是Java 7引入的语法。Connection、Statement、ResultSet三个对象都实现了AutoCloseable接口写在try的括号里就能自动关闭。很多没接触过这个语法的初学者会问我为什么要这样写我用一句话回答数据库连接是稀缺资源你不主动关闭连接池迟早被耗尽最终整个应用报Too many connections。用try-with-resources的好处是即使业务代码抛异常这三个资源也会按逆序自动关闭不会再出现查完数据忘记关连接的低级事故。实际开发中连接本身的创建和关闭通常由连接池管理但Connection依然是需要从池里按需获取和归还的。后面的章节会详细说连接池这里先记住一个原则谁打开的资源谁负责关闭。3. 增删改查完整实战用户表从零到封装3.1 建表和DAO结构光会连数据库自然不够实战中最基础的就是增删改查。我先建一张典型的用户表覆盖数值、字符串、时间三种常用类型USE jdbc_demo; CREATE TABLE t_user ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这表结构很简单但包含了不少细节。AUTO_INCREMENT主键用于演示获取自增IDUNIQUE约束用于演示重复插入时的异常处理created_at用数据库默认时间。关于MySQL中如何设置默认值很多人会写成DEFAULT 0摆在那里但在时间字段上显然不合适这里用DEFAULT CURRENT_TIMESTAMP才是正确姿势。Java端的DAO结构我习惯这样分一个User实体类一个UserDao类所有SQL操作集中在UserDao里不散落在Servlet或Controller中。3.2 新增PreparedStatement比Statement稳在哪插入用户是最常见的操作。这里必须使用PreparedStatement不建议用Statement拼接SQL。看代码import java.sql.*; import java.time.LocalDateTime; public class UserDao { private String url; private String username; private String password; public UserDao(String url, String username, String password) { this.url url; this.username username; this.password password; } public int insert(User user) throws SQLException { String sql INSERT INTO t_user (username, password, nickname, created_at) VALUES (?, ?, ?, ?); try (Connection conn DriverManager.getConnection(url, username, password); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getNickname()); ps.setObject(4, LocalDateTime.now()); int rows ps.executeUpdate(); try (ResultSet generatedKeys ps.getGeneratedKeys()) { if (generatedKeys.next()) { user.setId(generatedKeys.getLong(1)); } } return rows; } } }为什么用PreparedStatement两个核心原因第一SQL注入防护。如果我用String sql INSERT INTO t_user VALUES( username , ...)这种拼接当username传入abc OR 11这种内容时SQL语义就全变了。而PreparedStatement用占位符?驱动会把参数值转义后传给MySQL天然免疫这种注入。第二性能。PreparedStatement会预编译SQL同一结构的SQL多次执行时MySQL端可以复用执行计划。批量操作时差距更加明显。参数绑定时有个细节值得留意setObject(4, LocalDateTime.now())。MySQL 8驱动支持Java 8的时间类型但是要求JDBC URL里带上serverTimezone参数否则时区偏差会让时间数据错位几个到十几个小时。JDBC驱动不完全等同于JDBC规范MySQL驱动对时间类型的处理确实比其他驱动挑剔一些。3.3 查询ResultSet遍历的细节查询用户列表public ListUser findByName(String username) throws SQLException { String sql SELECT id, username, password, nickname, created_at FROM t_user WHERE username LIKE ?; ListUser list new ArrayList(); try (Connection conn DriverManager.getConnection(url, this.username, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % username %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { User user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setNickname(rs.getString(nickname)); user.setCreatedAt(rs.getTimestamp(created_at).toLocalDateTime()); list.add(user); } } } return list; }ResultSet的游标初始位置在第一条记录之前必须先调用next()才能读取第一行。每调用一次next()游标向后移动一行返回false就代表没有更多记录了。这个特性看起来简单但新手最容易在这种地方写错比如不调用next()直接getString或者循环条件写成if (rs.next())只取一行。列的读取可以用索引也可以直接用列名我推荐用列名。万一SQL里调整了列的顺序用索引的代码可能就取错数据了。另外getTimestamp返回java.sql.Timestamp可以通过toLocalDateTime()轻松转成LocalDateTime这也是现代Java代码中常见的做法。如果查询的数据量特别大MySQL驱动默认会把所有结果一次加载到JVM内存中可能导致OutOfMemoryError。此时可以通过设置连接参数和Statement参数来启用游标式逐条读取后续章节会再提。3.4 更新与删除executeUpdate的返回值更新和删除的代码结构类似都是先prepareStatement再绑定参数最后executeUpdate()。这个方法返回的是受影响的行数这个返回值非常有用public int updateNickname(Long id, String nickname) throws SQLException { String sql UPDATE t_user SET nickname ? WHERE id ?; try (Connection conn DriverManager.getConnection(url, username, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, nickname); ps.setLong(2, id); return ps.executeUpdate(); } } public int deleteById(Long id) throws SQLException { String sql DELETE FROM t_user WHERE id ?; try (Connection conn DriverManager.getConnection(url, username, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, id); return ps.executeUpdate(); } }当返回值为0时说明没有匹配的记录。这在业务上可以做判断比如修改了不存在的用户或者删除操作被重复提交。当下流行的ORM框架MyBatis、JPA底层其实还是这套JDBC接口只是把映射和资源管理封装掉了。所以我的建议是即使你工作后主要用框架也务必把今天这套原生JDBC的增删改查写一遍理解底层逻辑之后再用框架会顺手得多。4. 事务控制与批量操作数据库修改的原子性4.1 自动提交模式的坑JDBC默认情况下每条SQL执行完都会自动提交到数据库。这个行为由Connection的autoCommit属性控制默认值是true。在单条SQL插入的场景下自动提交很方便但是当一次业务操作需要多条SQL要么全部成功、要么全部失败时自动提交就成了大坑。举个典型场景用户注册时既要插入用户表又要初始化用户的积分账户表。如果两步之间抛了异常用户记录插入成功积分账户却没建数据就处于不一致状态。这种时候必须开启事务把多个操作包在同一个事务里。这里有个术语需要澄清网上很多人说JDBC中的事务是由Connection管理的这个说法不够准确。JDBC的Connection确实提供了事务控制接口但真正的事务边界在MySQL服务端Connection只是通过协议告诉MySQL在同一个会话里把多条SQL看作一个原子操作组。4.2 转账案例的完整事务代码我一般用经典的转账案例演示事务public void transfer(Long fromId, Long toId, BigDecimal amount) throws SQLException { String deductSql UPDATE t_account SET balance balance - ? WHERE id ? AND balance ?; String addSql UPDATE t_account SET balance balance ? WHERE id ?; try (Connection conn DriverManager.getConnection(url, username, password)) { conn.setAutoCommit(false); try { try (PreparedStatement deductPs conn.prepareStatement(deductSql)) { deductPs.setBigDecimal(1, amount); deductPs.setLong(2, fromId); deductPs.setBigDecimal(3, amount); int rows deductPs.executeUpdate(); if (rows 0) { throw new SQLException(扣款失败余额不足或账户不存在); } } try (PreparedStatement addPs conn.prepareStatement(addSql)) { addPs.setBigDecimal(1, amount); addPs.setLong(2, toId); addPs.executeUpdate(); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } } }注意finally块中的setAutoCommit(true)这个操作是很多教程不会提的细节。如果连接后续被归还到连接池而autoCommit状态没有恢复下一次从池中拿到这个连接时事务状态可能是脏的。所以每次使用完连接都应把状态还原成默认值。4.3 批量插入与rewriteBatchedStatements事务和批量通常一起出现。比如导入Excel里的1万行用户数据逐条SQL插入会很慢批量插入能把性能提升一个数量级。JDBC的批量插入写法如下public void batchInsert(ListUser users) throws SQLException { String sql INSERT INTO t_user (username, password, nickname, created_at) VALUES (?, ?, ?, ?); try (Connection conn DriverManager.getConnection(url, username, password)) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (User user : users) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getNickname()); ps.setObject(4, LocalDateTime.now()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } }这里有一个MySQL驱动的隐藏优化参数rewriteBatchedStatementstrue。如果不加这个参数executeBatch()内部还是逐条向MySQL发送SQL拿不到批量优化的好处。加上这个参数后驱动会把批量的Insert语句重写成一条多值Insert网络往返次数从N次降到1次。我在本地测试过往一个8字段的表中写入5万条记录开启这个参数后耗时从大约20秒降到2秒以内。代价是MySQL端单条SQL会比较长但影响微乎其微。这个参数官方文档有说明但很多生产代码里没有启用如果你的业务有批量写入需求值得检查一下。5. 连接池为什么是标配HikariCP配置和避坑5.1 裸连接的性能问题用DriverManager.getConnection直接创建连接在写Demo和工具时没问题。但放到生产环境里这种模式完全不可取。原因我去掉技术黑话用生活类比来解释数据库连接相当于你和MySQL之间的一条专用电话线。每次getConnection都要重新拨号TCP握手、MySQL认证、权限检查。电话通了业务SQL执行完如果直接挂断下次不得不再拨一遍。在高并发场景下正事全耽误在拨号上了数据库服务端也会因为频繁创建和销毁连接线程而压力巨大。连接池的思路是提前建好一批连接备用比如10条业务要连接时不用拨号直接拿一条空闲线用用完归还而不是挂断。这也就是HikariCP这类连接池存在的意义。Spring Boot 2.x之后默认内置HikariCP已经不需要额外引入依赖但如果你是手写JDBC项目就得自己配置。5.2 HikariCP参数逐个解析一个典型的HikariCP配置如下HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://127.0.0.1:3306/jdbc_demo?useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue); config.setUsername(demo); config.setPassword(Demo_123456); config.setDriverClassName(com.mysql.cj.jdbc.Driver); config.setMaximumPoolSize(10); config.setMinimumIdle(5); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); HikariDataSource dataSource new HikariDataSource(config);参数含义和我的建议见下表参数默认值说明与建议maximumPoolSize10最大连接数。根据应用并发量调整不是越大越好每条连接都要占用MySQL连接数和内存minimumIdle与max相同最小空闲连接数。为了稳定性建议设成和max一样避免流量波动时频繁创建连接connectionTimeout30000客户端获取连接的超时时间。默认30秒够用调太长会让调用方长时间挂起idleTimeout600000空闲连接回收时间。必须小于MySQL的wait_timeout否则连接会被MySQL在中间断开maxLifetime1800000连接最大存活时间。同样建议略小于MySQL侧的超时时间HikariCP的性能优势在社区已经得到公认它的字节码体积小、逻辑简洁、没有多余锁竞争。用Pair参数直接从源码层面看HikariCP在获取连接时使用了无锁队列相比一些老牌连接池在并发竞争时性能差异很大。总之没有特殊理由的话新项目直接用HikariCP就对了。5.3 连接池把连接搞挂的经典场景连接池虽然解决了很多问题但也会引入新问题。最典型的一个是连接被MySQL服务端断开。MySQL默认有个参数wait_timeout28800单位秒也就是8小时。如果一条数据库连接在8小时内没有任何操作MySQL服务端会主动把它断开。而连接池里的连接如果长时间空闲客户端还不知道这个连接已经失效一旦应用拿到这条僵尸连接去执行SQL就会报错。Communications link failure这种报错很大比例就是这么产生的。解决办法有两层第一层是给连接池设置比wait_timeout更短的maxLifetime比如设成30分钟这样连接即使空闲也不会活过8小时。第二层是配置连接测试HikariCP默认使用jdbc:mysql驱动自带的ping机制来做连接存活校验不需要额外写SELECT 1。如果你看到网上有文章说配置connectionTestQuerySELECT 1那其实是对老连接池如C3P0、DBCP的迁移习惯对HikariCP而言不是必须的。还有一类问题连接池的maximumPoolSize设置过小而业务代码没有用try-with-resources或finally关闭连接也就是连接泄漏。连接池里的连接被借走不还因为每次拿连接后没有归还池子很快耗尽后来的请求全部阻塞等待connectionTimeout超时。这时候排查的方式是开HikariCP的leakDetectionThreshold参数比如设成60000超过60秒连接仍未归还时日志里会输出告警和堆栈快照一眼就能定位是哪段代码泄漏了连接。6. 高频异常排查六个经典报错的完整链路6.1 ClassNotFoundException: com.mysql.cj.jdbc.Driver这个报错在入门阶段特别常见根因基本都是驱动jar没加入到classpath。排查顺序我建议这样走第一确认Maven坐标引入正确并去本地仓库~/.m2/repository/com/mysql/mysql-connector-j/看看jar文件是否真的存在且完整。如果存在但大小为0字节、或者一直报download from maven failed多半是Maven镜像没有MySQL驱动或者网络问题换阿里云镜像或腾讯云镜像重试。第二如果是手动导jar包的旧式项目检查IDEA的Project Structure里Libraries是否包含jar或者部署时是否打进了WEB-INF/lib。按照我的经验ClassNotFoundException本身不代表代码有问题它只说明驱动类不在运行时的类路径里。很多人在这一步卡了一整天才发现是Maven依赖没刷新。6.2 Communications link failure这是最容易被误判的报错之一。完整错误通常长这样com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.常见原因有四类第一MySQL服务没启动。用命令行试一下能不能连上比如mysql -h127.0.0.1 -P3306 -uroot -p。如果命令行都能连说明服务没问题问题在Java侧。第二网络不通。检查应用和数据库之间的网络策略。本地开发还好一旦上了服务器安全组、防火墙、容器网络都可能导致TCP端口不可达。这里提醒一句很多云服务器安全组默认只开放80/4433306端口没放行telnet ip 3306直接超时。第三MySQL的bind-address配置。默认情况下MySQL可能只绑定了127.0.0.1外部IP没法连。需要修改my.cnf里的bind-address0.0.0.0并重启服务。注意如果是容器部署还要确认端口映射正确。第四等待或超时问题。连接建立过程中MySQL端认证较慢导致客户端超时可以把JDBC URL里加上connectTimeout5000socketTimeout60000明确定义超时时间排查时会更容易定位问题。6.3 Public Key Retrieval is not allowed 与 SSL连接错误这个问题和MySQL 8的默认认证插件强相关。前面提到过MySQL 8默认使用caching_sha2_password认证这个插件在非SSL连接下需要从服务端取公钥进行密码加密传输。MySQL服务端默认不自动下发公钥需要客户端开启allowPublicKeyRetrievaltrue或者改用mysql_native_password认证。报错信息长这样java.sql.SQLException: Public Key Retrieval is not allowed解决办法最直接的是在JDBC URL参数里加上allowPublicKeyRetrievaltrue。如果加了之后还报SSL证书错误再加useSSLfalse。这两个参数经常同时出现。如果你用的是老版本驱动连接MySQL 8可能还会遇到数据库驱动版本和数据库版本不匹配的问题我的建议是直接升级驱动到8.0.x。有些同学习惯把报错截图发群里问但其实自行排查并不复杂。JDBC连接失败时优先看最后一个报错信息里的关键短语按我上面说的方法一步步对照即可。6.4 Unknown database 与 Access denied这两个报错的信息很直白java.sql.SQLException: Unknown database jdbc_demo java.sql.SQLException: Access denied for user demolocalhost (using password: YES)Unknown database说明URL里的库名在MySQL上不存在或者字符集/大小写不一致例如实际库名是JDBC_DEMO而你写成了jdbc_demo。用SHOW DATABASES;确认一下再改URL即可。Access denied则要区分两种可能密码确实错了或者用户demolocalhost不存在。这里有个决策点你建用户的时候用的是demo%但连接来源地址可能被MySQL解析为localhost。如果只有demo%这个账号理论上demolocalhost也可以匹配%通配但某些场景下密码认证顺序会比较特殊。保险做法是同时创建CREATE USER demo% IDENTIFIED BY Demo_123456; CREATE USER demolocalhost IDENTIFIED BY Demo_123456;日常开发没必要搞这么复杂但你理解了MySQL账号体系由用户名主机名共同决定之后遇到这类权限问题就不会瞎猜密码了。6.5 8小时问题与wait_timeout上面已经在连接池章节提过服务端主动断开连接的问题。这里我提供一条完整的排查思路如果应用运行一段时间后突然出现大量Communications link failure而且重启应用就好过几小时又复发那基本就是wait_timeout和连接池参数不匹配导致的。我处理过几次类似线上问题最终步骤都是SHOW VARIABLES LIKE wait_timeout;看这个值是多少秒。然后把连接池的maxLifetime设置成比它小至少几分钟。如果是HikariCP设置有maxLifetime180000030分钟就能覆盖8小时场景。如果服务端把wait_timeout改得很短比如有人为了安全给MySQL设成60秒那连接池的maxLifetime必须小于60秒否则重启后马上又会踩坑。这类参数调整建议部署前跟DBA一起确认别只靠开发侧使劲。7. 性能与安全把JDBC写得更可靠7.1 批量参数带来的实际效率变化我在前面提过rewriteBatchedStatementstrue的威力。现在再补充一组实测数据用最普通的批量插入1万条数据耗时大约在5秒到15秒之间开启rewriteBatchedStatementstrue后同样环境下降到1秒左右。这是因为驱动优化了网络协议把多行Insert合并成单条多值Insert发送。有一点要注意rewriteBatchedStatements只对INSERT优化明显对UPDATE、DELETE的批量处理处理逻辑各不相同不要盲目认为所有批量都会加速。以MySQL官方文档为准实测哪种SQL有效再决定要不要全局开启。7.2 密码与敏感信息处理把数据库账号密码硬编码在main方法里对Demo没问题对真实项目就是事故隐患。我见过不止一次源码不小心推到公开仓库密码直接被爬虫抓走的案例。我自己常用的做法是三层第一层代码中用System.getenv或外部配置中心读取环境变量不把密码写进Java文件。第二层密码字段在配置文件中加密存储应用启动时用专门的解密工具处理。可以用Jasypt也可以自己写一个简单的AES解密工具类。第三层数据库账号权限最小化。只给应用它需要的权限绝不随便给ALL PRIVILEGES ON *.*。如果应用只做增删改查就只授权SELECT, INSERT, UPDATE, DELETE。7.3 使用PreparedStatement之外的几个效率习惯除了PreparedStatement我建议再养成几个习惯查询不要使用SELECT *。给t_user表加一个bio的TEXT字段之后SELECT *会把不需要的大字段也拖出来白白消耗网络带宽和内存。按需指定列后面优化SQL可读性也好很多。使用fetchSize控制大数据量查询的内存占用。MySQL JDBC驱动默认会把查询结果全部加载到内存如果结果集很大加上useCursorFetchtrue参数并调用ps.setFetchSize(1000)可以变成每次从服务端取一批数据。不过在我实际使用中开启后查询性能会有所下降这个方案只在明确需要遍历超大数据集时才用不适合普通分页查询。善用setQueryTimeout防止慢SQL拖垮应用。对容易失控的管理后台查询我会在Statement上设置几秒超时宁可让这一次查询失败也不能让它一直占用数据库连接。数据库索引也是效率的一环。WHERE条件和JOIN字段上如果没有索引JDBC端怎么优化都效果有限。这个和JDBC本身关系不大但排查慢SQL时先从EXPLAIN看索引命中情况再回头检查Java代码顺序不要反。7.4 我最后想分享的一点小经验写原生JDBC的这些年我最深刻的体会是报错信息是最好的老师。Communications link failure、Public Key Retrieval is not allowed、Connection is not available每个报错背后都是具体原因网上也能搜到大量案例。但光搜答案不够你得理解连接建立的过程、事务的边界、连接池的生命周期才能在遇到新报错的时候不慌。我见过不少同学为了绕开JDBC直接把MyBatis配置一贴跑通了就不管了。这样短期内确实能省事但一旦遇到连接失败、事务不回滚、连接池泄漏这类问题缺少底层知识会让你完全无从下手。所以这篇详细教程虽然写的都是基础但每一步都值得自己动手敲一遍。尤其建议你故意把参数写错几次、把连接池调小几次亲眼看看不同报错是什么样这个经验的含金量比看十篇教程都高。上面这些内容都是我实际跑过、踩过之后沉淀下来的。照着一路做下来你应该不会再被JDBC连接MySQL的基本问题难住了。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。