简介:面向 Java 开发者的 Spring Boot 使用 JDBC 连接 MySQL 数据库整套解决方案,清晰定位传统 JDBC 方式下的数据访问场景,适合初学 Spring Boot 数据层搭建、需要快速跑通数据库连接的读者。资源包共 136 个文件,约 637MB,内容包含 Java 源码、class 编译文件、SQL 建表脚本、xml 与 properties 配置,以及大量 dll 动态库和 exe/msi 安装程序,覆盖从 MySQL 环境安装到项目编码调试的完整链路。目前已有 598 人学习下载,具备一定参考价值。读者可结合其中的控制器类与实体类,理解业务接口、实体映射与 JDBC 操作之间的协作方式,借助 SQL 脚本和配置示例快速替换为自己的数据库连接参数;附带安装包和驱动文件可减少本地环境搭建时间,是一份适合边看边练的实战型资料,尤其适合课程设计、毕业设计或企业项目前期快速验证场景。
1. SpringBoot 用 JDBC 直连 MySQL:为什么老方案至今仍是数据访问层的地基
“SpringBoot + JDBC 直连 MySQL”这套组合听起来像教科书里的老古董,但在生产环境里,它依然是所有数据访问框架绕不开的地基。MyBatis 的 SqlSession、JPA 的 EntityManager,底层拿到的都是 JDBC Connection;连接池 HikariCP 管理的同样是 JDBC 连接。用 JDBC 直连不是技术倒退,而是把黑匣子打开——连接报错时,你能直接看到是驱动、URL 还是连接池参数出了问题。这套方案适合两类人:刚接手 SpringBoot 项目的 Java 工程师,想搞清楚数据源怎么配才算对;以及准备 java面试题时被“JDBC 和连接池什么关系”卡住的求职者。标题里的“整套解决方案”其实就三步:装对 MySQL、配好数据源、用 JdbcTemplate 跑通增删改查。下面从安装包一路拆到连接池调优和排错,所有命令和代码都能直接照抄。
2. 装对 MySQL 和 JDBC 驱动:Windows 10 下安装包选型与 5.7/8.0 版本差异
数据访问层翻车,一大半发生在连接建立之前。MySQL 没装对、驱动版本不匹配,后面再好的代码也跑不起来。这一章把 Windows 10 上最常见的安装路线、账号边界和驱动版本匹配一次讲清楚。
2.1 安装包怎么选:MSI 图形安装与 ZIP 免安装两条路线
MySQL 官方安装包有两种形态:MSI 安装器和 ZIP 免安装版。MSI 适合不想碰运维的人,一路下一步;ZIP 适合要脚本化交付、要把数据目录放到独立目录的人。我在交付一类“带安装包”的整套工程时,默认把 ZIP 包放进项目,因为目录可控、初始化命令也能写进脚本。
搜索里经常看到“mysql 5.7.44 安装过程详细”,5.7.44 是 5.7 系列的最后一个安全修订版,很多老项目锁死在这个版本。如果机器上已经有一个 5.7 实例占着 3306 端口,再装 8.0 时把端口改成 3307,SpringBoot 数据源 URL 里的端口同步改就行。
MSI 路线的几个要点:下载 mysql-installer-community 时区分 web 版和 full 版,web 版装完还要联网拉组件,离线环境用 full 版。安装时选 Server only,省掉一堆用不到的组件。Type 里 Development Machine 占内存小,Server Machine 会把性能参数调得偏激进,开发机选前者。root 密码一定要记牢,同时建议顺手建一个业务账号,后面 2.2 会讲。Windows 10 下服务起不来,多半是缺 VC++ 运行库,装 Visual C++ Redistributable 2015-2022 就能解决。
ZIP 路线我实际用得多,整个 MySQL 目录可以直接和交付源码放一起,这也是标题里“安装包”最常见的落地形态。步骤和参数如下:
# 以 MySQL 8.0.x zip 为例,解压到 C:\mysql-8.0 # 1) 在 C:\mysql-8.0 根目录放一个 my.ini(内容见下) # 2) 管理员身份打开 cmd,初始化数据目录;data 目录必须尚不存在 C:\mysql-8.0\bin\mysqld --initialize-insecure --basedir=C:\mysql-8.0 --datadir=C:\mysql-8.0\data # 3) 注册成 Windows 服务并启动 C:\mysql-8.0\bin\mysqld --install MySQL80 --basedir=C:\mysql-8.0 --datadir=C:\mysql-8.0\data net start MySQL80[mysqld] basedir=C:/mysql-8.0 datadir=C:/mysql-8.0/data port=3306 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-authentication-plugin=caching_sha2_password [client] default-character-set=utf8mb4--initialize-insecure 的意思是初始化数据目录但不生成随机 root 密码,初始 root 密码为空,登录后必须立刻改密。--install 之前先执行 sc query MySQL80 确认没有同名服务。net start 失败时,去 C:\mysql-8.0\data 目录下找以 .err 结尾的错误日志,几乎都能定位到具体原因,这是 Windows 下最有用的排错入口。
5.7 和 8.0 在 zip 初始化上有两个差异值得注意:5.7 的 root 空密码就能直接本地登录,8.0 登录后执行任何查询前都会要求先改密;8.0 默认认证插件是 caching_sha2_password,5.7 默认是 mysql_native_password。这两个差异直接影响后面驱动的连接参数,第 5 章会展开。也有人图省事用 docker 起 MySQL,但 Windows 上 Docker Desktop 的端口映射和数据目录挂载问题不比 zip 装省心,这里不展开。
2.2 建库建账号:root 与 springapp 分离的第一道安全边界
初始化完成后,第一步是改 root 密码、建业务库、建专用账号。这类整套方案里,我一般不会让应用用 root 连库,而是建一个只拥有业务库权限的账号:
mysql -uroot -p ALTER USER 'root'@'localhost' IDENTIFIED BY 'YourRootPassword'; CREATE USER 'springapp'@'localhost' IDENTIFIED BY 'AppPassword123'; CREATE DATABASE IF NOT EXISTS user_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT ALL PRIVILEGES ON user_db.* TO 'springapp'@'localhost'; FLUSH PRIVILEGES;注意 springapp@'localhost' 和 springapp@'%' 是两个完全独立的账号。SpringBoot 和 MySQL 在同一台机器上时用 localhost 即可;如果你要跨机器部署,比如应用在另一台服务器上连这台数据库,必须建 springapp@'%',并且把数据源 URL 里的地址从 localhost 改成目标 IP。很多连不上的问题不是密码错,而是主机限制。
数据库名 user_db 的字符集直接用 utf8mb4,不要用老项目的 utf8。utf8mb4 是 MySQL 8 的默认字符集,能存 emoji 和生僻字,utf8 只是 utf8mb3 的别名,新库不该再选它。建库语句里的 COLLATE utf8mb4_unicode_ci 决定排序规则,如果你要做大小写敏感查询,后面再针对具体列改 collation,不要在库级别模糊处理。
2.3 驱动坐标与 SpringBoot 版本匹配:从 mysql-connector-java 到 mysql-connector-j
Java 连 MySQL 靠官方驱动 MySQL Connector/J。这个驱动经历过一次坐标改名,很多老项目翻车就翻在这里。
MySQL 8.0 之前的坐标是 mysql:mysql-connector-java:5.1.49,驱动类 com.mysql.jdbc.Driver。MySQL 8.0 发布后驱动跳到 8.0.x,坐标还是老样子,但驱动类换成了 com.mysql.cj.jdbc.Driver,老类名保留但已废弃。从 8.0.31 开始官方把 artifactId 改成 com.mysql:mysql-connector-j,包名没变,坐标变了。
这个改名直接影响了 SpringBoot 的版本管理:
| SpringBoot 版本 | 默认管理的驱动坐标 | 驱动类 | 要求 JDK |
|---|---|---|---|
| 2.3 ~ 2.7 | mysql:mysql-connector-java:8.0.x | com.mysql.cj.jdbc.Driver | JDK 8+ |
| 3.0 ~ 3.4 | com.mysql:mysql-connector-j:8.3+ | com.mysql.cj.jdbc.Driver | JDK 17+ |
社区里常有人问“springboot版本太高是不是连不上库”,多半不是版本高的错,是驱动坐标写成了老样子。SpringBoot 3 把 javax.* 换成了 jakarta.*,对 JDBC 这块影响很小,真正变化的是 Maven 坐标和 JDK 要求。判断方法很简单:SpringBoot 2.7 及以下用 mysql-connector-java,3.x 用 mysql-connector-j,pom 里不写版本号,交给 spring-boot-dependencies 统一锁版本,避免自己指定版本跟框架冲突。
真要手工指定版本,记住一个原则:驱动主版本不低于 MySQL 服务端大版本。8.0.x 驱动连 5.7 服务端完全正常;反过来用 5.1.x 驱动连 8.0 服务端,握手阶段就会报 SSL 和认证错误。想确认项目实际用的驱动版本,用 Maven 依赖树查:
mvn dependency:tree -Dincludes=com.mysql:mysql-connector-j mvn dependency:tree -Dincludes=mysql:mysql-connector-java输出里能看到版本号从哪个父 POM 或 BOM 来。如果两个坐标同时出现在依赖树里,运行时大概率报 NoClassDefFoundError,处理办法是统一坐标并排除多余依赖。
3. 让 SpringBoot 跑通 JDBC 的最小配置:依赖、数据源 URL 与 JdbcTemplate
这一章从空工程开始,把 JDBC 连接 MySQL 的最小闭环搭出来。目标只有一个:启动不报错,接口能查到数据。后面的连接池调优和排错都建立在这一章的配置上。
3.1 只加两个依赖:spring-boot-starter-jdbc 与 mysql-connector-j 的分工
SpringBoot 里用 JDBC 不需要引入完整的 spring-context,一个 starter 就够。pom.xml 里加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>两个依赖的分工很清楚:spring-boot-starter-jdbc 传递引入 spring-jdbc 和 HikariCP,负责连接管理和 JdbcTemplate;mysql-connector-j 是驱动,负责把 JDBC 调用翻译成 MySQL 协议。驱动用 runtime scope,因为业务代码里不会 import 任何驱动类,它只需要在运行时出现在 ClassLoader 里。
如果是 springboot modules 多模块工程,数据源相关配置放在 common 模块,业务模块依赖 common 即可,驱动坐标也只在 common 里声明一次,避免每个模块各引一份造成版本混乱。SpringBoot 的自动装配会扫描 classpath 里的驱动注册信息,驱动类名和数据源配置写好,DataSource 和 JdbcTemplate 的 Bean 就会自动创建。
启动时报“Failed to configure a DataSource”时,先检查是不是漏了 starter-jdbc,或者驱动坐标在某个模块里被排除了。这个报错的含义是自动装配没找到可用的 DataSource,90% 是依赖问题,不是代码问题。
3.2 数据源 URL 参数逐个拆解:serverTimezone、useSSL、allowPublicKeyRetrieval
application.yml 里最核心的是 datasource 段:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: springapp password: "AppPassword123"URL 里这几个参数,每一个都有具体的翻车故事。
useUnicode=true 和 characterEncoding=utf8 是一对,作用是告诉驱动连接字符集按 UTF-8 处理。8.0 驱动对 characterEncoding=utf8 会自动映射到 utf8mb4,除非你显式写 characterEncoding=utf8mb4。中文乱码问题八成出在这一对参数或库表字符集上。
useSSL=false 表示不启用 SSL 加密。本地开发、内网调用建议关掉,因为开 SSL 需要额外处理证书信任链,传输性能也有损耗。外网环境需要传输加密时,应该靠网络层面的方案解决,改这个参数本身解决不了安全问题。
serverTimezone=Asia/Shanghai 是 MySQL 8 时代几乎必加的参数。驱动在建立连接时会去读服务端时区,服务端没配的话直接报“The server time zone value is unrecognized”然后启动失败。加上这个参数后,驱动不再读服务端时区,也就绕开了这个错。
allowPublicKeyRetrieval=true 和 MySQL 8 的 caching_sha2_password 认证有关,首次认证时需要从服务端获取公钥,驱动出于安全考虑默认不允许。这个参数放在本地和内网环境没什么风险,放到公网环境要慎重,第 5 章会展开。
username 和 password 对应 2.2 里建的 springapp 账号。密码如果以纯数字开头,YAML 解析时会转成数字类型导致认证失败,用双引号包住字符串最省事。
3.3 用 JdbcTemplate 跑通增删改查:RowMapper、自增主键与 DDL
配置完成后的最小验证是建一张表,再用 JdbcTemplate 做一次插入和查询。表结构 DDL:
CREATE TABLE IF NOT EXISTS t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, email VARCHAR(128) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;DDL 放在 resources/schema.sql,配合 spring.sql.init.enabled=true 可以在启动时自动执行,适合开发环境。生产环境不要开自动执行,表结构变更应该走迁移工具。ENGINE 和 CHARSET 一定要写:漏了 ENGINE 会用默认引擎,漏了 CHARSET 会继承库级字符集,这两项显式写出来才可预期。
然后是一个最简 DAO:
@Repository public class UserDao { private final JdbcTemplate jdbcTemplate; public UserDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public User findById(Long id) { return jdbcTemplate.queryForObject( "SELECT id, name, email FROM t_user WHERE id = ?", (rs, rowNum) -> new User( rs.getLong("id"), rs.getString("name"), rs.getString("email") ), id ); } public long insert(User user) { KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement( "INSERT INTO t_user(name, email) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS ); ps.setString(1, user.getName()); ps.setString(2, user.getEmail()); return ps; }, keyHolder); Number key = keyHolder.getKey(); if (key == null) { throw new IllegalStateException("insert failed: no generated key"); } return key.longValue(); } public int updateEmail(Long id, String email) { return jdbcTemplate.update( "UPDATE t_user SET email = ? WHERE id = ?", email, id ); } }先看 findById。queryForObject 的第一个参数是 SQL,第二个是 RowMapper,第三个是可变参数。RowMapper 用 lambda 写法,(rs, rowNum) -> new User(...),这里 rs 是结果集,rowNum 是行号,习惯上只取 rs。查询不到数据时 queryForObject 会抛 EmptyResultDataAccessException,接口层要处理成 404,这是新手最容易漏的。
insert 方法用 KeyHolder 取自增主键。核心是 prepareStatement 的第二个参数 Statement.RETURN_GENERATED_KEYS,没有它 MySQL 不会返回自增 ID,keyHolder.getKey() 就是 null。这是 JDBC 规范的标准能力,但不同数据库表现不同,MySQL 和 PostgreSQL 都支持,换成 Oracle 就是另一套写法。
updateEmail 返回 int 是影响行数。影响行数为 0 表示 id 不存在,不能当成更新成功,业务上要区分。所有 SQL 都用 ? 占位符,这是 PreparedStatement 防注入的基本姿势,字符串拼接 SQL 的做法在 JDBC 直连方案里属于直接否决项。
再配一个 Controller 快速验证:
@RestController public class UserController { private final UserDao userDao; public UserController(UserDao userDao) { this.userDao = userDao; } @GetMapping("/user/{id}") public User getUser(@PathVariable Long id) { return userDao.findById(id); } }启动后在浏览器或 curl 里访问 GET /user/1,能返回 JSON 就说明 JDBC 链路已经通了。如果你习惯 MyBatis-Plus 用实体类生成建表 SQL,JDBC 直连就得自己手写 DDL,这也是很多人从 ORM 转回 JDBC 时最不适应的一点,没有任何办法绕开,只能接受。
4. 连接池与 MySQL 侧参数配合:高并发下不崩的 4 组数字
连接池是 JDBC 方案的性能核心。SpringBoot 2.0 起默认连接池就是 HikariCP,不用额外引入。这一章不讲“把每个参数都调一遍”,而是说清楚哪几个参数必须和 MySQL 侧的值对齐。
4.1 HikariCP 必调的 5 个参数
HikariCP 的默认值在绝大多数中小项目里是够用的,但生产环境有几个参数必须明确写出来,避免“用的是默认值”这个状态本身成为隐患。我常用的一组配置:
spring: datasource: hikari: pool-name: UserDbPool minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 600000 max-lifetime: 1500000 connection-timeout: 5000 connection-test-query: SELECT 1| 参数 | 默认值 | 本次配置 | 说明 |
|---|---|---|---|
| pool-name | 无 | UserDbPool | 监控和日志里识别连接归属 |
| minimum-idle | 10 | 5 | 空闲时保留的最少连接数 |
| maximum-pool-size | 10 | 20 | 池里最多同时 20 个连接 |
| idle-timeout | 600000 | 600000 | 10 分钟,闲超过就回收 |
| max-lifetime | 1800000 | 1500000 | 25 分钟强制换连接 |
| connection-timeout | 30000 | 5000 | 等不到连接最多等 5 秒 |
pool-name 很多人不设,我建议设。连接出问题时,SHOW PROCESSLIST 里每个连接会带这个名字,一眼看出是哪套应用在耗库,排查效率高很多。
maximum-pool-size 的常见估算公式是 (CPU 核数 × 2) + 每块磁盘的并行 IO 数。这个公式来自连接池领域经典论述,放到 MySQL 同样适用。20 个连接对单台 MySQL 已经是中等压力,别盲目调到 100。连接越多,MySQL 的线程切换和内存开销越大。多实例部署时还要做乘法:三个应用实例每个 20 连接就是 60,加上监控和备份工具,已经接近 MySQL 默认 max_connections 的一半。上线后发现连接不够用,先看慢查询而不是加连接数,这是容易踩的思维惯性。
max-lifetime 和 idle-timeout 的关系是硬约束:max-lifetime 必须小于 MySQL 的 wait_timeout(默认 28800 秒 = 8 小时)。否则会出现 MySQL 把空闲连接杀掉,连接池还在用,发请求时报 Communications link failure。常见的可靠组合是 max-lifetime 25 分钟,idle-timeout 10 分钟。
connection-timeout 是应用侧等连接的限时。高并发瞬间超出连接池上限时,请求会在这里排队,超过 5 秒抛 SQLTransientConnectionException。设置太短会让正常排队被误杀,太长会让故障响应变慢,5 秒是个折中值。生产上如果频繁看到这个超时,说明 maximum-pool-size 确实不够,或者有慢查询把连接占死了。
connection-test-query 在 MySQL 8.0 驱动上其实不需要配,HikariCP 默认用 JDBC4 的 isValid() 做连接存活校验,比 SELECT 1 更轻。我保留这个配置是为了兼容某些公司网络里中间网关会掐断空闲连接的场景,普通项目可以删掉。
4.2 MySQL 侧 wait_timeout、max_connections 与字符集对齐
连接池参数不是单方面定的,要看 MySQL 服务端能接受什么。先查当前服务端配置:
SHOW VARIABLES LIKE 'wait_timeout'; SHOW VARIABLES LIKE 'interactive_timeout'; SHOW VARIABLES LIKE 'max_connections'; SHOW VARIABLES LIKE 'character_set_server';wait_timeout 是服务端关闭空闲连接的阈值,默认 28800 秒。interactive_timeout 是命令行客户端场景下的阈值,一般和 wait_timeout 一起改。max_connections 默认只有 151,如果连接池最大 20,三个实例就是 60,再加监控、备份、运维工具,很容易逼近上限。
我一般会在 my.ini 里把这几项写成显式配置:
[mysqld] max_connections = 500 wait_timeout = 28800 interactive_timeout = 28800 character-set-server = utf8mb4 collation-server = utf8mb4_unicode_cimax_connections 设 500 对大多数业务足够,不要盲目上千,每个连接都要占用线程栈和缓冲区内存。改完后用 SHOW STATUS LIKE 'Threads_connected' 观察实际连接数,确认离上限还有余量。注意改 my.ini 后要重启 MySQL 服务才生效,在 MySQL 8 里用 SET GLOBAL 改的变量重启后会还原。
字符集这块常见误区是只设置库表字符集,忽略连接字符集。SpringBoot 端 URL 里 characterEncoding=utf8,MySQL 端 server 字符集设 utf8mb4,这套组合下大多数中文场景不会有问题。出现乱码时,按照库、表、连接三个层级逐个查,哪一级还是 latin1 就从哪一级改。注意修改字符集不会自动转换已有数据,ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 这类操作要在数据迁移方案里单独评审,直接在生产执行容易把索引和排序规则搞乱。
5. JDBC 连接 MySQL 的 5 条避坑记录:每一条都是线上真实翻车现场
前面把正向配置讲完了,这一章写排错。以下 5 条问题按出现频率排,每一条都是现象、原因、解决三步,也都是我实际处理过的线上事故。
5.1 驱动类找不到:两个类名的错位
现象:SpringBoot 启动或第一次发请求时抛 ClassNotFoundException: com.mysql.jdbc.Driver,或者 “No suitable driver found for jdbc:mysql://...” 。
原因:驱动类名写错。MySQL 8 之后的驱动主类已经改成 com.mysql.cj.jdbc.Driver,老的 com.mysql.jdbc.Driver 在 8.x 驱动里虽然还保留着,但很多环境里已经被移除,尤其是 mysql-connector-j 新坐标的驱动包。
解决:driver-class-name 统一写成 com.mysql.cj.jdbc.Driver。如果项目里同时存在老坐标和新坐标的驱动,用第 2 章的方法查依赖树,把其中一个排除掉。还有一个隐蔽情况:SpringBoot 自动装配其实不要求你写 driver-class-name,它可以从 URL 里推断。写了反而容易因为写错而启动失败。我的做法是 URL 写对,driver-class-name 可写可不写,这句是经验,不是官方要求。
5.2 时区报错与中文乱码:serverTimezone 和 characterEncoding 的连带问题
现象:连接时报 “The server time zone value '...' is unrecognized”,或者查询结果里的中文全部变成问号。
原因:第一个是服务端 time_zone 变量没设,驱动拿不到可识别的时区;第二个是字符集链路断裂,可能是库表是 utf8mb4 但连接 characterEncoding 没写,也可能是连接字符集对了但字段还是 latin1。
解决:URL 加 serverTimezone=Asia/Shanghai,这是最直接的办法。字符集从三个层级确认:库表 DDL 里 CHARSET=utf8mb4,连接 URL 里 characterEncoding=utf8,必要时在建连接后执行 SET NAMES utf8mb4。排查时用 SHOW VARIABLES LIKE 'character_set_database'; 和 SHOW CREATE TABLE t_user; 看实际值,不要靠猜。
5.3 连接半夜被断开:wait_timeout 与 maxLifetime 谁先动手
现象:早上第一个请求报 Communications link failure,错误信息里有 “The last packet successfully received from the server was ... milliseconds ago”。
原因:MySQL 的 wait_timeout 默认 8 小时,连接空闲超过阈值被服务端关闭。连接池不知道这回事,把已死连接继续发给应用。如果 HikariCP 的 max-lifetime 默认 30 分钟,其实小于 8 小时,理论上不该出问题;问题多半出在有人手动把 wait_timeout 调低了,或者连接经过了网络层 NAT 老化。
解决:两个值对齐,max-lifetime 设成小于服务端 wait_timeout 的值。同时在连接池参数里保留连接有效性校验。另外有个容易被忽略的点:定时任务在半夜跑批时,用的可能是长期空闲的连接,建议批处理任务前先手动 jdbcTemplate.execute("SELECT 1") 做一次心跳,成本低收益大。
5.4 Public Key Retrieval is not allowed:MySQL 8 认证策略默认不给公钥
现象:启动直接报 “Public Key Retrieval is not allowed”。
原因:MySQL 8 默认认证插件是 caching_sha2_password,首次认证需要获取服务端公钥来加密密码,驱动为了防止中间人攻击,默认不允许自动拉取公钥。
解决:内网环境在 URL 加 allowPublicKeyRetrieval=true。另一个办法是把账号改回 mysql_native_password:
ALTER USER 'springapp'@'%' IDENTIFIED WITH mysql_native_password BY 'AppPassword123';注意 MySQL 8.4 已经把 mysql_native_password 标记为废弃,新装环境建议走 allowPublicKeyRetrieval 路线,而不是为了绕错去改认证插件。外网生产环境如果担心公钥拉取风险,应该用 SSL 连接方案,这属于安全专项,不在这里展开。
5.5 参数没问题还是慢:先查锁等待再查索引
现象:连接池调完了,接口还是几百毫秒甚至超时,但 CPU 不高,连接也没断。
原因:连接参数背了锅,真正的瓶颈是 SQL 或锁。常见的两类:查询没走索引导致全表扫描;写操作被行锁或间隙锁阻塞。在 MySQL 锁的分类里,行锁和间隙锁造成的等待最容易在并发更新场景出现,两个事务互相等对方的资源。
解决:先用 EXPLAIN 看执行计划:
EXPLAIN SELECT id, name, email FROM t_user WHERE email = 'test@example.com'\G再看当前有没有锁等待:
SELECT * FROM sys.innodb_lock_waits\G SHOW PROCESSLIST;EXPLAIN 里 type 字段是 ALL 表示全表扫描,重点看 key 字段有没有命中索引。sys.innodb_lock_waits 能看到谁在等锁、谁持锁,定位到具体事务。这一套走完再回头看连接池参数,你会发现大多数所谓的“连接池问题”其实是 SQL 问题,连接池只是背锅侠。
6. 验证连接是否真的稳:事务回滚与批量写入的实测套路
最后给一个我每接手新环境都会做的验证套路。连接池参数配完,光看日志“启动成功”不算数,必须用一组带事务和批量的测试确认连接状态和回滚行为。
6.1 用事务回滚验证连接是否真的可用
@Service public class AccountService { private final JdbcTemplate jdbcTemplate; public AccountService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Transactional(rollbackFor = Exception.class) public void transfer(Long fromId, Long toId, int amount) { jdbcTemplate.update( "UPDATE t_account SET balance = balance - ? WHERE id = ?", amount, fromId ); jdbcTemplate.update( "UPDATE t_account SET balance = balance + ? WHERE id = ?", amount, toId ); if (amount > 1000) { throw new RuntimeException("触发回滚"); } } }测试方法是先插入两条带余额的记录,再调用 transfer 传入大于 1000 的金额,事务会抛异常回滚。执行后两条记录的余额如果都没变,说明事务注解生效、连接状态可靠;如果出现一条扣了一条没加,说明事务没生效,要检查有没有漏加 @EnableTransactionManagement,或者事务方法被同类内部调用绕过了代理。
6.2 批量写入观察连接池的真实水位
第二个验证是批量写入,目的是观察连接池在高频请求下的实际水位:
int[] deltas = new int[]{ -10, -20, -30 }; Long[] ids = new Long[]{ 1L, 2L, 3L }; jdbcTemplate.batchUpdate( "UPDATE t_account SET balance = balance + ? WHERE id = ?", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setInt(1, deltas[i]); ps.setLong(2, ids[i]); } @Override public int getBatchSize() { return ids.length; } } );batchUpdate 的优点是一条网络往返处理多条语句,比循环单条 update 快一个数量级。setValues 里通过 i 索引取对应参数,getBatchSize 告诉驱动这一批有多少条,这两个回调是具体实现入口,别漏。
压测时打开另一个终端循环执行 SHOW PROCESSLIST,观察 pool-name 对应的连接数,应该在触达 maximum-pool-size 之后稳定不再上涨;请求高峰期结束后,连接数回落到 minimum-idle 附近。如果看到连接数反复震荡,说明 minimum-idle 配得太高或池参数和业务流量不匹配。
我有个习惯:每到一个新环境,第一件事永远是查 wait_timeout 和 max-lifetime 有没有打架,然后跑一遍上面的转账回滚用例。这套验证做过一次,后面半年基本不用再碰数据源配置。JDBC 直连看起来原始,但它把每一层都摊开在明面上,出问题时能直接定位,这也是我长期用它而不是过度封装 ORM 的原因。希望帮到你。
本文还有配套的精品资源,点击获取