你的网站又卡了?八成是数据库连接数撑不住了

页面转圈、接口报错、后台打不开……查日志一看全是 Too many connections。别急着重启服务,这问题其实挺常见,也比你想的更容易摸清门道。

第一步:别急着加连接数,先搞清楚谁在“占着茅坑”

看到报错第一反应调大 max_connections?先停一停。这就像会议室满了,你不去看看谁坐在里面发呆,反而想着再搭个帐篷——问题不在空间,而在人怎么坐。

马上连进数据库,跑这条命令:

SHOW PROCESSLIST;

重点盯三类连接:

  • StateSleep,但 Time 超过 60 秒的(空着不走)
  • CommandQueryTime 却一直涨的(卡在执行)
  • Info 里明显是重复请求、没参数、或者带 SELECT * FROM xxx WHERE id = ? 却没走索引的(大概率慢查询)

真实案例:一个做本地生活的后台系统,高峰期总崩。一查 PROCESSLIST,发现 80% 的连接都是 Sleep 状态,最长的挂了 4 小时。最后定位到是某段老代码用完连接后没调 close(),还配了个超长的 idleTimeout=3600000,等于主动把连接锁死一小时。

连接池配置:你的连接管理“大管家”用对了吗?

连接池不是越大越好,它更像一个值班表——排得太满,人累;排得太松,活干不完。出问题,9 成都在这里。

打开你的应用配置文件(比如 application.ymldruid.properties),检查这几项:

  • maxActive / maxTotal:别设成数据库 max_connections 的 80%,建议控制在 30%~50%
  • minIdle:别为了“预热”设太高,2~5 就够,启动时占一堆空连接很伤
  • maxWaitMillis:别设成 -1(无限等),3000 毫秒是合理底线
  • timeBetweenEvictionRunsMillis + minEvictableIdleTimeMillis:必须配!建议前者 30000(30 秒扫一次),后者 60000(空闲超 1 分钟就回收)

特别提醒:Spring Boot 默认 HikariCP 的 connection-timeout 是 30 秒,但很多团队没改 idle-timeout(默认 10 分钟),这就埋下了“僵尸连接”的种子。

SQL语句是性能黑洞?慢查询拖垮整个连接池

一条慢 SQL,就是一个卡住的线程。它不释放连接,后面所有请求全得排队。这不是性能问题,是资源死锁。

立刻确认数据库是否开了慢查询日志:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;

然后去翻日志,找 Query_time 超过 2 秒的语句。重点关注:

  • WHERE 条件字段有没有索引?
  • ORDER BY 字段是不是在联合索引最左?
  • JOIN 表有没有加驱动表提示?
  • 是不是写了 SELECT *,却只用其中两三个字段?

真实案例:一个 SaaS 后台的客户列表页,每次打开都卡顿。慢日志里反复出现一条 SELECT * FROM customers WHERE status = ? ORDER BY created_at DESC LIMIT 20。表有千万级数据,但 statuscreated_at 没联合索引。补上 (status, created_at) 索引后,查询从秒级降到毫秒级,该页面的连接占用时间明显下降,用户不再反馈“点不动”。

长事务和失效的索引:连接被长期“绑架”的元凶

事务不是越长越好。一个事务里做了三次 HTTP 调用、等用户填表单、再更新数据库?那这个连接可能被锁住几分钟——别人想读同一行数据,只能干等。

原则就一条:事务只包数据库操作,其他一律搬出去。HTTP 请求、文件写入、消息发送,全都放到 @Transactional 外面。

另外,索引不是建完就一劳永逸。数据量翻倍、分布变化、字符集升级……都可能让优化器“看走眼”。定期用 EXPLAIN 看关键查询:

EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'paid';

如果 typeALL,或者 key 显示 <null>,说明索引没用上。这时候别急着重建,先试试:

ANALYZE TABLE orders;

统计信息一更新,有时候优化器自己就“醒”了。

架构层面:读写分离和缓存能救你的命吗?

当单库连接数稳定在 max_connections 的 70% 以上,且优化完 SQL 和连接池仍没缓解,就得动架构了。

读写分离不是高大上概念,很多项目已经用上了:

  • MyBatis Plus 支持 @DS("slave") 切数据源
  • ShardingSphere-JDBC 只要改个配置就能自动路由读请求到从库
  • Spring Cloud Alibaba Seata 也能配合做读写分离兜底

关键是:别让所有读都压主库。用户中心、商品详情、订单历史这些读多写少的接口,优先切到从库。

缓存不是万能胶,但它是最快见效的减压阀

  • 用户登录态、权限菜单、系统配置 → 丢进 Redis,TTL 设 30 分钟起
  • 商品基础信息、分类树、热门搜索词 → 加个本地缓存(Caffeine)+ Redis 双层,避免缓存击穿
  • 页面片段(比如首页推荐位、活动 banner)→ 渲染好直接缓存 HTML 片段,过期时间按业务节奏设(比如 5 分钟)

真实案例:一个教育平台的课程详情页,每次访问要查 6 张表拼数据。团队把课程基础信息 + 教师简介 + 章节大纲三块拆出来,分别缓存到 Redis,过期时间设为 10 分钟。上线后,该接口的数据库连接数消耗降到了原来的零头,页面加载速度也翻了好几倍。

除了重启,今天下班前你能做什么?

别等明天,现在打开终端/Navicat/MySQL Workbench,照着做:

  1. 查现状:运行

    SHOW VARIABLES LIKE 'max_connections';
    SHOW STATUS LIKE 'Threads_connected';
    

    记下两个数字,心里就有谱了。

  2. 揪异常:运行

    SHOW PROCESSLIST;
    

    把结果按 Time 倒序,截图发给开发同事,标出所有 Time > 60State = 'Sleep' 的连接,以及 StateSending dataSorting result 的活跃查询。

  3. 翻配置:打开你项目的 application.yml(或 druid.properties),找到连接池配置段,确认:

    • max-active / max-total 没超过数据库 max_connections 的一半
    • min-evictable-idle-time-millisidle-timeout 已设置(建议 60000)
    • connection-timeout 不是 -1,而是 3000 左右
  4. 推一把:把第 2 步的截图 + 第 3 步的配置问题,直接钉钉/飞书发给后端负责人,标题写清楚:“【紧急】数据库连接堆积,疑似空闲连接未回收 + 某些查询卡住,请帮忙看下对应代码逻辑”。

  5. 临时止血(仅限低峰):如果连接数已飙到 95% 以上,且确认某些 Sleep 连接确实无业务关联(比如来自测试脚本、旧监控任务),可以手动 KILL [id] ——但一定要先 SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST; 再核对,别误杀。

做完这五步,你至少知道“谁占着、为什么占、怎么赶”,而不是靠重启续命。接下来,才是深入查慢 SQL、调事务、上缓存的节奏。