恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
MySQL连接池爆满排查实战:从现象到根因的完整解决路径
首页
资讯中心
/
MySQL连接池爆满排查实战:从现象到根因的完整解决路径
MySQL连接池爆满排查实战:从现象到根因的完整解决路径
发布时间:2026/10/3 14:27:25
凌晨两点半告警电话准时响起。接起来就一句话“支付系统挂了MySQL连接池全满了新请求全部超时。”屏幕那头一连串“Cannot get a JdbcConnection”的报错刷过去DBA已经在查processlist业务方群里已经开始问“到底是SQL慢还是连接池配置错了”。这类问题每个月几乎都会上演一次。连接池爆满听起来是个老生常谈的MySQL故障但每次排查起来根因却五花八门——有时候是代码泄漏连接有时候是慢SQL堆积有时候是连接池和数据库配置不匹配还有时候干脆是数据库本身的连接数被打满。这篇文章把我实际排查过、处理过的连接池爆满案例完整拆一遍从现象、排查路径、根因定位到最终解决方案一次性说透。不管你是刚接手Java后端的初中级开发还是已经被线上问题折腾过几轮的资深工程师这条排查思路和那些坑多少能帮你少走几次弯路。1. 连接池爆满的本质与典型现象1.1 连接池到底是怎么“爆”的先说个基础认知。数据库连接池的本质就是一堆MySQL连接的“蓄水池”。应用启动时预先创建一批连接放在池子里业务线程要操作数据库时不是去新建连接而是从池子里借一条用用完再还回去。池子里的连接数量是有限的这个上限就是连接池的maxPoolSize或者maxActive。连接池爆满的意思很简单所有连接都被借走了一个空闲的都不剩。这时候新的请求线程过来只能阻塞在“等连接”这一步上。连接池通常会有一个最大等待时间比如HikariCP的connectionTimeout默认30秒Druid的maxWait常见配置是10秒。等不到连接就抛异常异常再往上抛就变成了业务接口的5xx。但这里有一个容易忽略的点连接池只是一个入口真正的瓶颈往往在池子背后的MySQL服务端。连接池爆满可能有两种完全不同的层次应用层连接池满了池子里的连接确实都被业务线程占用但MySQL实际收到的查询压力未必有那么大数据库层连接数满了MySQL的max_connections被占满连管理端都登录不进去。两侧是联动的。应用层连接池的maxPoolSize通常远小于MySQL的max_connections比如池子配50MySQL上限配500。正常情况下即使池子满MySQL侧也还有余量。但如果你配置的比例失调或者多个应用实例同时打一个库就会出现“应用连接池报错、MySQL侧也满了”的双重故障。1.2 爆满时的第一现场长什么样遇到连接池爆满第一时间要抓取的就是“现场证据”。我通常按这个顺序去收集第一应用日志。那是最直观的。HikariCP的报错典型长这样java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30004ms.Druid的报错长这样com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10000, active 50, maxActive 50, creating 0.这里有个关键信息active 50, maxActive 50。active代表当前正在使用的连接数maxActive是上限。两者相等说明池子里的连接全部被占用了没有连接到泄漏但所有连接都被事务或者长查询占着。第二MySQL侧的快照。登录数据库如果还能登录的话执行SHOW FULL PROCESSLIST;这个命令会列出所有会话的完整状态。重点关注State列如果大量会话卡在“Waiting for handler commit”说明有大批事务没提交如果大量“Sending data”说明有大查询在扫数据如果大量“Sleep”说明连接借出去之后没有及时归还代码里有连接泄漏。第三连接来源统计。用下面的SQL快速聚合一下连接从哪来、在干什么SELECT db, user, host, command, state, COUNT(*) FROM information_schema.processlist GROUP BY db, user, host, command, state ORDER BY COUNT(*) DESC;这样一眼就能看到是否有某个应用实例异常地占用了大量连接比如某个服务有10个节点正常每节点30个连接突然发现其中一个节点占了300个连接问题大概率就在这个节点上。1.3 先做“止血”还是先做“排查”很多人一上来就急着改配置把maxPoolSize调大或者直接重启应用。这确实是“止血”的手段但几十个线上案例告诉我连接池爆满的时候盲目扩容往往会让数据库更快地被打死。因为连接池里的每条连接都是一个对MySQL的活跃会话会话越多MySQL需要分配的thread、排序缓存、临时表资源就越多。如果瓶颈本来就在数据库侧你把池子调大只会让数据库更早达到资源上限。正确的“止血”顺序是先杀掉MySQL侧明显的异常会话比如长时间Running的慢查询、长时间Sleep但事务未提交的连接再确认是不是某个接口被打爆导致连接被占满如果是能限流就先限流最后才考虑调整连接池参数或者重启应用。也就是说恢复优先于根因定位。先把访问恢复再把证据留下来慢慢查。2. 排查思路从“猜”到“定位”的完整路径2.1 沿着连接的生命周期拆解连接从应用到数据库路径大概是这样的业务代码发起查询 → 从连接池借连接 → 执行SQL → 事务提交/回滚 → 归还连接到连接池 → 连接池与MySQL之间的实际网络会话可能还保持着。连接池爆满说明在这条路径的某一环卡住了。我把常见的“卡住点”整理成了一张排查对照表排查方向关键证据结论业务线程阻塞线程dump大量线程卡在“getConnection”上连接被借空线程池也可能被打满连接泄漏MySQL侧大量Sleep连接应用日志无SQL报错代码里拿连接没归还长事务processlist有大量“Waiting for handler commit”且trx_started时间很久事务开启未提交连接被长事务占住慢SQL堆积processlist出现大量“Sending data”或“Sorting result”单条SQL执行时间超过数秒SQL执行太慢后续请求全部排队等连接连接池参数不合理active0或很小但连接还不断创建失败池子本身配通配或者恢复连接过慢这个表格基本覆盖了绝大多数连接池爆满的根因但实际的排查顺序不是表上这个顺序而是反过来先排查最严重、影响面最大的问题。2.2 第一步抓线程栈还是抓连接快照有人习惯先分析线程dump看Java线程都在干嘛。这没有错但在我自己的经验里线程dump在连接池爆满时适合观察“等连接”的线程有多少却不适合定位“谁占着连接不还”。因为你只看到了一堆线程在干等却看不到池子里的连接到底被哪些业务执行着。更有效的做法是双管齐下线程dump用来确认“等连接”线程的数量与调用栈快速定位是哪个业务入口引发的洪水MySQL侧的processlist用来确认“占用连接”的具体SQL和会话来源。比如有一次我通过线程dump发现卡在getConnection的线程全部来自一个订单导出接口再翻MySQL侧的processlist发现有一批大查询在跑“SELECT * FROM orders WHERE create_time BETWEEN ...”单条查询跑了快两分钟。因果链一下闭环了导出接口的SQL没加时间索引全表扫描拖满连接后续请求全在池子外排队。所以这个阶段的核心任务不是猜而是把“等连接的线程”和“占连接的会话”用同一个业务维度关联起来比如同一个接口、同一张表、同一个账号。2.3 第二步打开performance_schema看历史会话如果故障已经发生你发现MySQL侧processlist里已经没有了那些慢查询——比如连接被自动断开、应用重启了现场被清理掉了这时候怎么查答案是performance_schema。MySQL自带的这套性能监控表里events_statements_history_long会在内存中保留较长的SQL执行历史。前提是你在故障前开启了相关采集项SELECT THREAD_ID, EVENT_NAME, SQL_TEXT, ROWS_EXAMINED, TIMER_WAIT FROM performance_schema.events_statements_history_long ORDER BY TIMER_WAIT DESC LIMIT 20;TIMER_WAIT的单位是皮秒除以1000000000000就是秒。这个查询能把“刚才哪个SQL占用的扫描行数最多、执行时间最长”拉出来。还有一张关键表sys.session。它聚合了processlist和事务信息能直接看到每个会话的事务开启时间和当前状态SELECT * FROM sys.session WHERE command ! Sleep ORDER BY time DESC;这样能快速发现事务跑了很久没提交的会话。如果一个会话的Time是800多秒而且State是“Waiting for handler commit”那基本可以断定这个会话拖着一个长事务连接被它锁死了。2.4 第三步两个常见误判要避开排查过程中有两个特别容易掉进去的坑第一个只看应用日志不看MySQL侧。应用日志只会告诉你“连接池拿不到连接”但真正的原因是数据库侧连接被占满还是应用侧连接没有被释放单靠应用日志判断不了。必须结合MySQL侧的会话快照一起看才能分清上下游。第二个一看到慢SQL就以为是SQL的问题。慢SQL确实是连接池爆满最常见的导火索但有时候SQL本身不慢是锁等待让SQL“看起来慢”。比如一个UPDATE语句单条执行只要几十毫秒但遇到行锁冲突时它会卡在“Statistics”或者“updating”状态好几秒。一条锁等待的SQL背后往往是另一个持锁未提交的事务。所以看到慢SQL要多看一眼“当前是否有其他会话在写同一行数据”。我看到过不止一次开发同学一查出来是某条UPDATE慢就急着加索引、改SQL结果那条SQL本来就走了主键慢的根源是一个没提交事务的会话一直持着行锁。排查锁问题可以用这个查询SELECT * FROM performance_schema.data_lock_waits\G或者更简单看processlist有没有多个会话的State同时卡在“updating”或“Waiting for handler commit”。3. 五大典型根因与实战定位方法3.1 根因一慢SQL把连接占满这是最常见的一种。慢SQL的可怕之处在于它在批量到达时并不是“一条把连接占完”而是“每条占一段时间累积起来让连接周转不过来”。假设你的连接池有50条连接正常每个查询50ms一秒内一条连接能处理20个请求50条连接一秒能处理1000个请求。但如果某条SQL因为缺索引从50ms退化到5秒一条连接一秒只能处理0.2个请求50条连接一秒只能处理10个请求。吞吐量直接跌到原先的1%。排查这类问题的方法很直接在processlist里用Time字段倒序找出执行时间最长的SQL然后用EXPLAIN分析执行计划。通常是没走索引、扫描行数巨大、排序内存溢出。举一个实际案例。一个后台系统的列表页上线后跑了三个月都没事某天突然连接池爆满。MySQL侧快照显示所有连接都在执行同一条SQLSELECT * FROM user_login_log WHERE login_time BETWEEN 2024-03-01 00:00:00 AND 2024-03-31 23:59:59 ORDER BY login_time DESC LIMIT 20;问题很明显user_login_log表数据量已经增长到千万级而login_time上并没有索引。EXPLAIN的结果是typeALLrows800万。加一个idx_login_time(login_time)索引后SQL从全表扫描变成索引范围扫描连接池立刻恢复。教训很朴素SQL执行时间才是连接池健康度的核心指标。连接池参数配得再好一条全表扫描也能让池子塌掉。治理连接池首先要治理慢SQL。3.2 根因二连接泄漏连接泄漏是另一种典型。代码里通过连接池获取了连接但因为异常分支没有写finally或者使用完后没有close导致连接一直保持“借出”状态。MySQL侧看这些连接是Sleep状态没在执行SQL但应用侧计数里它们就是“占用中”永远回不了池子。连接泄漏的特点是慢性的、逐渐积累的。上午一切正常下午开始偶尔报错到晚上高峰期彻底爆满——因为一天的泄漏量攒到了池子上限。排查连接泄漏最有效的方式是看监控。HikariCP支持暴露hikaricp_connections_active和hikaricp_connections_idle指标如果active持续走高、idle持续走低且应用没什么大查询就可以调高怀疑等级。代码侧可以用动态代理做一个“连接借用时间统计”比如记录连接被借出的时间戳归还时校验借用时长超过阈值就打印借用线程的栈。Druid自带了这个能力DruidDataSource的removeAbandonedtrue可以强制回收超时未归还的连接但要谨慎使用——它可能中断一个正常的长事务。我处理过一个最典型的泄漏场景Connection conn null; try { conn dataSource.getConnection(); // 执行SQL // 这里抛出了业务异常下面没执行 } catch (Exception e) { // 只打了日志没有关闭连接 log.error(query failed, e); }代码里没有finally块也没有try-with-resources。一个异常分支连接就永久泄漏了。这个接口虽然不是高频接口但每天会被调用几万次只要异常率超过万分之一积累几天后连接池必然被耗干。解决方案是统一改用try-with-resources写法try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 执行SQL }这样无论代码走到哪个分支连接都会被自动关闭归还。3.3 根因三事务没提交导致连接长期被占慢SQL和连接泄漏都比较容易发现事务不提交的问题则更隐蔽。一条连接一旦开启事务这个连接就被“绑定”了。事务没提交之前连接上执行的加锁操作会一直持有锁连接本身也不会被其他线程使用。典型的报错场景是这样的某个Service方法加了Transactional注解方法内部调用了一个耗时很长的远程接口或者一个循环里反复查库整个事务的占用时间被拉得极长。事务期间虽然只发了几条SQL但连接被独占的时间可能是SQL执行时间的几十倍。我在一次排查中见过最夸张的事务一个统计接口事务里有20次for循环内查询每次查询200ms总共4秒但因为方法里还调了一个第三方HTTP接口单次调用要2秒最后事务持有了快30秒。更麻烦的是30秒内如果另外有10个请求过来每个都要等这个事务释放连接连接池直接被打穿。排查事务问题时要结合两个维度看processlist里commandSleep或StateWaiting for handler commit但time很长information_schema.innodb_trx表里trx_started时间很长且trx_stateRUNNING。定位SQLSELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.innodb_trx WHERE trx_state RUNNING ORDER BY trx_started;再用trx_mysql_thread_id去processlist里找对应的线程ID就能定位到具体的应用会话。解决方向有两个层面接口层面把事务外的方法调用拆出去事务内只留必要的DB操作代码层面用TransactionTemplate代替声明式事务精确控制事务边界避免事务方法里夹带远程调用和耗时的IO操作。3.4 根因四连接池参数和数据库配置不匹配还有一种情况SQL不慢、代码也没泄漏、事务也都正常提交了但连接池还是爆。这种时候要开始怀疑参数匹配问题。典型的不匹配场景application连接的wait_timeout太短。数据库侧wait_timeout默认28800秒如果某个中间件或者DBA手动调成了60秒而应用连接池的空闲连接回收周期超过60秒就会出现“连接被数据库主动断开但应用侧还认为连接活着”。一个请求过来从池子里拿到这条失效连接执行SQL时才发现连接已断报错后连接被移除并重建。重建过程需要时间如果并发高新连接创建速度跟不上请求到达速度连接池就会表现为拿不到连接。连接池maxLifetime大于数据库wait_timeout。HikariCP的maxLifetime默认是1800000毫秒即30分钟官方建议必须比数据库的wait_timeout小几秒。如果数据库wait_timeout是60秒而maxLifetime是30分钟大量连接会被数据库提前切断。maxPoolSize配得过大。连接池上限设了200数据库max_connections只有500再多加两个应用实例就把库打满了。针对这类问题排查时用这个SQL验证数据库侧配置SHOW VARIABLES LIKE wait_timeout; SHOW VARIABLES LIKE max_connections;再对比应用的连接池配置就能定位不匹配点。我记得有个项目测试环境怎么压都正常一上生产就连接池爆满后来发现生产MySQL的wait_timeout被中间件团队调成了30秒而应用的HikariCP配置根本不知道这件事。所有空闲超过30秒的连接全被服务端断开客户端池里的连接全部“僵死”首次请求全部报连接异常系统进入“不断建连、不断断连”的死循环。3.5 根因五数据库连接数本身被占满最后一类是数据库层面的连接数满了。应用连接池爆满之前还有可能先出现“数据库max_connections被占满”。这时候不光业务系统报错DBA想登录上去执行命令都会失败。这类场景常出现在多套环境共用一个数据库实例的时候。比如测试环境的批量任务和生产的核心交易都连着同一个MySQL。测试那边一个全量数据清洗任务开了一堆连接生产连接池还没来得及反映数据库先满了。还有一个常见因素是长时间未提交事务累积出来的连接数。MySQL的max_connections限制的是server端的会话数每一条客户端连接都占一个。即使应用连接池配的是50但如果应用重启过旧的连接还残留在数据库层没有被释放——特别是一些没有properly配置TCP keepalive的网络环境连接断开后服务端要等到tcp_keepalive_time或者wait_timeout才能感知。碰到这种场景先尝试用管理通道登录mysql -uadmin -p -h127.0.0.1。如果不能登录就是用预留的管理连接SHOW STATUS LIKE Threads_connected;同时把连接数上限临时调大或直接清理SET GLOBAL max_connections 1000;再通过查询杀掉异常会话SELECT CONCAT(KILL , id, ;) FROM information_schema.processlist WHERE command Sleep AND time 300;生成kill语句后手动执行。注意不要一次性复制执行所有kill先确认那些长时间Sleep的会话里没有正常的长事务。4. 解决方案参数配置、代码规范、兜底手段4.1 连接池参数到底怎么配这里直接从HikariCP和Druid两个主流连接池的参数说起。配置没有银弹但有可遵循的原则。HikariCP的核心参数spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000maximum-pool-size池子最大连接数。不是越大越好经验公式参考核心数 * 2 有效磁盘数。如果应用实例是8核通常20到50都是合理区间。连接数过大会让数据库线程上下文切换开销暴增。minimum-idle空闲连接的下限。对于流量不均的服务建议不要保守设0预留几条空闲连接能避免突发流量瞬间建连的耗时。connection-timeout等待连接的最大毫秒数默认30秒。如果业务对延时有严格要求可以缩短到5到10秒让系统早失败而不是一直卡在等待。max-lifetime连接最大存活时间官方文档明确要求必须小于数据库wait_timeout留出几秒的余量。如果MySQL wait_timeout默认8小时这个值保持默认30分钟就合理。idle-timeout空闲连接回收时间必须小于max-lifetime。Druid的关键参数还多了几个spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 50 max-wait: 10000 test-while-idle: true validation-query: SELECT 1 time-between-eviction-runs-millis: 60000 remove-abandoned: true remove-abandoned-timeout: 180其中remove-abandoned是一个兜底手段不是日常依赖的手段。开启后Druid会强制回收超过180秒未归还的连接但对正常长事务会有误杀风险。我的建议是排查期间可以临时开启定位完问题后关掉靠代码审查和监控来根治。4.2 连接池监控与提前发现连接池爆满是典型的“可以提前发现、不应该等到爆发才知道”的故障。有监控体系和没监控体系处理时间差异巨大。Druid自带了StatFilter和监控页面DruidDataSource支持通过StatViewServlet暴露监控数据能看到当前active连接数、池中连接数、逻辑连接打开/关闭次数。其中logicConnectCount和logicCloseCount的差如果持续增长说明有连接泄漏。HikariCP则通过Micrometer集成了metrics数据可以直接打到Prometheusspring: datasource: hikari: metrics-enabled: true再加上micrometer-registry-prometheus依赖就能暴露hikaricp_connections_active、hikaricp_connections_idle、hikaricp_connections_pending、hikaricp_connections_timeout这些指标。我通常会为这几个指标配两条告警规则hikaricp_connections_active持续大于maximum-pool-size的80%超过5分钟hikaricp_connections_timeout总次数大于0哪怕只有1次也要告警。第一条防慢SQL第二条防连接池即将耗尽。连接池timeout一旦出现说明已经出现过等待连接超时的请求这时候通常还没完全雪崩是介入的最佳时机。4.3 代码层面的硬约束连接池爆满的根因里代码层面的问题至少占一半。治理代码层面的隐患我推广几条硬约束。硬约束一所有数据库操作必须使用try-with-resourcesJava环境或with语句禁止手动管理连接的关闭。用显式finally当然也可以但直接写try-with-resources能规避99%的“忘记close”问题。硬约束二事务方法内部禁止调用RPC或HTTP接口。事务期间的连接占用时长和不可控的远程调用时长强相关。远程调用慢事务就慢连接就被占住。事务方法只做和数据库相关的操作远程调用放在事务的外层。硬约束三禁止在循环里逐条执行SQL改为批量提交。循环执行100条单条INSERT一条连接要串行执行100次每次都有网络往返开销。如果用JDBC batch或者MyBatis的批量插入总耗时能从5秒降到一个事务内的几百毫秒。硬约束四连接池的借用与释放必须埋点监控。通过Filter、拦截器或者HikariCP的setConnectionInitSql等方式统计连接被借出到归还之间的耗时分布。超过3秒的连接占用都值得检查超过10秒基本可以认定为异常占用。这些硬约束看着简单真正落地需要靠代码评审和静态扫描工具来保障。我见过太多“开发的时候知道写代码的时候忘了”的情况。4.4 兜底快速恢复的应急操作聊完常态治理最后说下已经爆掉了怎么快速恢复。应急操作的优先级永远是“先恢复、再定位”。第一步找到并杀死异常会话。先看processlist杀掉那些长时间Sleep、长时间慢查询、长时间Running的会话。生成kill语句批量执行。第二步摘掉问题节点的流量。如果是某个应用实例的某个接口导致连接池被占满可以先从负载均衡上把这台机器摘掉或者对接口做限流切断新增的洪水流量。第三步重启应用实例。重启能清掉应用侧连接池里的所有残留状态。但这只是治标如果根因是慢SQL或者代码泄漏重启一两个小时后照样爆。第四步临时调整连接池参数。比如把connection-timeout缩短让请求快速失败而不是进入长等待或者把maximum-pool-size适当增大一点。但这只是争取排查时间不是最终解决手段。最后强调一点应急过程中所有操作要有记录谁在什么时间杀了哪些线程、改了什么参数都要留痕。不然故障结束了复盘的时候连“当时做了什么”都说不清楚问题没根治下次还会再来。5. 实战复盘一个完整的排查时间线5.1 一个典型的“双11”前夕爆满案例这里整理一个完整的实战记录。某业务系统在凌晨流量高峰前夕生产环境突然报警订单接口大量5xx错误日志里全是HikariCP的Connection is not available。我当时的时间线是这样的第1分钟拉取应用节点日志确认所有报错都集中在订单查询接口报错信息是请求超时后无法获取连接第3分钟登录MySQL执行SHOW FULL PROCESSLIST发现库上活跃会话达到600多个数据库配置的max_connections是1000虽然没有打满数据库但应用连接池的50个连接已经全部被占用第5分钟按time倒序排查发现3条SQL执行时间已经超过200秒且全部是同一张订单流水表的大范围查询第8分钟看EXPLAIN计划确认这3条SQL没有走索引全表扫描一张3000万行的表每条SQL要扫差不多20分钟。又因为订单流水表是热表新请求不断同样命中这个查询连接被一批批地拖死第10分钟杀掉这几条慢SQL连接池立即释放出连接订单接口恢复到可用状态。后续的根因分析很有意思。这个查询本身并不是新增的需求——用户端App的订单列表页最近发了一个版本把默认查询时间范围从一个月改成了“全部”用户数据几年下来有几千条订单服务端做了一个「查询全部订单再在内存里分页」的逻辑。SQL上表现为WHERE user_id ?没有时间范围限制而原来的索引是(user_id, create_time)因为查询条件里没有了create_time索引只能用到user_id的前缀。修复方案就是补了一条针对时间范围和排序的索引ALTER TABLE order_table ADD INDEX idx_user_create (user_id, create_time) USING BTREE;加了这个索引之后同样的用户查询扫描行数从几万行降到几十行执行时间从几十秒降到几十毫秒。连接池的压力瞬间消解。这个案例有三个值得记下来的点连接池爆满的直接表现是连接耗尽但根因通常不在这里。如果当时只去调大连接池等于给数据库加更大的压力服务器大概率直接被打挂。processlist的time字段是定位慢SQL的第一抓手。不管应用日志怎么报错最终要看数据库侧是什么SQL在占用连接。代码上线前的SQL审查缺位是诱因。新版本改了查询范围没有对应调整索引如果当时有简单的SQL Review环节这个问题根本不会上线。5.2 一个隐蔽的锁等待案例还有一个案例印象很深因为整个过程“看起来”就像是慢SQL导致的连接池爆满但实际完全不是。某后台管理系统的导出功能偶尔报连接池超时但频率不高一天几次。查应用日志报错之前总有几条UPDATE语句执行时间超过30秒。但单独拿这几条SQL去数据库执行都是毫秒级返回。这就很奇怪了。后来在processlist里蹲守发现这些UPDATE语句在状态为“updating”时time已经涨到了几十秒。再一查performance_schema.data_lock_waits真相大白这些UPDATE都在等待一个行锁而行锁的持有者是一个已经跑了一个多小时的事务一直没提交。链路是这样的某个后台管理员打开了一个查询页面页面上有个事务没有自动提交因为当时用的客户端把autocommit关了事务里SELECT了一些数据相当于给这些行加了共享锁导出功能里的UPDATE要对这些行加排他锁被共享锁挡住UPDATE排队等待连接被占住导出接口并发量稍微一上来连接池就被等锁的UPDATE拖垮。处理这个问题的关键不是优化SQL而是治理“长事务不提交”。之后做的改造包括对后台查询页面的连接强制开启autocommit在应用侧为所有只读查询设置事务超时和只读标记用performance_schema和sys.session做定时巡检发现超过10分钟未提交的事务直接告警。这个案例告诉我们连接池爆满的时候SQL慢不慢要看它在等什么不能只看执行时间。一个等锁的SQL出现时背后往往有一个占着锁不放的会话把根因挖到头才算解决。6. 连接池日常治理与体检清单6.1 每季度一次的连接池健康度检查与其等故障上门不如把排查工作前置成周期性的检查项。我整理了一份连接池健康度检查清单直接照着执行就行第一项检查慢查询日志。开启MySQL慢查询日志设置long_query_time 2每季度分析一次重点看那些扫描行数超过十万、执行时间超过2秒的SQL评估是否可以优化索引或改写SQL。第二项检查连接使用率趋势。从监控平台上拉取近半年的hikaricp_connections_active和hikaricp_connections_pending数据看高峰期连接使用率是否超过80%。如果连续几次峰值都接近上限要么扩容要么优化SQL不能等到爆了再处理。第三项检查长事务分布。用一条查询把超过5分钟未提交的事务拉出来SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) 300;如果频繁出现长事务就要去业务代码里找那些在事务里做远程调用、批量循环、用户交互的逻辑。第四项检查连接泄漏盯住连接池指标中的connections_leaked或者“打开次数减去关闭次数”。这个差值如果在一天内稳步上涨说明代码里有连接没归还。第五项边界测试。模拟高峰期的并发请求压测环境里故意把连接池配成一个较小的值比如50然后观察数据库侧进程数、连接池pending数、线程数等指标的反应找出最弱的那个环节并加固。6.2 依赖版本和驱动选型的隐形坑连接池问题还有一类不容易被注意的“隐性坑”驱动版本和连接池版本不匹配。MySQL驱动从5.1到8.0行为差异很大。在Connector/J 8.x的版本里默认的useSSLfalse但连接属性里如果漏了allowPublicKeyRetrievaltrue使用caching_sha2_password认证时会出现连接建立不稳定的情况。连接池如果因此频繁重建连接active连接数会时高时低看起来就像连接池在波动。另外生产环境建议用官方mysql-connector-j而不是某些魔改分支。曾经有一个项目用了一个第三方分装的驱动包连接闲置超过一定时间后不会发送ping包导致连接池里的连接和数据库侧的状态长期不一致最终表现为“连接池里有空闲连接但请求一到就报连接失效”进而触发连接重建风暴。6.3 自动化巡检脚本最后分享一个简单的巡检脚本思路可以放到定时任务里每天跑一次把异常提前暴露出来。脚本基于MySQL的sys库检查三类指标慢会话、长事务、连接数水位。核心逻辑是#!/bin/bash MYSQLmysql -h127.0.0.1 -uadmin -pxxxx echo 长事务检查 $MYSQL -e SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS seconds, trx_mysql_thread_id FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) 300; echo 慢会话检查 $MYSQL -e SELECT id, user, host, db, command, time, state, LEFT(info, 80) FROM information_schema.processlist WHERE command ! Sleep AND time 30; echo 连接数水位 $MYSQL -e SHOW STATUS LIKE Threads_connected; $MYSQL -e SHOW VARIABLES LIKE max_connections;配合一个简单的告警判断比如Threads_connected超过max_connections的80%就输出告警信息。这个脚本不用做得多复杂能提前半天发现问题就已经值回票价了。写在最后的实操体会连接池爆满这个问题我在不同项目里遇到过不下二十次。把这一路的排查经验浓缩成三句话第一句连接池爆满的锅很少真正在连接池身上。它更像是一个“症状”慢SQL是它连接泄漏是它长事务是它。只盯着连接池调参数和发烧了只吃退烧药没区别指标暂时正常了病灶还在体内。第二句先抓现场再动手先止血再排查。看到故障别急着改配置重启先花几分钟把processlist、线程dump、监控数据保存下来。故障可以恢复但现场没了下一次可能还会栽在同一个坑里。第三句最简单的根因往往最容易被忽略。我见过太多团队在连接池爆满时把精力花在复杂的高可用架构上结果最后发现就是一条SQL没走索引或者一个事务里调了一次外部接口。基础工作做扎实比任何花哨的架构都管用。最后再分享一个小技巧。遇到连接池问题养成第一时间记录三个数据的习惯连接池active数量、MySQL侧的线程连接数、等待连接的pending数量。这三个数字分别代表“应用侧有多少连接在用”“数据库侧有多少会话”“还有多少请求在排队”。哪一侧数据异常方向就不会偏。排查效率能提升一大截。