风险提示

优化往往伴随着变更,任何生产环境的调整请务必先在测试环境验证,并做好备份与回滚方案;大表 DDL 建议使用 pt-online-schema-change 或 gh-ost,并先 —dry-run 验证。


1. 硬件层面优化

硬件优化 基础硬件是性能的基石,通常遵循”木桶效应”。需根据业务负载适当增加 CPU 核心数、内存容量,并选用高性能 SSD/NVMe 硬盘以降低 I/O 延迟。若 wa 升高且怀疑 I/O 瓶颈,可用 iostat -x 1 观察磁盘饱和度,并结合 Innodb_buffer_pool_reads 增长判断是否工作集已超出缓冲池。


2. 系统层面优化

指标含义优化建议
id (idle)CPU 空闲率数值越大表示越空闲;若接近 0,说明 CPU 满负荷。
us (user)用户空间占用率表示应用程序(如 MySQL)对 CPU 的使用率。
sy (system)内核空间占用率表示系统与内核交互的频率。若过高,说明内核处理请求负担重。
wa (iowait)I/O 等待率CPU 等待磁盘 I/O 的时间。若过高,通常意味着磁盘读写成为瓶颈。
Bash
UTF-8|6 Lines|
[root@mysql-master ~]# top
top - 15:05:11 up 35 days,  5:54,  2 users,  load average: 0.00, 0.01, 0.05
Tasks: 225 total,   2 running, 223 sleeping,   0 stopped,   0 zombie
%Cpu0  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 24522416 total, 14931524 free,  3675344 used,  5915548 buff/cache
KiB Swap: 12386300 total, 12386300 free,        0 used. 20450988 avail Mem

2.1 进程与线程定位

当发现 CPU 负载过高时,可进一步定位到具体线程:

Bash
UTF-8|5 Lines|
# 查看整体负载
top

# 指定 PID 查看该进程下的具体线程 (-Hp)
top -Hp 

假设发现线程 ID 1893 占用过高,可关联数据库内部视图进行定位:

SQL
UTF-8|14 Lines|
-- 在 performance_schema 中查找对应的数据库线程
SELECT 
    THREAD_ID, 
    NAME, 
    TYPE, 
    PROCESSLIST_USER, 
    PROCESSLIST_HOST, 
    PROCESSLIST_DB, 
    PROCESSLIST_COMMAND, 
    PROCESSLIST_TIME, 
    PROCESSLIST_STATE,
    THREAD_OS_ID
FROM performance_schema.threads
WHERE THREAD_OS_ID = 1893;

2.2 I/O 问题排查

若 wa 值较高,怀疑存在 I/O 瓶颈,可查询 sys 库中的相关视图:

SQL
UTF-8|7 Lines|
USE sys;
SHOW TABLES LIKE 'x$io%';

-- 常用视图:
-- x$io_by_thread_by_latency: 按线程统计的 I/O 延迟
-- x$io_global_by_file_by_bytes: 按文件统计的 I/O 字节数
-- x$io_global_by_wait_by_latency: 按等待事件统计的 I/O 延迟

策略确认 若确认 I/O 是瓶颈,可考虑增加 innodb_buffer_pool_size,用内存换取时间,减少磁盘读写;并确认缓冲池命中率是否达标。

2.3 观测与监控工具

建议建立持续监控闭环:使用 Prometheus + Grafana + mysqld_exporter 或 PMM 监控 QPS/TPS、连接数、缓冲池命中率等;慢日志分析使用 pt-query-digest 按指纹聚合,快速定位 Top SQL。


3. MySQL 版本选择优化

提示 强烈推荐使用 MySQL 8.0+。在同等硬件配置下,MySQL 8.0 相比 5.7 在优化器、窗口函数、CTE、JSON 支持等方面有明显提升;2026 年的调优资料已专门讨论 MySQL 8.4 LTS 对性能调优的影响,选型时建议纳入评估。同时请注意:MySQL 8.0 已彻底移除 Query Cache,不要再配置 query_cache_* 参数。

选型原则:

  • 稳定性优先:选择开源社区的 GA(General Availability)稳定版。
  • 时间窗口:建议选择 GA 版本发布后 6-12 个月的偶数版本(通常更稳定)。
  • 兼容性:确保所选版本与现有应用框架、ORM 及驱动兼容。
  • 缓存替代:MySQL 8.0 读多场景应使用应用层缓存(Redis / Caffeine / 本地缓存)或代理层缓存(如 ProxySQL),而非已移除的查询缓存。

4. 架构与参数优化(三层结构)

注意:以下参数仅供参考,请根据实际服务器配置(CPU/内存/磁盘)及业务场景调整,并在测试环境验证。

4.1 连接层优化(Connection Layer)

主要负责处理客户端连接、认证及权限校验。

ini
UTF-8|9 Lines|
[mysqld]
max_connections = 1000              # 最大连接数;建议按业务峰值 500~2000 评估
max_connect_errors = 10000          # 允许的最大错误连接数,防止误封 IP
wait_timeout = 600                  # 非交互式连接超时时间(秒);小规格 VPS 可下调到 60 快速回收
interactive_timeout = 3600          # 交互式连接超时时间(秒)
net_read_timeout = 120              # 读超时时间
net_write_timeout = 120             # 写超时时间
max_allowed_packet = 500M           # 最大数据包大小,防止大 SQL 报错
thread_cache_size = 64              # 线程缓存;可设 64~128,资源较小时 50 亦可

连接池 配合连接池(如 HikariCP、Go database/sql 池、Node.js 池)使用,避免频繁创建/销毁连接;并发连接数过多(如数千)会让原本很快的查询也变慢。

4.2 Server 层优化(Server Layer)

负责 SQL 解析、分析、优化、缓存及内置函数处理。

ini
UTF-8|24 Lines|
[mysqld]
# 排序与缓冲区
sort_buffer_size = 8M               # 排序缓冲区(每个会话)
read_buffer_size = 2M               # 顺序读缓冲区
read_rnd_buffer_size = 8M           # 随机读缓冲区
join_buffer_size = 8M               # 连接缓冲区(全表扫描时使用)
key_buffer_size = 16M               # MyISAM 索引缓冲(若全用 InnoDB 可调小)

# 慢查询与日志
slow_query_log = 1                  # 开启慢查询日志
long_query_time = 1                 # 慢查询阈值(秒);生产建议 0.5 或 1,繁忙系统可到 100~300ms
slow_query_log_file = /data/mysql/mysql-slow.log
log_queries_not_using_indexes = 1   # 记录未使用索引的查询;高并发下建议配合 min_examined_row_limit=1000 防日志膨胀
min_examined_row_limit = 1000
sql_safe_updates = 1                # 禁止无 WHERE 条件的 UPDATE/DELETE(开发环境推荐)
binlog_format = ROW                 # 推荐 ROW 格式,数据一致性更好
sync_binlog = 1                     # 每次事务提交同步刷盘,保证 crash-safe
max_execution_time = 28800          # SELECT 语句最大执行时间(毫秒)
log_timestamps = SYSTEM             # 日志时间戳格式
init_connect = "SET NAMES utf8mb4"  # 连接初始化字符集

# 其他
event_scheduler = OFF               # 若无定时任务需求,建议关闭
lock_wait_timeout = 31536000        # 锁等待超时时间(秒)

慢日志分析示例:

Bash
UTF-8|1 Line|
pt-query-digest /var/log/mysql/slow.log | head -100

4.3 Engine 层优化(InnoDB Engine)

负责数据的存储与提取,是优化的核心。

核心:InnoDB Buffer Pool

独占且以 InnoDB 为主的实例,多数 2026 调优指南建议 innodb_buffer_pool_size 设为物理内存的 70%–80%;若主机还需为 OS、线程栈和连接保留更多内存,也可按 50%–70% 分配,并至少预留 1GB 给系统。目标命中率应达 99%+,低于 95% 说明偏小,低于 90% 属紧急情况。

可通过如下 SQL 计算命中率:

SQL
UTF-8|10 Lines|
SELECT 
    FORMAT(100 - (Innodb_buffer_pool_reads * 100.0 / Innodb_buffer_pool_read_requests), 2) AS hit_ratio_pct,
    FORMAT_BYTES(@@innodb_buffer_pool_size) AS configured_size
FROM (
    SELECT 
        SUM(IF(variable_name = 'Innodb_buffer_pool_reads', variable_value, 0)) AS Innodb_buffer_pool_reads,
        SUM(IF(variable_name = 'Innodb_buffer_pool_read_requests', variable_value, 0)) AS Innodb_buffer_pool_read_requests
    FROM performance_schema.global_status
    WHERE variable_name IN ('Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests')
) stats;

推荐参数

ini
UTF-8|21 Lines|
[mysqld]
transaction-isolation = READ-COMMITTED  # 推荐 RC 级别,减少间隙锁,提高并发
innodb_data_home_dir = /data/mysql/data
innodb_log_group_home_dir = /data/mysql/redolog

# Redo Log 配置
innodb_log_file_size = 2048M            # 可按写负载设为 1G~4G;也有示例用 512M 并配合 64M log buffer
innodb_log_files_in_group = 3
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 2      # 1: 强一致/生产推荐;2: 性能折中,崩溃可能丢失约 1 秒数据

# Buffer Pool 配置(核心)
innodb_buffer_pool_size = 48G           # 示例:按物理内存 70%~80% 或 50%~70% 计算
innodb_buffer_pool_instances = 8        # 缓冲池实例数;可按每 GB 一个实例或按 CPU 核数调整
# innodb_dedicated_server = 1           # 可选:8.0+ 自动调优;注意开启后会忽略手动 buffer_pool/redo_capacity/flush_method

# I/O 控制
innodb_flush_method = O_DIRECT          # 绕过操作系统缓存,避免双重缓冲
innodb_io_capacity = 2000               # 每秒 I/O 操作数;SSD 可设为 5000+
innodb_io_capacity_max = 4000           # 最大 I/O 爆发能力
innodb_max_dirty_pages_pct = 85         # 脏页比例上限,触发刷脏

tcmalloc 小技巧 高并发服务器可安装 tcmalloc,据称可降低 20%–40% 内存占用,约 5 分钟即可完成,是常被团队跳过的收益点。


5. 开发规范(SQL 编写与设计)

5.1 基本编写规范

  • 避免 SELECT *:只查询需要的列,减少网络传输与内存消耗,并增加命中覆盖索引的概率。
  • 避免对索引列使用函数:例如将 WHERE YEAR(created_at)=2026 改写为范围扫描 WHERE created_at BETWEEN '2026-01-01' AND '2026-12-31' 以使用索引。
  • JOIN 优化:JOIN 字段必须有索引且类型一致;尽量小表驱动大表;能转 INNER JOIN 就不要滥用 LEFT JOIN;大表 JOIN 可拆分为多次查询在应用层组装。
  • 子查询 vs JOIN:MySQL 8.0 对子查询优化已增强,但多数场景 JOIN 更稳定,应使用 EXPLAIN 对比 type 与 rows 后选择。
  • 预编译与连接池:使用 Prepared Statements 与连接池,减少解析开销与连接抖动。

5.2 分页与批量

深分页(LIMIT 100000, 20)会扫描并丢弃大量行,应使用延迟关联或游标法。

延迟关联示例:

SQL
UTF-8|3 Lines|
SELECT o.* FROM orders o
INNER JOIN (SELECT id FROM orders ORDER BY create_time DESC LIMIT 100000, 20) tmp
ON o.id = tmp.id;

游标法(推荐)示例:

SQL
UTF-8|1 Line|
WHERE id < last_id ORDER BY id DESC LIMIT 20

批量操作建议:

  • 多行 INSERT 合并为单条语句,建议单次 ≤ 5000 行。
  • 避免大事务,分 chunk 提交,防止 undo log 膨胀与锁竞争。
  • 大批量 UPDATE/DELETE 加 LIMIT 循环执行,并配合 sleep(0.1) 降低负载。

5.3 事务与观测

  • 缩短事务:及时提交,减少锁等待、锁超时与死锁概率。
  • 执行计划验证:MySQL 8.0+ 使用 EXPLAIN ANALYZE(而非仅 EXPLAIN),它展示实际执行时间与行数;若预估 100 行但实际返回 200 万行,说明统计信息或谓词有问题。
  • 持续分析:定期用 pt-query-digest 分析慢日志,优先解决 Top 慢 SQL。

6. 索引优化

6.1 索引不是越多越好

每个二级索引都会增加写放大:INSERT/UPDATE/DELETE 都需维护索引。曾有案例因单表 12 个索引导致写入性能下降 60%。建议单表索引控制在合理数量(有实践建议 ≤ 8 个),并定期用不可见索引验证收益。

查找未使用索引与冗余索引:

SQL
UTF-8|6 Lines|
-- 自上次重启以来未使用的索引(排除系统库)
SELECT * FROM sys.schema_unused_indexes
WHERE object_schema NOT IN ('performance_schema', 'information_schema', 'mysql', 'sys');

-- 冗余索引
SELECT * FROM sys.schema_redundant_indexes;

也可查 performance_schema.table_io_waits_summary_by_index_usage 中 count_read = 0 的索引。

删除前先设为不可见,观察一周确认无影响后再 DROP:

SQL
UTF-8|5 Lines|
ALTER TABLE orders ALTER INDEX idx_created_at INVISIBLE;
-- 观察;如需恢复:
-- ALTER TABLE orders ALTER INDEX idx_created_at VISIBLE;
-- 确认安全后:
-- DROP INDEX idx_created_at ON orders;

6.2 复合索引与最左前缀

复合索引应将等值条件放在最左,其次是范围条件,再考虑 ORDER BY 列,以避免 Using filesort。

例如订单查询可建:

SQL
UTF-8|1 Line|
CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at);

使 WHERE user_id=? AND status=? ORDER BY created_at DESC 能走完整索引。

6.3 覆盖索引

覆盖索引包含查询所需的全部列,可避免回表;EXPLAIN 的 Extra 出现 Using index 即表示命中:

SQL
UTF-8|1 Line|
CREATE INDEX idx_orders_dashboard ON orders(user_id, status, created_at, total);

6.4 索引失效与统计信息

常见索引失效场景包括:对索引列使用函数、隐式类型转换、!= / NOT IN、OR 条件含无索引列、LIKE '%abc'、不满足最左前缀、范围查询后的列失效、字符串未加引号等。低选择性列(如布尔值、只有两个值的状态)会被优化器忽略,因为全表扫描更快。

若 EXPLAIN ANALYZE 显示实际行数与预估偏差很大,应先执行 ANALYZE TABLE 更新统计信息;对数据倾斜列,可建立直方图 ANALYZE TABLE ... UPDATE HISTOGRAM 来提升选择率估计。

6.5 防止执行计划回归

在 CI/CD 中对热 SQL 使用 EXPLAIN FORMAT=JSON,断言其访问类型或使用到的索引,及时捕获”从索引扫描退化为全表扫描”的回归。


7. 事务与锁

MySQL 的锁机制复杂,理解各类锁对于解决阻塞和死锁至关重要。

7.1 全局读锁(Global Read Lock / FTWRL)

  • 加锁:FLUSH TABLES WITH READ LOCK;
  • 解锁:UNLOCK TABLES;
  • 影响:
    • 阻塞所有事务的写入(DML)。
    • 阻塞所有事务的提交(Commit)。
    • 属于 MDL(Metadata Lock)层面。
  • 典型场景:传统逻辑备份(如 mysqldump 不加 --single-transaction 时)。

排查方法(通用 5.6+):

SQL
UTF-8|8 Lines|
USE performance_schema;
-- 确保 instrumentation 开启
UPDATE setup_instruments SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME = 'wait/lock/metadata/sql/mdl';

-- 查看元数据锁情况
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_DURATION, LOCK_STATUS, OWNER_THREAD_ID
FROM performance_schema.metadata_locks;

5.7+ 专用视图:

SQL
UTF-8|2 Lines|
SHOW PROCESSLIST;
SELECT * FROM sys.schema_table_lock_waits;

经典故障案例:

模拟备份时的 FTWRL,此时会发现命令阻塞,发起正常查询请求也会被阻塞。5.7 版本的 XtraBackup / mysqldump 备份数据库时出现锁表状态,所有查询不能正常进行。

SQL
UTF-8|3 Lines|
SELECT *, SLEEP(100) FROM `user` WHERE username = 'test1' FOR UPDATE;
FLUSH TABLES WITH READ LOCK;
SELECT * FROM icours.user WHERE username = 'test' FOR UPDATE;

7.2 表级锁(Table Lock)

  • 显式加锁:
    • LOCK TABLE t1 READ;(当前会话只读,其他会话可读)
    • LOCK TABLE t1 WRITE;(当前会话读写,其他会话阻塞)
  • 隐式锁:某些 DDL 操作或特定存储引擎特性。
  • 检测:
SQL
UTF-8|2 Lines|
SELECT * FROM performance_schema.metadata_locks;
SELECT * FROM performance_schema.threads;

7.3 元数据锁(MetaData Lock / MDL)

  • 作用:保证并发访问下表结构的一致性。执行 DML 时自动加 MDL 读锁,执行 DDL 时加 MDL 写锁。
  • 风险:长事务持有 MDL 读锁未释放,会阻塞后续 DDL(如加字段),导致大量后续查询堆积(Thundering Herd)。
  • 检测方式:
SQL
UTF-8|8 Lines|
-- 找到阻塞的线程 ID(OWNER_THREAD_ID)
SELECT * FROM performance_schema.metadata_locks WHERE LOCK_STATUS = 'PENDING';

-- 关联 threads 表获取具体信息
SELECT * FROM performance_schema.threads WHERE THREAD_ID = ;

-- 终止线程
KILL ;

7.4 自增锁(Auto-inc Lock)

由参数 innodb_autoinc_lock_mode 控制:

  • 0(Traditional):每条插入都申请表级锁,并发差。
  • 1(Consecutive,默认):预估行数申请 mutex;但在 LOAD DATA 或 INSERT … SELECT 未知行数时会退化为模式 0。
  • 2(Interleaved):强制使用 mutex,并发最高,但可能导致自增 ID 不连续(主键空洞)。推荐在高并发写入场景设置为 2。

7.5 行级锁(InnoDB Row Lock)

包括 record lock、gap lock、next-key lock 等。

SQL
UTF-8|3 Lines|
SHOW STATUS LIKE 'Innodb_row_lock%';
SELECT * FROM information_schema.innodb_trx;   -- 查看运行中的事务
SELECT * FROM sys.schema_table_lock_waits;     -- 查看锁等待

7.6 死锁(Deadlock)

当多个事务互相持有对方需要的锁并等待对方释放时发生。

SQL
UTF-8|1 Line|
SHOW ENGINE INNODB STATUS\G;

开启死锁日志记录:innodb_print_all_deadlocks = 1。

修复方式包括:所有应用代码按一致顺序访问行、保持事务短小、仅在严格必要时使用 SELECT ... FOR UPDATE,并通过 innodb_print_all_deadlocks=1 持久化记录死锁以便审计。

优化方向:

  • 索引优化:确保 WHERE 条件使用索引,避免全表扫描升级为大量行锁。
  • 缩小范围:尽量减小事务更新的数据范围。
  • 隔离级别:业务允许时,使用 READ-COMMITTED(RC)替代 Repeatable-Read(RR),以减少间隙锁(Gap Lock)。
  • 拆分大事务:将大批量更新拆分为小批次执行。
SQL
UTF-8|6 Lines|
-- 优化前:可能锁定大量不相关的行(若 k1 非唯一索引)
UPDATE t1 SET num = num + 10 WHERE k1 < 100;

-- 优化后:先查 ID,再精确更新
SELECT id FROM t1 WHERE k1 < 100;
UPDATE t1 SET num = num + 10 WHERE id IN (20, 30, 50);

8. 架构与安全

8.1 监控与慢查询闭环

建议建设持续性能监控:使用 Prometheus + Grafana + mysqld_exporter 或 PMM,并结合 pt-query-digest 聚合慢日志;重点监控缓冲池命中率、QPS/TPS、连接数、复制延迟等指标。将热 SQL 的 EXPLAIN FORMAT=JSON 纳入 CI,提前捕获计划回归。

8.2 读写分离与分库分表

  • 读写分离:可使用 Proxy 层(ProxySQL、MaxScale)或客户端方案(ShardingSphere-JDBC、dynamic-datasource);核心痛点是主从延迟,写后立即读的场景应强制走主库或用缓存过渡。
  • 分库分表:单表超过 2000 万行或 50GB,且索引已无法解决时再考虑;按业务维度(如 user_id / tenant_id)分片,可使用 ShardingSphere/MyCat;需提前设计路由与汇总,避免跨分片 JOIN/聚合。分区表在 MySQL 8.0 仍限制较多,生产环境应慎用。

8.3 在线 DDL 与数据导入

  • 大表 DDL 使用 pt-online-schema-change 或 gh-ost,在业务低峰执行并先 --dry-run 验证,避免锁表与 redo/undo 打满。
  • 初始批量数据导入可使用 MySQL Shell 的并行加载 util.loadDump,大数据集比 mysqldump 快约 70%。
  • 备份尽量在副本(从库)上进行,避免拖慢主库。

8.4 其他工程实践

  • 图片等二进制资源建议存为文件、数据库仅保存路径,因为 Web 服务器对文件的缓存通常优于数据库内容。
  • 统计类/报表类查询尽量基于定期生成的汇总表,而非实时大表聚合,以区分”实时”与”统计”负载。
  • 在 Kubernetes / 云 RDS 环境中,参数控制受限(如受 Pod limit 或参数组限制),应更聚焦查询优化、schema 设计与连接池(如 ProxySQL sidecar、RDS Proxy)。