程序员面试宝典

一站式面试准备平台

返回分类
mysql高级

MySQL 连接池打满排查思路与详细解决方案

深入讲解 MySQL max_connections 打满的原因分析、连接池原理、连接泄漏排查、参数调优及高并发场景下的最佳实践

2026-04-09
阅读时间: 28分钟

MySQL 连接池打满排查思路与详细解决方案

MySQL 连接池打满是生产环境中常见的严重问题。本文从原理到实践,详细讲解连接池打满的原因、排查方法和解决方案。

MySQL 连接机制

连接架构

应用程序
    │
    │  1. 建立 TCP 连接
    │  2. 发送认证信息
    │  3. 验证通过后创建"连接"
    │
    ▼
MySQL Server
    │
    ├── Connection Manager (连接管理器)
    │   └── 负责接收连接、认证、分配线程
    │
    ├── Thread Cache (线程缓存)
    │   └── 缓存已用完的线程,避免重复创建销毁
    │
    └── Connection Thread (连接线程)
        └── 每个连接对应一个线程

max_connections 参数

sql
-- 查看最大连接数
SHOW VARIABLES LIKE 'max_connections';
-- 默认值:151(MySQL 5.7)
-- 推荐值:500-2000(根据内存和业务调整)

-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
-- 查看历史峰值
SHOW STATUS LIKE 'Max_used_connections';
-- 查看缓存线程数
SHOW STATUS LIKE 'Threads_cached';

wait_timeout 和 interactive_timeout

sql
-- 非交互式连接超时(应用程序默认用这个)
SHOW VARIABLES LIKE 'wait_timeout';  -- 默认 8 小时

-- 交互式连接超时(mysql 命令行)
SHOW VARIABLES LIKE 'interactive_timeout';  -- 默认 8 小时

-- 建议值:
-- 生产环境:30-300 秒(根据业务特性调整)
SET GLOBAL wait_timeout = 300;
SET GLOBAL interactive_timeout = 300;

连接池原理

连接池工作流程

┌─────────────────────────────────────────────────────────┐
│                    连接池                               │
│  ┌─────────────────────────────────────────────────┐  │
│  │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐          │  │
│  │  │ Conn │ │ Conn │ │ Conn │ │ Conn │  ...      │  │
│  │  │  1   │ │  2   │ │  3   │ │  4   │          │  │
│  │  └──────┘ └──────┘ └──────┘ └──────┘          │  │
│  │  minIdle=5    maxPoolSize=20                  │  │
│  └─────────────────────────────────────────────────┘  │
│                                                         │
│  1. 请求到来,从池中获取连接                            │
│  2. 连接空闲时间 > maxIdleTime → 关闭回收              │
│  3. 连接数 < minIdle → 后台创建新连接                  │
│  4. 连接数达到 maxPoolSize → 等待或拒绝               │
└─────────────────────────────────────────────────────────┘

常见连接池配置

yaml
# HikariCP(Java)
hikari:
  maximum-pool-size: 20        # 最大连接数
  minimum-idle: 5              # 最小空闲连接
  connection-timeout: 30000    # 获取连接超时(ms)
  idle-timeout: 600000         # 空闲超时(ms)
  max-lifetime: 1800000        # 连接最大生命周期(ms)
  leak-detection-threshold: 60000  # 泄漏检测阈值(ms)

# Druid(Java)
druid:
  maxActive: 20               # 最大连接数
  minIdle: 5                  # 最小空闲连接
  maxWait: 6000              # 获取连接最大等待时间(ms)
  timeBetweenEvictionRunsMillis: 60000  # 清理线程运行间隔
  minEvictableIdleTimeMillis: 300000    # 最小空闲时间

# Node.js mysql2
mysql2:
  connectionLimit: 20         # 最大连接数
  waitForConnections: true    # 连接用完是否等待
  queueLimit: 0               # 等待队列长度(0=无限制)
  connectTimeout: 10000        # 连接超时(ms)

连接池打满的原因

连接池打满 = 连接需求 > 连接释放
           = 获取连接速度 > 释放连接速度

常见原因:
1. 连接泄漏(Connection Leak)
2. 慢查询占用连接
3. 事务未提交/未回滚
4. 短连接频繁创建销毁
5. maxPoolSize 设置过小
6. 高并发峰值超出预期

排查方法

1. 查看当前连接状态

sql
-- 查看所有连接
SHOW FULL PROCESSLIST;
-- 或
SELECT * FROM information_schema.PROCESSLIST 
ORDER BY TIME DESC;

-- 查看各状态连接数
SELECT 
    USER,
    COUNT(*) as count,
    STATE
FROM information_schema.PROCESSLIST
GROUP BY USER, STATE;

-- 状态说明:
-- Sleep: 空闲等待
-- Query: 执行查询
-- Locked: 表锁等待
-- Sending data: 发送数据
-- Analyzing: 分析查询
-- Sorting result: 排序
-- Closing tables: 关闭表

2. 统计连接数分布

sql
-- 按用户统计连接数
SELECT 
    USER,
    COUNT(*) as connection_count,
    HOST
FROM information_schema.PROCESSLIST
GROUP BY USER, HOST
ORDER BY COUNT(*) DESC;

-- 按数据库统计
SELECT 
    DB,
    COUNT(*) as connection_count
FROM information_schema.PROCESSLIST
WHERE DB IS NOT NULL
GROUP BY DB
ORDER BY COUNT(*) DESC;

-- 按命令类型统计
SELECT 
    COMMAND,
    COUNT(*) as count
FROM information_schema.PROCESSLIST
GROUP BY COMMAND;

3. 找出慢查询占用的连接

sql
-- 查看长时间运行的查询
SELECT 
    ID,
    USER,
    HOST,
    DB,
    COMMAND,
    TIME,
    STATE,
    LEFT(INFO, 100) as query
FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep'
  AND TIME > 10  -- 运行超过 10 秒
ORDER BY TIME DESC;

-- 查看有哪些锁等待
SELECT 
    r.trx_id,
    r.trx_mysql_thread_id,
    r.trx_state,
    r.trx_started,
    r.trx_rows_locked,
    r.trx_query,
    l.lock_id,
    l.lock_mode
FROM information_schema.INNODB_TRX r
JOIN information_schema.INNODB_LOCKS l ON r.trx_id = l.lock_trx_id
WHERE r.trx_state = 'LOCK WAIT';

4. 监控连接创建释放

sql
-- 查看连接相关状态变量
SHOW GLOBAL STATUS LIKE 'Connections';        -- 总连接数
SHOW GLOBAL STATUS LIKE 'Aborted_connects'; -- 失败连接数
SHOW GLOBAL STATUS LIKE 'Aborted_clients';  -- 客户端异常断开

-- 查看是否接近上限
SHOW STATUS LIKE 'Max_used_connections';    -- 历史峰值
SHOW VARIABLES LIKE 'max_connections';       -- 最大值

-- 计算连接使用率
-- 使用率 = Max_used_connections / max_connections * 100%
-- 超过 80% 就要警惕

5. 开启慢查询日志

sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 查看最近 5 条慢查询
SHOW FULL PROCESSLIST 
WHERE Command != 'Sleep' 
  AND Time > 1 
LIMIT 5;

连接泄漏(Connection Leak)

什么是连接泄漏

go
// 连接泄漏:获取连接后没有释放
// 每次请求都泄漏一个连接,直到连接池耗尽

// Java 示例(伪代码)
func getUserById(id int) {
    conn := dbPool.GetConnection()  // 获取连接
    defer conn.Close()               // 如果忘记写这行,就泄漏了
    
    result := conn.Query("SELECT * FROM users WHERE id = ?", id)
    return result
}

// 正确写法
func getUserById(id int) {
    conn := dbPool.GetConnection()
    defer conn.Close()  // 无论成功失败都会释放
    
    result := conn.Query("SELECT * FROM users WHERE id = ?", id)
    return result
}

Python 连接泄漏示例

python
# ❌ 错误写法:异常时连接未释放
def get_user(user_id):
    conn = db_pool.get_connection()
    try:
        result = conn.execute("SELECT * FROM users WHERE id = ?", user_id)
        return result
    except Exception as e:
        return None
    # 如果异常,conn 永远不会被释放!

# ✅ 正确写法:使用 context manager
def get_user(user_id):
    with db_pool.connection() as conn:  # 自动释放
        try:
            result = conn.execute("SELECT * FROM users WHERE id = ?", user_id)
            return result
        except Exception as e:
            return None

Java 连接泄漏排查

java
// HikariCP 连接泄漏检测
// 配置 leak-detection-threshold
// 当连接借用时间超过阈值,打印警告日志

HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setLeakDetectionThreshold(60000);  // 60 秒

// 警告日志示例:
// Connection leak detection triggered for connection #1 on thread "http-nio-8080-exec-1"
// , stack trace follows
//    at com.mypackage.UserService.getUserById(UserService.java:45)
    
// 排查步骤:
// 1. 找到泄漏的代码位置
// 2. 检查是否有 try-catch 导致 close() 未执行
// 3. 检查是否有 return 语句在 close() 之前

检测连接泄漏的代码审查清单

go
// 检查清单:
// 1. defer conn.Close() 是否在每个函数中都写了?
// 2. 是否有早期 return 导致 defer 未执行?
// 3. 错误处理路径是否也释放了连接?

// ❌ 问题代码
func getUser(id int) (*User, error) {
    conn := db.Get()
    
    if conn.Ping() != nil {
        return nil, errors.New("ping failed")
        // conn 未释放!defer 在 return 之后才执行
    }
    
    user := &User{}
    conn.QueryRow("SELECT * FROM users WHERE id = ?", id, user)
    
    conn.Close()  // 太晚,可能前面就泄漏了
    return user, nil
}

// ✅ 正确代码
func getUser(id int) (*User, error) {
    conn := db.Get()
    defer conn.Close()  // defer 会保证释放
    
    if conn.Ping() != nil {
        return nil, errors.New("ping failed")
        // defer 会执行
    }
    
    user := &User{}
    err := conn.QueryRow("SELECT * FROM users WHERE id = ?", id, user)
    if err != nil {
        return nil, err
        // defer 会执行
    }
    
    return user, nil
    // defer 执行
}

事务未提交/未回滚

常见问题

go
// 问题:忘记提交或回滚
func transferMoney(from, to, amount int) error {
    conn := db.Get()
    defer conn.Close()
    
    conn.Begin()  // 开始事务
    
    conn.Execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
    conn.Execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
    
    // ❌ 忘记 conn.Commit()
    // 如果函数异常退出,会自动回滚(defer)
    // 但正常执行时会一直持有锁
    
    return nil
}

Go 正确事务写法

go
// ✅ 推荐写法:使用 Tx 上下文
func transferMoney(from, to, amount int) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()
    
    _, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
    if err != nil {
        return err  // defer 会回滚
    }
    
    _, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
    if err != nil {
        return err  // defer 会回滚
    }
    
    return nil  // defer 会提交
}

// ✅ 更简洁:使用 commit callback
func transferMoney(from, to, amount int) error {
    return db.Transaction(func(tx *sql.Tx) error {
        _, err := tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
        if err != nil {
            return err
        }
        
        _, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
        return err
    })
}

解决方案

1. 紧急处理

bash
# 1. 找出长时间运行的查询并 kill
mysql> SHOW FULL PROCESSLIST;
mysql> KILL <thread_id>;  -- 杀死占用连接的线程

# 2. 如果是应用层连接泄漏,重启应用
systemctl restart your-app

# 3. 临时增加 max_connections(重启后恢复)
mysql> SET GLOBAL max_connections = 500;

2. 优化慢查询

sql
-- 找出最慢的查询
SELECT 
    COUNT(*) as executions,
    SUM(duration) as total_duration,
    AVG(duration) as avg_duration,
    LEFT(digest_text, 100) as query
FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text != ''
GROUP BY digest_text
ORDER BY SUM(duration) DESC
LIMIT 10;

3. 合理设置连接池参数

yaml
# 推荐配置(根据服务器配置调整)
hikari:
  maximum-pool-size: 20        # CPU 核心数 * 2
  minimum-idle: 5             # 正常负载下保持的空闲连接
  connection-timeout: 10000   # 10 秒足够
  idle-timeout: 300000        # 5 分钟
  max-lifetime: 1800000       # 30 分钟
  
# 估算公式:
# maxPoolSize = (核心数 * 2) + 磁盘数
# 例如:4 核 + 1 块磁盘 = 9,建议设为 10-20

4. 监控告警

go
// 应用层监控
metrics := prometheus.NewGaugeVec(prometheus.GaugeOpts{
    Name: "db_pool_connections_active",
    Help: "Active connections in pool",
}, []string{"pool_name"})

metrics := prometheus.NewGaugeVec(prometheus.GaugeOpts{
    Name: "db_pool_connections_waiting",
    Help: "Connections waiting to acquire",
}, []string{"pool_name"})

// 告警规则(Prometheus)
# 连接使用率 > 80% 持续 5 分钟
- alert: DatabaseConnectionPoolHighUsage
  expr: db_pool_connections_active / db_pool_connections_max > 0.8
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "数据库连接池使用率过高"

# 连接等待数量 > 10
- alert: DatabaseConnectionPoolWaiters
  expr: db_pool_connections_waiting > 10
  for: 1m
  labels:
    severity: critical

5. 连接池打满时的熔断策略

go
// 连接池满时的降级处理
func GetUserWithFallback(id int) (*User, error) {
    user, err := getUserFromDB(id)
    if err != nil {
        // 连接池可能满了,降级到缓存
        return getUserFromCache(id)
    }
    return user, nil
}

// 或者使用 circuit breaker
breaker := gobreaker.NewCircuitBreaker(circuitbreaker.Config{
    Name:        "db-pool",
    MaxRequests: 3,           // 熔断打开后,3 个请求后尝试半开
    Interval:    10 * time.Second,
    Timeout:     30 * time.Second,
})

result, err := breaker.Execute(func() (interface{}, error) {
    return getUserFromDB(id)
})

6. 长连接 vs 短连接

go
// 短连接:每次请求都创建新连接(性能差)
func handler(w http.ResponseWriter, r *http.Request) {
    conn, _ := sql.Open("mysql", "user:pass@tcp(host)/db")
    // 每次请求都创建 TCP 连接
    conn.Query("SELECT ...")
    conn.Close()  // 关闭连接
}

// 长连接:复用连接池中的连接(推荐)
var db *sql.DB

func init() {
    db, _ = sql.Open("mysql", "user:pass@tcp(host)/db?parseTime=true")
    db.SetMaxOpenConns(20)
    db.SetMaxIdleConns(10)
    db.SetConnMaxLifetime(time.Hour)
}

func handler(w http.ResponseWriter, r *http.Request) {
    // 复用 db 管理的连接池
    db.Query("SELECT ...")
}

预防措施

1. 连接治理规范

markdown
## 连接使用规范

1. **获取连接后必须释放**
   - defer conn.Close()
   
2. **禁止在连接上执行耗时操作**
   - 禁止 SELECT * 查大表
   - 禁止无 LIMIT 的查询
   
3. **禁止长事务**
   - 事务内禁止网络 IO
   - 超时自动回滚
   
4. **禁止自动提交关闭**
   - 保持 autocommit=1(除非需要事务)
   
5. **禁止连接池过大**
   - maxPoolSize ≤ CPU 核心数 * 2

2. SQL 审核

sql
-- 上线前检查:
-- 1. 所有查询必须有 LIMIT
SELECT * FROM orders WHERE user_id = ? LIMIT 1000;

-- 2. 禁止全表更新/删除
-- ❌ UPDATE orders SET status = 1;
-- ✅ UPDATE orders SET status = 1 WHERE created_at > '2024-01-01' LIMIT 10000;

-- 3. 检查执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = ?;

3. 定期巡检

sql
-- 每日巡检
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME IN (
    'Threads_connected',
    'Max_used_connections',
    'Aborted_connects',
    'Slow_queries',
    'Innodb_row_lock_waits'
);

常见面试问题

Q1: max_connections 和连接池大小的关系?

sql
-- MySQL max_connections: MySQL 服务端允许的最大连接数
-- 应用连接池 maxPoolSize: 应用端持有的最大连接数

-- 建议:
-- MySQL max_connections > 应用连接池 maxPoolSize * 应用实例数 * 1.5
-- 例如:3 个应用实例,每个 20 连接,max_connections 应该 > 90

Q2: 连接池大小如何计算?

markdown
# 经验公式
maxPoolSize = (CPU 核心数 * 2) + 磁盘数

# 考虑因素:
1. CPU 密集型任务:CPU 核心数 + 少量连接
2. IO 密集型任务:可以更多连接(等待 IO 时不占 CPU)
3. 混合型:取中间值

# 实际调整:
1. 基准测试找最优值
2. 监控 Active 和 Waiting 比例
3. Waiting 高 → 增加连接
4. Active 高但 Waiting 低 → 连接够用

Q3: 如何处理连接 TIME_WAIT?

bash
# Linux 层面优化
# /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1           # 允许重用 TIME_WAIT 连接
net.ipv4.tcp_fin_timeout = 30       # 减少 FIN_WAIT_2 超时

# MySQL 层面优化
# 减少 wait_timeout,让空闲连接更快释放
SET GLOBAL wait_timeout = 300;      # 5 分钟

# 应用层面
# 使用连接池复用连接,避免频繁创建销毁

Q4: 连接池和线程池的区别?

markdown
# 连接池:管理数据库连接
# - 获取连接有开销(TCP 连接、认证)
# - 复用连接减少开销
# - 连接是有状态的(事务、临时表等)

# 线程池:管理工作线程
# - 线程创建有开销
# - 复用线程减少开销
# - 线程通常无状态

# 配合使用:
# 请求 → 线程池取线程 → 从连接池取连接 → 执行 → 归还

总结

阶段操作命令/方法
发现问题查看当前连接SHOW FULL PROCESSLIST
定位原因找慢查询/长事务SHOW ENGINE INNODB STATUS
紧急处理kill 阻塞连接KILL <thread_id>
临时解决增加连接数SET GLOBAL max_connections=500
根本解决修复代码/优化查询代码审查 + EXPLAIN

预防胜于治疗:做好监控、规范开发、合理配置,比出问题后再救火重要得多。

相关标签