先说个结论:Spring Boot 应用启动阶段“连 MySQL 很慢”,十次里有八次不是 MyBatis-Plus 的问题,卡住的反而是 JDBC 连接建立、连接池初始化或 MySQL 侧握手。有人把关键词从 mybatis-plus 改成 mybatis-plush,搜索引擎都迷惑了,问题依然还在。今天我把这条链路从头拆到尾:启动日志怎么看、连接池到底干了什么、MySQL 为什么迟迟不回包,最后给出一套能直接抄的配置,覆盖 DNS 反查、TCP 超时、认证握手和连接池预热这几个最常见的坑。
这套内容适合正在被“Service 启动 20 秒、其中 17 秒都在等数据库”困扰的开发者,也适合刚接触 Spring Boot 和 MyBatis-Plus 的新人。我会尽量把每一步解释清楚,而不是丢给你一堆参数让你硬背。
1. 先把“慢”钉死在具体环节
1.1 启动日志里最值得看的两行时间戳
排查这个问题,第一步不是看 Spring Boot 启动 Banner,也不是打开 MyBatis-Plus 的 SQL 日志,而是找到 HikariCP 这两行日志:
2025-01-05 09:15:01.203 INFO [main] com.zaxxer.hikari.HikariDataSource : biz-pool - Starting... 2025-01-05 09:15:01.478 INFO [main] com.zaxxer.hikari.HikariDataSource : biz-pool - Start completed.如果你的日志长这样,两行之间只差 275ms,说明数据库连接层基本没问题。真正的问题可能在其他 Bean 初始化,或者某个启动阶段执行的 SQL 上。
如果两行之间差出 5 秒、10 秒甚至更久:
2025-01-05 09:15:01.203 INFO [main] com.zaxxer.hikari.HikariDataSource : biz-pool - Starting... 2025-01-05 09:15:21.198 INFO [main] com.zaxxer.hikari.HikariDataSource : biz-pool - Start completed.这就说明问题很可能出在“物理连接建立”这个过程上。你要先盯住这个区间,再去改配置,而不是在 MyBatis-Plus 的 XML 映射文件里浪费时间。
1.2 把“连接慢”拆成三种类型
我在实际排查时会把现象拆成三类,避免被表象带偏:
| 类型 | 典型表现 | 责任方 |
|---|---|---|
| 连接建立慢 | Hikari 的 Starting 和 Start completed 之间差距大 | JDBC 驱动、网络、DNS、MySQL 服务器 |
| 首次查询慢 | 连接池初始化很快,但第一个 Mapper 方法调用卡顿 | 懒加载连接、首次 SQL 编译、认证握手延迟 |
| 整体启动慢 | 池日志看着正常,但 Started Application 前面那段日志消失了 | MyBatis-Plus 扫描、@PostConstruct 代码、多数据源初始化 |
很多开发者只在日志里看到整体启动时间变长,就下意识认为是“MyBatis-Plus 连数据库慢”。其实要先定位到具体区间,才知道该往哪个方向查。把这三种类型对照着看,后续排查会少走很多弯路。
2. 逐层复盘:为什么连接就是快不起来
2.1 DNS 反查是第一大嫌疑人
MySQL 在收到客户端 TCP 连接后,默认会对客户端的 IP 做一次反向 DNS 解析,目的是和用户表里的 host 字段做匹配。如果解析畅通,几百毫秒内就能完成;如果内网 DNS 查不到记录、解析超时或者请求被防火墙拦截,连接就会一直悬在那里,表现就是“明明 MySQL 活着,但应用启动时连库要等十几秒”。
这个问题在容器和 Kubernetes 环境里尤其常见。客户端 IP 是动态分配的,根本不存在对应的 PTR 记录,MySQL 会一直等到 DNS 查询超时。你从应用容器里ping数据库地址没问题,直接命令行连 MySQL 也正常,但应用启动就是慢,原因往往是双方不在同一个网络命名空间下,DNS 响应被放大了。
最直接的验证方法是在 MySQL 服务器上执行:
SHOW GLOBAL VARIABLES LIKE 'skip_name_resolve';如果结果是OFF,而你的客户端 IP 无法反解,那基本就可以锁定这个原因了。解决办法很简单,在my.cnf的[mysqld]段加上:
skip_name_resolve注意:开启后,MySQL 的用户授权就不能再用'user'@'some.hostname'这种域名形式,只能使用 IP 或通配符,若线上账户是按域名分配的,改之前要提前核对好权限配置。
2.2 JDBC URL 里缺了连接超时参数
排查过几十次这类问题后,我基本会把“连接慢”分成两类:一类是网络环境复杂导致的慢,另一类是压根没设置超时导致的“慢性死亡”。
很多人的 JDBC URL 长这样:
jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8没有connectTimeout,也没有socketTimeout。JDBC 驱动和 TCP 层在连接失败或握手缓慢时,会按照操作系统的 TCP 超时策略重试,默认可能需要几十秒甚至一分钟。
我的习惯是至少补上这两个参数:
jdbc:mysql://127.0.0.1:3306/demo?connectTimeout=3000&socketTimeout=60000connectTimeout=3000表示 TCP 连接建立最多等 3 秒,连不上就直接抛异常,而不是让应用傻等。socketTimeout=60000表示建立连接成功后,单次读写操作最多等 60 秒,避免某个慢 SQL 把连接挂死。
这里有个容易搞混的点:HikariCP 里的connection-timeout其实是“从连接池获取一条连接的最大等待时间”,不是“JDBC 建立物理连接的等待时间”。很多人只改了 Hikari 的connection-timeout,但 JDBC URL 里的connectTimeout没改,问题依然存在。
2.3 认证握手与 SSL 的开销被低估了
MySQL 8 默认使用的caching_sha2_password认证插件,在非 SSL 连接下第一次握手需要通过 RSA 公钥交换来验证密码。如果应用所在网络到数据库的延迟较高,或者驱动版本太老、没启用公钥获取机制,握手阶段就会多出好几次网络往返。
常见的情况是,连接不是建立不起来,而是“每次建立都要多几百毫秒”。单独看一次无所谓,但连接池启动时要创建多条连接,累加起来就能砸出几秒的启动延迟。
如果是在内网或开发环境,且安全策略允许,可以考虑在 JDBC URL 中加入:
sslMode=DISABLED&allowPublicKeyRetrieval=truesslMode=DISABLED可以避免 SSL 握手的额外往返,allowPublicKeyRetrieval=true允许客户端从服务器获取 RSA 公钥。但这里必须提醒:生产环境是否允许禁用 SSL、是否允许明文传输,一定要以公司的安全规范为准,这条路适合本地排障,不适合无脑照抄。
2.4 连接池初始化策略影响启动速度
Spring Boot 2.x 之后默认使用 HikariCP,它对连接池的启动策略是保守的:默认会在池初始化时尝试获取一条连接,用于确认数据库可达。如果数据库响应慢,这条初始连接就会把启动过程拉长。
如果你想缩短启动时间,不是把maximum-pool-size调小,而是要检查两个参数:
minimum-idle:池中最小空闲连接数。如果设置成 2,启动时往往至少创建 2 条连接。initialization-fail-timeout:控制启动阶段获取初始连接的超时行为。保持默认即可,不建议为了“快”直接设成 -1,那样数据库挂了应用可能也不会快速失败。
另外一个非常容易被忽略的点:如果项目里配置了“多数据源”或者“动态数据源”,每一个 DataSource 都对应一个独立连接池,启动时都会执行一次初始化。连接池数量翻倍,启动耗时基本也是成倍增加。我之前接手过一个项目,四个数据源全在主类初始化阶段一起拉连接,启动直接慢到 40 秒。后来确认其中两个数据源根本不是所有模块都需要,改成按需装配后,启动时间立刻降下来了。
2.5 MyBatis-Plus 到底背不背这个锅
MyBatis-Plus 创建SqlSessionFactory时,主要是扫描 Mapper 接口、解析 XML 文件、初始化实体元数据。这一步通常不涉及数据库连接,不会因为表多、字段多而让 MySQL 连接变慢。
那为什么很多人觉得是 MyBatis-Plus 的问题?因为项目里很可能存在“启动阶段就调用了 Mapper”的代码。最常见的是在@PostConstruct、ApplicationRunner、CommandLineRunner里写了几行初始化逻辑,比如启动时加载菜单表、加载字典表、统计订单量。这些 SQL 一执行,连接池才真正开始创建物理连接,延迟就会落到“Spring Boot 启动”这个大时间段里。
排查方法很简单,看启动日志里“Started Application”前面的最后几步。如果能看到类似“加载字典表”“初始化权限数据”这类业务日志,那就不是连接池的问题,而是业务初始化 SQL 拖慢了整体启动,和 MyBatis-Plus 本身没什么关系。
3. 一步步定位问题,别凭感觉改配置
3.1 先用命令行量一遍“原始连接”耗时
把应用放一边,先在应用同一台机器上执行:
time mysql -h 127.0.0.1 -u demo -pdemo123 -e "select 1"如果这条命令本身就要 5 秒、10 秒,那问题一定出在 MySQL 侧或网络侧,应用侧怎么调连接池都没用。
如果想进一步确认 TCP 层情况,可以测一下端口连通时间:
time nc -vz 127.0.0.1 3306nc只做 TCP 握手,不走 MySQL 认证。如果nc很快但mysql命令行很慢,说明问题在握手/认证阶段;如果nc都慢,说明 TCP 链路本身就有问题,需要查防火墙、路由、安全组。
3.2 看 MySQL 侧的真实状态
连接慢不一定是 MySQL 在偷懒,也可能是一堆僵尸连接把线程池挤满了。登录 MySQL 执行:
SHOW GLOBAL STATUS LIKE 'Threads_created'; SHOW GLOBAL STATUS LIKE 'Connections'; SHOW GLOBAL VARIABLES LIKE 'connect_timeout'; SHOW PROCESSLIST;Threads_created如果特别大,说明有大量线程被频繁创建和销毁,MySQL 每次创建线程都要消耗内存和 CPU。借助performance_schema你还可以看到连接的耗时分布。
另一个细节:connect_timeout默认是 10 秒,它指的是 MySQL 服务器等待客户端完成认证握手的最长时间。网络状况很差时,应用侧connectTimeout=3000先断了,MySQL 还没等到超时,这个参数对“连接慢”的影响没有大家想的那么大,但也不妨看一眼。
3.3 打开 Hikari 和 JDBC 驱动日志
到了应用侧,把日志级别打开,可以帮助确认连接请求到底卡在哪一步:
logging: level: com.zaxxer.hikari: DEBUG com.mysql.cj.jdbc: DEBUGHikari 的 DEBUG 日志会打印连接创建、连接获取、连接释放的过程。JDBC 驱动的 DEBUG 日志能看到驱动是否在做认证、是否在做元数据读取。如果看不到明显异常,再用jstack <pid>抓一下线程栈,重点看main线程是否阻塞在SocketInputStream.read或SocketOutputStream.write这类方法上。
这一步适合做最后确认。大多数情况下,先用命令行和 MySQL 状态就能定位到 80% 的问题,不必每次都上线程转储。
4. 一套可以直接上手的优化配置
4.1 JDBC URL 的最终形态
推荐把连接串写成这样,至少覆盖connectTimeout、socketTimeout、时区和公钥获取这几个点:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&connectTimeout=3000&socketTimeout=60000&useSSL=false&allowPublicKeyRetrieval=true注意我把localhost写成了127.0.0.1。Java 解析localhost时可能先尝试 IPv6 地址::1,如果环境关闭了 IPv6,解析超时会再退回 IPv4,白白浪费几秒。开发环境直接用127.0.0.1最省事。
4.2 HikariCP 连接池参数
spring: datasource: hikari: pool-name: biz-pool minimum-idle: 2 maximum-pool-size: 10 connection-timeout: 3000 validation-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000解释一下这些参数的实际作用:
minimum-idle: 2:保证池里至少预留 2 条空闲连接,避免首次请求时才现建连接。maximum-pool-size: 10:对一个常规业务服务来说已经足够,不需要动不动就设 20、30。connection-timeout: 3000:应用线程从池中拿不到连接时最多等 3 秒,之后直接报错,不让业务无限阻塞。max-lifetime: 1800000:连接最长存活 30 分钟,需要小于 MySQL 的wait_timeout,否则 MySQL 侧已把连接断开,连接池却还在复用。
很多人漏掉max-lifetime,结果白天业务量上来以后出现偶发连接异常,查半天才发现是连接被 MySQL 服务端提前关闭了。连接池参数不只是管启动,也会影响运行期的稳定性。
4.3 MySQL 服务端参数调整
如果确认是 MySQL 一侧的问题,可以在my.cnf里做以下调整:
[mysqld] skip_name_resolve connect_timeout = 10 wait_timeout = 28800 interactive_timeout = 28800 max_connections = 300skip_name_resolve的优先级是最高的,它从根源上避免了 DNS 反查。max_connections不要拍脑袋调成几千,连接数是需要内存的,设置过大在并发不高时反而浪费资源。
改完my.cnf记得重启 MySQL,并用下面的命令确认:
SHOW GLOBAL VARIABLES LIKE 'skip_name_resolve';4.4 系统与容器层面的优化
如果应用跑在容器里,检查一下 JVM 是否启用了 IPv6 解析优化。可以在启动命令中加入:
-Djava.net.preferIPv4Stack=true这个参数和 JDBC URL 里用127.0.0.1是一套组合拳,能明显减少因为 DNS、IPv6 解析导致的额外等待。
另一个容易忽略的优化点是 Mapper 扫描路径。MyBatis-Plus 扫描时如果写成:
@MapperScan("com.example.**.mapper")扫描范围过大,启动期会做大量无意义的类加载和资源扫描。虽然这不是“连接 MySQL 慢”,但会叠加在启动耗时上,让你误判为数据库问题。建议把包路径尽量写精确,比如:
@MapperScan("com.example.module.user.mapper")5. 常见问题速查与实操经验
5.1 一个速查表,按现象对号入座
| 现象 | 可能原因 | 优先处理方式 |
|---|---|---|
| Hikari Starting 到 Start completed 卡十几秒 | DNS 反查、网络握手慢 | MySQL 开启skip_name_resolve,JDBC URL 加connectTimeout |
| 启动时报连接超时,但 MySQL 单独连没问题 | 防火墙、安全组拦截或驱动版本过旧 | 先测nc -vz,再升级驱动 |
| 连接建立快,但首次执行 SQL 很慢 | 连接池懒加载、首次 SQL 编译慢 | 适当调大minimum-idle做预热 |
| 整体启动慢,Hikari 日志正常 | 启动阶段业务代码执行 Mapper SQL | 检查@PostConstruct、ApplicationRunner |
| 多数据源项目启动翻倍变慢 | 多个连接池同时初始化 | 不常用的数据源改为按需装配 |
| 偶发“连接已被关闭”异常 | 连接池max-lifetime大于 MySQLwait_timeout | 把max-lifetime调到wait_timeout以内 |
| 本地开发环境连库慢 | 使用localhost触发 IPv6 解析 | 改127.0.0.1,加-Djava.net.preferIPv4Stack=true |
5.2 再分享几条个人经验
排查这类问题,我的顺序是先做减法再做加法。先去掉一切慢表因素,比如 DNS 反查、IPv6 解析、SSL 握手,把连接时间压到几百毫秒以内,再回头审视连接池参数是否需要调大。
另一点是不要盲信“连接池越大越好”。很多应用并发量并不高,却把maximum-pool-size设成 50,结果 MySQL 侧线程数量虚高,连接创建和销毁开销反而拖慢启动和运行。连接池大小应该从业务并发、数据库吞吐和响应时间出发,而不是拍脑袋。
最后补充一个容易踩的坑:如果你用了useSSL=false这种方式来加速,一定要确认团队安全规范是否允许。本地开发可以这么干,但生产环境连接是否加密,必须由负责安全的人拍板,否则后面审计会很被动。排查慢连接时临时禁用加密没问题,不要在没评估的情况下把它留给生产。
这个项目后续如果再遇到“启动慢”,我建议你先保留一份启动日志的时间戳基线,改一次配置就对比一次。用“Hikari 两行日志之间的耗时”作为核心指标,比盯着整段启动时间可靠得多。有了基线,再做参数调整和方案验证都会更从容。