☰
JDBC连接MySQL全链路详解:驱动配置、PreparedStatement、事务与连接池实战
2026/10/20 20:19:26 网站建设 项目流程

JDBC 这个东西,很多 Java 开发天天在用,但真要自己从零写一遍“Java 连 MySQL 并完成增删改查”,不少人反而会卡在几个很基础的环节上。驱动版本不一致、时区报错、连接池参数不会填、PreparedStatement 和 Statement 搞混,这些坑我早年都踩过。这篇文章我会从一条完整的链路讲起:怎么选驱动、怎么写连接代码、怎么处理事务和批量操作、怎么配置连接池,最后再把常见的报错和排查思路整理成一张速查表。目标是让你看完能直接照着写,并且理解每一步为什么要这么干。

这篇内容适合谁?刚学完 Java 语法、想在项目里接数据库的同学;或者工作中主要用框架(比如 Spring Boot + JPA),但对底层 JDBC 交互逻辑不太清楚的开发者。如果你属于后者,这篇文章尤其有用,因为框架只是把底层的 Connection、Statement、ResultSet 封装了,一旦出现诡异报错,最终还是得回到 JDBC 原理上来排查。

1. 连接 MySQL 的整体思路与技术选型

1.1 连接背后的核心链路

Java 连接任何关系型数据库,走的核心链路都是一样的:应用代码通过 JDBC API 发起请求,驱动把请求翻译成数据库能理解的协议,数据库返回结果后,驱动再把结果翻译回 Java 对象。打个比方,JDBC 就像电源插座的标准,驱动就是各种转换插头,MySQL 驱动负责把 Java 的“电流”转换成 MySQL 能接收的“电流”。

这条链路上有几个关键角色:

  • JDBC API:是 Java 标准库的一部分(java.sql包),定义了一组接口,比如Connection、Statement、ResultSet、PreparedStatement。
  • JDBC Driver:真正的实现者。MySQL 官方提供了mysql-connector-j(以前叫mysql-connector-java),它实现了 JDBC 接口,底层用 socket 和 MySQL 服务端通信。
  • 数据库 URL:一个格式固定的字符串,告诉驱动“你要连哪台机器上的哪个数据库实例”。
  • 连接池:解决“频繁创建数据库连接开销大”的问题,HikariCP、Druid 都是干这个的。

很多人一开始直接学框架,反而跳过了这条链路。结果就是:Spring Boot 里配置一个spring.datasource.url就能连上数据库,但一旦换环境、改端口、调参数,就不知道怎么排查。

1.2 为什么直接用 JDBC 而不是上手就谈框架

我见过不少新手有这种疑问:现在写 Java 都是 Spring Boot + MyBatis,谁还手写 JDBC?

我的观点是:框架解决的是效率问题,JDBC 解决的是理解问题。你只有亲自写过一遍Class.forName(虽然现在可以省略)、DriverManager.getConnection、preparedStatement.executeQuery()、resultSet.next(),才能理解 MyBatis 的Mapper接口背后发生了什么,才能理解“数据库连接一定要释放”这句话为什么是铁律。

而且,手写 JDBC 在一些轻量级工具类里仍然有不可替代的价值。比如你写一个小的数据迁移脚本、一个定时任务需要快速从数据库拉数据,没必要为了这点功能引入一整套 ORM 框架。所以这篇文章的代码示例也会刻意不用框架,就用纯 JDBC,把底子打牢。

1.3 版本匹配那些容易踩的坑

版本问题是我最想先说的,因为这是新手最容易踩的第一个坑,报错信息还不直观。

MySQL 服务器版本和驱动版本需要匹配。如果你用的是 MySQL 5.7,那驱动用 5.x 或 8.x 都行;如果你用的是 MySQL 8.x,那驱动强烈建议用 8.x,因为 MySQL 8 默认的认证插件是caching_sha2_password,老驱动(5.1.x 及更早)根本不支持这个插件,连上去直接报Authentication plugin 'caching_sha2_password' cannot be loaded。

驱动坐标也变了。老项目里你可能看到的是:

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>

新版本(8.0.31 之后)官方把 artifactId 改成了mysql-connector-j:

<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.3.0</version> </dependency>

别小看这个改名,Maven 中央仓库里老的坐标虽然还能下,但已经不再更新,用新坐标才是长期方案。

另外一个容易被忽略的点是 URL 参数。MySQL 8 的驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,URL 也必须处理时区:

jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8

很多报错The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized就是时区参数没配导致的。

2. 环境准备与依赖配置

2.1 JDK、MySQL 与驱动的版本搭配

软件版本搭配没有唯一标准答案,但有个稳妥组合可以照抄:

组件推荐版本说明
JDK8 / 11 / 178 仍然兼容性最好,17 是长期支持版
MySQL Server5.7 / 8.0生产环境建议 8.0
Connector/J8.0.x / 8.3.x对应新 artifactId

JDK 版本不会太挑剔,真正有兼容性门槛的是驱动和数据库。有些老项目还在用 JDK 7,那你只能选驱动 5.1.x。JDK 8 及以上就可以无脑上 8.x 驱动了。

2.2 Maven 依赖引入的正确写法

用 Maven 管理依赖,推荐直接写新坐标。如果你已经在用 Spring Boot 2.7+,spring-boot-starter-jdbc里自带的驱动版本管理会帮你覆盖版本号,不写<version>也可以。但如果你没有用 Spring Boot,只是单独的普通 Java 工程,那必须显式声明版本。

完整依赖长这样:

<dependencies> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.3.0</version> </dependency> <!-- 推荐再带上日志,排查的时候能看到 SQL 执行信息 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.13</version> </dependency> </dependencies>

2.3 没有 Maven 的老项目怎么办

我接手过一些老项目,不用 Maven 也不用 Gradle,就是普通 Eclipse Java 工程,依赖全靠手动往 classpath 里塞 jar 包。这种情况下注意一点:驱动 jar 包必须放在 WEB-INF/lib 下(如果是 Web 项目)或者直接加进 classpath,并且发布的时候确认 jar 包真的打进去了。

之前在某个老系统上遇到过“本地能连数据库,部署到服务器就连不上”的诡异问题,排查到最后发现是构建工具没有把驱动 jar 包打进去,classpath 里根本没有驱动类。这种问题不会给你清晰的报错提示,只会报ClassNotFoundException,但从日志上不容易看出是打包问题。

3. 基础交互:JDBC 五步法与关键细节

3.1 加载驱动与建立连接

早期写法里第一行一定是:

Class.forName("com.mysql.cj.jdbc.Driver");

JDBC 4.0 之后,SPI 机制会自动加载驱动,这行代码其实可以省略。但我个人的建议是:在框架外写代码时保留这一行,不要赌环境。用了Class.forName可以让驱动注册过程显式化,一旦驱动包缺失,报错能更早暴露。

接下来是建立连接:

String url = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; String username = "root"; String password = "123456"; try (Connection conn = DriverManager.getConnection(url, username, password)) { System.out.println("连接成功,数据库产品信息:" + conn.getMetaData().getDatabaseProductName()); }

这段代码有几个值得展开的地方。

getConnection的底层逻辑是:DriverManager遍历所有注册过的驱动,逐个尝试用你的 URL 去连接,哪个驱动能解析这个 URL 就用哪个。如果 URL 写错了,比如端口不对,会报Communications link failure,这个错我后面会详细说。

URL 里的参数每个都有讲究:

  • useSSL=false:MySQL 8 默认是 true,但本地开发环境一般没有配置 SSL 证书,true 反而会报 SSL 警告甚至连接失败。生产环境如果要加密传输,单独配置证书链。
  • serverTimezone=Asia/Shanghai:解决时区问题。
  • characterEncoding=utf8:避免中文乱码。注意 MySQL 里utf8不是真正的全量 UTF-8,utf8mb4才是。所以强烈建议数据库建表用utf8mb4,连接参数可以写characterEncoding=utf8(驱动会做映射),也可以用最新的参数名connectionCollation=utf8mb4_unicode_ci。

还要注意一点,MySQL 8.x 默认认证插件是caching_sha2_password,如果驱动版本支持,连接没问题;但当你看到Public Key Retrieval is not allowed这个报错时,需要给 URL 加上:

allowPublicKeyRetrieval=true

这个参数的含义是允许客户端向服务端请求公钥,用于 RSA 加密传输密码。生产环境谨慎开启,本地开发直接打开省事。

3.2 Statement 执行 SQL 与 ResultSet 遍历

连接建立后,执行 SQL 需要三个步骤:创建 Statement、执行、处理结果。

try (Connection conn = DriverManager.getConnection(url, username, password); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT id, name, age FROM user")) { while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); int age = rs.getInt("age"); System.out.println("id=" + id + ", name=" + name + ", age=" + age); } }

这里最容易忽略的是ResultSet的行指针机制。刚查询完,指针指向第一行之前的区域,必须调用rs.next()把指针往下移动一行,移动到有数据的位置时返回true,移动到末尾返回false。这个机制也是很多从别的语言转过来的开发者容易不习惯的地方。

getXxx方法有按列名取和按列索引取两种方式。按列索引取下标从 1 开始,不是 0,这一点和数组不同。如果列名写错,运行时不会马上报错,只有取数据的时候报Column 'xxx' not found,排查起来要仔细看 SQL 里的别名和列名。

3.3 增删改查的完整实现

增删改查四类操作,只有查询用executeQuery,其他都用executeUpdate。这里直接给一个完整的工具类雏形,覆盖五个基础方法:

public class UserDao { private static final String URL = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=true"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; public List<User> findAll() { String sql = "SELECT id, name, age FROM user"; List<User> list = new ArrayList<>(); try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { User user = new User(rs.getInt("id"), rs.getString("name"), rs.getInt("age")); list.add(user); } } catch (SQLException e) { e.printStackTrace(); } return list; } public int insert(User user) { String sql = "INSERT INTO user (name, age) VALUES ('" + user.getName() + "', " + user.getAge() + ")"; try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); Statement stmt = conn.createStatement()) { return stmt.executeUpdate(sql); } catch (SQLException e) { e.printStackTrace(); } return 0; } }

注意:这个 insert 方法演示用没问题,真实项目绝对不能这样写。字符串拼接 SQL 会引发两个严重问题,一是 SQL 注入,二是在值里遇到单引号时直接语法错误。下一节我会重点讲怎么用PreparedStatement解决。

3.4 资源释放的正确姿势

资源释放是 JDBC 学习里最重要的一条军规:Connection、Statement、ResultSet 全部都要关闭,顺序是逆序关闭——先关 ResultSet,再关 Statement,最后关 Connection。

为什么要逆序?从资源依赖关系上理解:Connection 是底层物理连接,Statement 依赖 Connection 创建,ResultSet 又依赖 Statement。释放的时候如果先关 Connection,底层连接已经断了,依赖它的 Statement 和 ResultSet 可能就没办法正常释放内部资源。

现代 Java 提供了try-with-resources语法,极大简化了这个过程。只要类实现了AutoCloseable接口,try块结束时 JVM 会按照创建顺序的逆序自动关闭所有资源。所以上面的代码里我全部用了try (...)。

但不是所有资源都能写进 try 的括号里。比如你从连接池里拿的Connection,关闭它其实意味着“归还给连接池”,而不是真正物理断开,这个语义上的差异用try-with-resources也能正确处理,因为连接池的close()方法内部就是归还逻辑。

注意:finally块里手动关闭资源的老写法仍然大量存在于生产代码中,但新代码请一律用try-with-resources。它不仅能避免遗漏关闭,还能让异常处理更干净。

4. SQL 注入与 PreparedStatement 实战

4.1 SQL 注入是怎么发生的

假设你的登录查询是:

String sql = "SELECT * FROM user WHERE name = '" + name + "' AND password = '" + password + "'";

如果用户在输入框里输入admin' --(注意后面有个空格),拼接出来的 SQL 就变成了:

SELECT * FROM user WHERE name = 'admin' -- ' AND password = ''

--在 MySQL 里是注释符,后面的条件全被注释掉了。只要你输入一个存在的用户名,不需要密码就能绕过认证。更严重的还能拼出'; DROP TABLE user;--这种破坏性语句。

这是 Web 安全领域最经典的攻击方式之一,至今依然存在。别看是老技术,我见过的新项目里依然有这种拼接写法。

4.2 PreparedStatement 防注入的原理

PreparedStatement解决注入的思路是:SQL 骨架先编译,数据后填充。

String sql = "SELECT * FROM user WHERE name = ? AND password = ?"; try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, name); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { // 处理结果 } } }

?是占位符,setString方法填进去的内容会被驱动转义成一个合法的字符串字面量,单引号、反斜杠都会被正确处理。用户输入变成“数据”而不是“SQL 片段”,从根上断了注入的路。

这里有几个细节容易出问题:

  • 占位符序号从 1 开始,不是 0。
  • setString并不是唯一选择,传入日期要用setDate,传入整数用setInt,类型不匹配时数据库会报错,方便提前暴露问题。
  • 同一个PreparedStatement可以重复执行:先setObject(1, "a")执行一次,再setObject(1, "b")执行一次,SQL 骨架不变,效率比反复拼接 SQL 高。

4.3 预编译在性能上的收益

预编译另一个好处是性能。数据库接到 SQL 之后,要先解析、优化、编译,生成执行计划。对于使用?占位符的 SQL,只要骨架一样,MySQL 会缓存执行计划,后续只需要把参数填进去执行即可。

举个例子,循环插入一万条数据:

for (User user : userList) { String sql = "INSERT INTO user (name, age) VALUES ('" + user.getName() + "', " + user.getAge() + ")"; stmt.executeUpdate(sql); }

执行这条 SQL 一万次,数据库编译了一万次。

而用PreparedStatement:

String sql = "INSERT INTO user (name, age) VALUES (?, ?)"; try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { for (User user : userList) { ps.setString(1, user.getName()); ps.setInt(2, user.getAge()); ps.addBatch(); if (userList.indexOf(user) % 500 == 0) { ps.executeBatch(); } } ps.executeBatch(); }

SQL 只编译一次,配合addBatch/executeBatch做批量提交,性能提升非常明显。我实测过,万条数据逐条 insert 大约需要十几秒,用批量提交压缩到一两秒以内。

5. 事务控制与批量操作

5.1 手动提交事务的完整流程

JDBC 默认的提交模式是自动提交,也就是每条 SQL 执行完就立刻 commit。但在真实业务里,比如转账、下单扣库存,必须保证“多条 SQL 同时成功或同时失败”。

核心步骤就三步:关闭自动提交、执行多条 SQL、提交或回滚。

try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD)) { // 关键:关闭自动提交 conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("UPDATE account SET balance = balance - 100 WHERE id = ?")) { ps.setInt(1, 1); ps.executeUpdate(); try (PreparedStatement ps2 = conn.prepareStatement("UPDATE account SET balance = balance + 100 WHERE id = ?")) { ps2.setInt(1, 2); ps2.executeUpdate(); } } conn.commit(); } catch (SQLException e) { // 一旦出错,回滚 conn.rollback(); e.printStackTrace(); }

注意回滚的写法。很多人会把rollback放在catch块里,这没错,但有一个细节:如果数据库连接本身因为网络异常断开,执行rollback可能又抛出一个新的 SQLException。更稳妥的做法是 catch 里先判断连接是否还有效:

catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.addSuppressed(e); ex.printStackTrace(); } } }

另外一个高频错误是:如果在事务里执行了 DDL(比如CREATE TABLE),MySQL 会隐式提交,导致事务边界失效。这一点极其坑人,排查事务不生效的问题时,先看看事务里有没有混入 DDL 语句。

5.2 批量写入的十万行实践

批量写入在实际工作中非常有价值。你帮业务方导数据,十万条记录如果一条条 insert,可能要几分钟;用批量写入,十几秒就能跑完。

批量写入的底层逻辑是:先把多条 SQL 攒在客户端内存里,攒到一定量后一次性发给数据库执行,减少网络往返。这个“一定量”很关键,不是越多越好。每个数据库驱动都有自己的参数限制,MySQL 驱动默认有rewriteBatchedStatements参数,如果不开它,批量写实际退化成逐条执行。

所以要开连接参数:

jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true

代码写法:

String sql = "INSERT INTO user (name, age) VALUES (?, ?)"; try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); int batchSize = 500; for (int i = 0; i < 100000; i++) { ps.setString(1, "user_" + i); ps.setInt(2, i % 100); ps.addBatch(); if (i % batchSize == batchSize - 1) { ps.executeBatch(); conn.commit(); } } ps.executeBatch(); conn.commit(); }

批量大小我一般设 300 到 1000,经验值。设太小,频繁执行批量操作,收益打折;设太大,内存占用和驱动内部缓冲可能出问题。如果你要导大量数据,建议先小样本测试,找到当前表结构下的最优批量大小。

经验:批量插入最怕主键冲突和唯一索引。批量执行报错时,日志不会明确告诉你哪条数据出了问题,定位难度大。导入前先清洗一遍数据,确保没有重复记录再执行批量。

6. 连接池选型与配置

6.1 为什么不用 DriverManager 裸连

DriverManager.getConnection每次调用都会创建一个物理连接,数据库端需要完成 TCP 握手、认证、分配线程等操作,这个开销相当可观。在一个日活几万人的系统里,每个请求都裸连数据库,数据库很快就会被拖垮。

连接池的思路是:预先创建一批物理连接放在池子里,谁用谁取,用完归还,不够就新增,空闲太多就回收。

市面上主流的连接池有:

连接池特点适用场景
HikariCPSpring Boot 默认,性能极高,代码精简绝大多数新项目
Druid阿里开源,监控能力强,内置 SQL 防火墙需要监控和权限控制的中大型系统
DBCP老牌连接池老项目维护、简单场景
C3P0时代久远,不推荐新项目使用遗留系统

我的建议是:新项目直接 HikariCP,一个常用功能一个没有短板,性能还最强。需要监控大屏和 SQL 审计再看 Druid。

6.2 HikariCP 配置实战

引入依赖:

<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.1.0</version> </dependency>

注意:HikariCP 5.x 要求 JDK 11,如果还在用 JDK 8,用 4.0.3 版本。

基本配置:

HikariConfig config = new HikariConfig(); config.setJdbcUrl(URL); config.setUsername("root"); config.setPassword("123456"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); config.setPoolName("myAppPool"); HikariDataSource dataSource = new HikariDataSource(config);

使用的时候很简单:

try (Connection conn = dataSource.getConnection()) { try (PreparedStatement ps = conn.prepareStatement("SELECT 1")) { try (ResultSet rs = ps.executeQuery()) { rs.next(); System.out.println(rs.getInt(1)); } } }

拿到的 Connection 用法和直接用DriverManager拿到的完全一样,只是close()的语义从“关闭物理连接”变成了“归还连接池”。

6.3 连接池参数怎么调

连接池参数没有万能模板,要根据系统的并发量、SQL 平均执行时间、数据库规格来调整。几个关键参数:

  • maximumPoolSize:最大连接数。很多人一上来就设 200,数据库 CPU 都打满了还加连接,反而互相抢资源。经验公式大概是((核心线程数 * 2) + 有效磁盘数),HikariCP 官方文档推荐的计算方式是:connections = ((core_count * 2) + effective_spindle_count)。8 核机器例如就是 16~20。并发再高也建议先压测,不要盲目调大。
  • minimumIdle:池中最小空闲连接数,一般和 maximumPoolSize 设成一样,避免频繁创建和销毁连接。
  • connectionTimeout:等待连接的最大毫秒数,超过就抛异常。一般 3 秒到 10 秒。
  • maxLifetime:连接池中连接的最长存活时间,要小于数据库端wait_timeout,不然连接会被数据库端空闲断开,池子里拿到坏连接。
  • idleTimeout:连接空闲多久后从池中移除,要小于 maxLifetime。

踩过最典型的坑是:数据库端wait_timeout默认 8 小时,而连接池里连接的 maxLifetime 设置成了 13 小时,半夜没流量,MySQL 把连接断开,第二天早上来流量,应用从池子里拿到已经被断开的死连接,报Communications link failure。解决方式就是让连接池的连接生命週期低于数据库端的超时时间,并且加上连接有效性校验。

7. 常见问题与排查技巧

7.1 经典报错速查表

整理一份速查表,按报错信息准确度排序,方便遇到问题快速定位:

报错信息常见原因解决思路
ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动 jar 包没引入或没打进去检查 Maven 依赖、检查打包产物
Communications link failure网络不通、端口错、MySQL 没启动、连接被数据库断开用 telnet 测端口,确认 MySQL 状态
Access denied for user 'root'@'localhost'账号密码错误或主机白名单限制核对账号密码,确认 MySQL 的 host 授权
Unknown database 'test'数据库不存在先建库或者改 URL 里的库名
Server time zone value ... is unrecognizedURL 缺少时区参数加上serverTimezone=Asia/Shanghai
Public Key Retrieval is not allowedMySQL 8 认证插件问题,URL 缺参数加上allowPublicKeyRetrieval=true
The driver has not received any packets from the server网络问题或服务器端防火墙拦连接用 MySQL 客户端先本机测试,再检查防火墙
Can't connect to MySQL server on 'localhost' (10061)MySQL 服务未启动或端口未监听启动服务,检查端口监听状态

7.2 排查思路与方法

排查数据库连接问题,我有几个固定套路,成本低见效快:

先telnet测端口通不通:

telnet localhost 3306

端口不通,不用看代码,先看数据库服务状态和防火墙。端口通了,再用命令行客户端试试:

mysql -uroot -p -h localhost -P 3306

这个命令能成功,说明账号、网络、权限都没问题,问题大概率在 JDBC 驱动的 URL 参数或驱动版本上。也验证过 MySQL 8 下跑了 5.x 驱动导致的认证失败,用这个套路能快速定位。

还有一个容易被忽略的:历史项目里存在多个版本驱动 jar 包。比如容器里既有mysql-connector-java-5.1.x.jar,又有mysql-connector-java-8.0.x.jar,DriverManager是按类加载顺序来的,可能同一个 Java 进程里某些区域用老驱动,某些区域用新驱动,报错非常隐蔽。检查一遍 lib 目录,把旧驱动清理干净。

7.3 性能调优的实操体会

最后说点性能优化的实操体会。连接池配好、PreparedStatement 用上、批量写入开了之后,数据库性能还有几个提升空间:

  • SQL 执行计划分析:对慢 SQL 用EXPLAIN查看执行计划,关注type和rows列。全表扫描的type=ALL要重点优化,考虑加索引。
  • 字段类型匹配:Java 端setString传的值,如果和数据库字段类型不匹配,MySQL 会发生隐式类型转换,导致索引失效。比如数据库字段是int,Java 端传"1",两个都没错,但执行计划就走偏了。
  • 连接参数:rewriteBatchedStatements=true必须开,这是批量写入提速的关键。useServerPrepStmts=true&cachePrepStmts=true也能提升 PreparedStatement 的预编译缓存效果。

我个人在实际操作中还有一个习惯:每个查询都先问自己“这条 SQL 会扫描多少行”。不会扫描超过一万行的查询,不需要什么花哨优化;会扫描上百万行的查询,连接池配得再好也救不了你。先优化 SQL,再考虑连接层面的调优,顺序不能反。

数据库连接这块,踩坑是正常的。版本、时区、连接池参数、事务边界,每一项单独看都不难,但叠在一起就容易让人焦头烂额。遇到问题别急着翻框架源码,回到 JDBC 这条最基础的链路上,从 URL 到 Driver 到 Connection 一层层排查,多半能定位。最后再分享一个小技巧:写测试类的时候,先写一个SELECT 1的验证代码,确认“最小链路”通了,再往上叠业务逻辑,能帮你把问题的范围缩小一大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询