12_MySQL InnoDB 内存与锁调优 ------ Buffer Pool、redo log、隔离级别与锁等待
系列定位 :《MySQL 数据库全栈运维实战系列》第 12 篇,与《Oracle 数据库全栈运维实战系列(VMware 版)》同构对标。适用版本 :MySQL 8.0(5.7 差异随文标注)。官方文档:MySQL 8.0 Reference Manual、InnoDB Buffer Pool / Redo Log / Locking / InnoDB Multi-Versioning 章节。
一、InnoDB 内存与锁全景(官方:InnoDB In-Memory Structures)

图 1

图 2
| 组件 | 职责 | 类比 Oracle |
|---|---|---|
| Buffer Pool | 数据页缓存,读加速、写缓冲 | Database Buffer Cache |
| Log Buffer | redo 事务提交缓冲 | Redo Log Buffer |
| redo log | 崩溃恢复前滚 | Redo Log / Online Redo Log |
| undo log | 回滚 + MVCC 版本 | Undo Segments / Rollback |
| Change Buffer | 非唯一二级索引写入延迟合并 | --- |
核心差异 :Oracle 的 Buffer Cache 是块级共享池,MySQL InnoDB Buffer Pool 是按页(16KB)自管理的 LRU 链,内部有年轻/老年代划分防全表扫描污染。
二、环境准备(VMware 虚机)
单台 MySQL 8.0(192.168.10.11),分配 8GB 内存给虚机,专门验证 Buffer Pool 命中率与锁等待。
# 造压测流量(解释:让 Buffer Pool 热起来,观察命中率)
sysbench oltp_read_write --mysql-host=127.0.0.1 --tables=8 --table-size=5000000 run
VMware 注意事项(本篇相关):
| 事项 | 说明 |
|---|---|
| 内存预留/锁留 | Buffer Pool 大小接近物理内存时,必须在虚机设置里预留全部内存(Reserve all guest memory),否则 balloon/换出会人为造成命中率骤降 |
| 关 swap | vm.swappiness=1,Buffer Pool 被换出是性能灾难 |
| 磁盘 IOPS | redo log 刷盘是顺序写,数据盘要能承受稳定写入;挂 SSD 直通 |
| CPU | 锁等待与并发有关,vCPU 数量要与压测并发匹配 |
三、Buffer Pool 调优(官方:InnoDB Buffer Pool Configuration)
# /etc/my.cnf(解释:Buffer Pool 是 InnoDB 性能第一参数)
[mysqld]
innodb_buffer_pool_size = 5G # 专用 DB 机设物理内存 60~70%(8G 虚机给 5G)
innodb_buffer_pool_instances = 2 # 多实例减少互斥争用(>1G 才建议拆分)
innodb_buffer_pool_dump_at_shutdown = ON # 关机时 dump 热点页
innodb_buffer_pool_load_at_startup = ON # 启动时预热,避免冷启动抖动
-- ① 查看 Buffer Pool 配置(解释:8.0 可在线调整大小)
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SET GLOBAL innodb_buffer_pool_size = 6*1024*1024*1024; -- 在线扩容(8.0 支持)
-- ② 计算命中率(解释:健康应 > 99%)
SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests'; -- 逻辑读
SHOW STATUS LIKE 'Innodb_buffer_pool_reads'; -- 物理读(未落 BP 直接读盘)
-- 命中率 ≈ 1 - reads / read_requests
-- ③ 查看 BP 内部状态(解释:LRU 年轻/老年代、脏页、空闲页)
SHOW ENGINE INNODB STATUS\G
-- 重点看 BUFFER POOL 段落:Free buffers / Database pages / Dirty pages / Hit rate
| 参数 | 建议 |
|---|---|
innodb_buffer_pool_size |
专用 DB:物理内存 60~70%;共享机别超 50% |
innodb_buffer_pool_instances |
BP > 1G 时拆 2~8 个,降低单锁争用 |
innodb_old_blocks_pct |
老年代占比(默认 37%),防全表扫描污染 |
innodb_max_dirty_pages_pct |
脏页占比阈值(默认 90),控制刷盘节奏 |
命中率低怎么办 :先确认是不是虚机内存被换出(
vmstat看 si/so),再考虑扩 BP。盲目加内存却让 OS swap,等于没调。
四、redo log 与刷盘调优(官方:InnoDB Redo Log)

图 3
# /etc/my.cnf(解释:redo 大小与刷盘策略直接影响写性能与持久性)
[mysqld]
# 8.0.30+:用总容量参数(自动管理两个 redo 文件)
innodb_redo_log_capacity = 2G
# 5.7 / 8.0.30 之前:innodb_log_file_size + innodb_log_files_in_group
innodb_log_buffer_size = 16M # 大事务可调到 32M
innodb_flush_log_at_trx_commit = 1 # 1=每次提交刷盘(最安全,默认)
innodb_flush_method = O_DIRECT # 绕开 OS 缓存,避免双缓存
| 参数 | 取值权衡 |
|---|---|
innodb_flush_log_at_trx_commit=1 |
每次提交刷盘,宕机不丢数据(金融默认) |
=2 |
提交只写 OS 缓存,每秒刷盘,OS 崩才丢 |
=0 |
每秒刷盘,mysqld 崩丢 1 秒(不建议生产) |
innodb_flush_method=O_DIRECT |
推荐,数据页不走 OS page cache |
redo 太小的症状 :脏页还没刷完 redo 就被写满,触发激进刷盘(sharp checkpoint) ,表现为周期性性能毛刺。
innodb_redo_log_capacity太小时Innodb_log_waits上升,调大即可。
五、隔离级别与 MVCC(官方:InnoDB Multi-Versioning)
-- 查看/设置隔离级别(解释:InnoDB 默认 REPEATABLE READ)
SELECT @@transaction_isolation; -- 8.0(5.7 用 tx_isolation)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 各隔离级别行为
-- READ COMMITTED:每次快照读都读最新版本(MVCC 快照每次更新)
-- REPEATABLE READ:整个事务一个快照(可重复读,默认)
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁差异 |
|---|---|---|---|---|
| READ COMMITTED | 否 | 可能 | 可能 | 半一致读 ,UPDATE 只锁匹配行 |
| REPEATABLE READ(默认) | 否 | 否 | 基本避免 | next-key lock 防幻读 |
| SERIALIZABLE | 否 | 否 | 否 | 所有普通 SELECT 加共享锁 |

图 4
MVCC 的意义 :普通
SELECT是快照读 ,靠 undo 版本链取历史版本,完全不加锁 ,这就是 InnoDB 读写不互相阻塞的原因。只有UPDATE/DELETE/SELECT...FOR UPDATE/LOCK IN SHARE MODE这类当前读 才加锁。选 RC 还是 RR:高并发写多、且能接受"同一事务两次读可能不同"的业务,常用 RC(半一致读锁更少、死锁更少);金融账务类守 RR。
六、锁机制与锁等待排查(官方:InnoDB Locking)
6.1 锁类型

图 5
| 锁 | 作用范围 | 说明 |
|---|---|---|
| 记录锁 Record Lock | 单行索引记录 | 当前读加在索引记录上 |
| 间隙锁 Gap Lock | 索引记录之间的间隙 | 防插入,RR 下存在 |
| 临键锁 Next-Key Lock | 记录 + 前面间隙 | RR 下范围查询默认,防幻读 |
| 意向锁 IS/IX | 表级 | 表锁与行锁的协同标记 |
| 自增锁 AUTO-INC | 自增列 | 插入时序列化 |
6.2 排查锁等待(8.0 新视图)
-- ① 正在等待的锁(解释:8.0 用 performance_schema.data_locks / data_lock_waits)
SELECT * FROM performance_schema.data_lock_waits\G
-- ② 谁持锁、谁等锁(解释:data_locks 看锁对象,结合 innodb_trx 看事务)
SELECT t.trx_id, t.trx_state, t.trx_started, t.trx_query,
l.lock_type, l.lock_mode, l.lock_table, l.lock_index
FROM performance_schema.data_locks l
JOIN information_schema.innodb_trx t ON l.lock_trx_id=t.trx_id\G
-- ③ sys 一键看谁堵谁(解释:最常用)
SELECT * FROM sys.innodb_lock_waits\G
-- 输出:waiting_pid / blocking_pid / waiting_query / blocking_query / sql_kill_blocking_query
-- ④ 杀掉阻塞事务(解释:sys 直接给 KILL 语句)
-- 复制 sql_kill_blocking_query 列的值执行,如 KILL 12345;
6.3 锁等待参数与死锁

图 6
-- 锁等待超时(解释:默认 50 秒,太长会卡业务;OLTP 调小)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; -- 50
SET GLOBAL innodb_lock_wait_timeout = 5;
-- 死锁检测(解释:默认 ON,发现循环等待立即回滚牺牲者)
SHOW VARIABLES LIKE 'innodb_deadlock_detect'; -- ON
-- 查看最近一次死锁详情(解释:Status 里 LATEST DETECTED DEADLOCK 段)
SHOW ENGINE INNODB STATUS\G
| 调优点 | 建议 |
|---|---|
innodb_lock_wait_timeout |
OLTP 5~10 秒,快速失败别死等 |
| 死锁检测 | 高并发小事务下可考虑,极高并发时反成瓶颈(8.0 可用并行控制) |
| 长事务 | 锁持最久,innodb_trx 里 trx_started 过久的要查 |
| 索引缺失 | UPDATE 无索引 → 锁全表/大量间隙,必加索引 |
锁等待的头号根因 :
UPDATE/DELETE的 WHERE 条件没走索引,导致锁扫描放大(RR 下连间隙一起锁),一个小更新堵一片。这和 11 篇索引设计直接关联。
七、长事务与 undo 监控
-- ① 找长事务(解释:运行超过 60 秒的事务,锁持最久、undo 膨胀)
SELECT trx_id, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS run_sec,
trx_rows_modified, trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY run_sec DESC;
-- ② undo 与历史长度(解释:history list length 持续增长 = 有长事务回滚段)
SHOW ENGINE INNODB STATUS\G
-- TRANSACTIONS 段:History list length,持续涨说明有事务没提交/回滚
| 异常信号 | 含义 | 处理 |
|---|---|---|
| History list length 暴涨 | 长事务撑住 undo,purge 跟不上 | 找长事务 KILL |
| 锁等待超时频发 | 锁冲突激烈 | 加索引/缩短事务/调隔离级别 |
| Buffer Pool 命中率 < 95% | 内存不足或被换出 | 扩 BP / 关 swap |
八、踩坑记录(实测汇总)
| # | 坑 | 现象 | 解决 |
|---|---|---|---|
| 1 | Buffer Pool 设太大但虚机没预留内存 | OS swap,命中率骤降 | 预留全部客户机内存 + vm.swappiness=1 |
| 2 | redo log 太小 | 周期性性能毛刺、Innodb_log_waits 涨 |
调大 innodb_redo_log_capacity |
| 3 | innodb_flush_log_at_trx_commit=2 图快 |
OS 重启丢数据 | 金融坚持 =1,配合 O_DIRECT |
| 4 | UPDATE 没走索引 | 锁放大、大面积锁等待 | WHERE 条件加索引(11 篇) |
| 5 | 隔离级别用 RR 又不理解间隙锁 | 莫名其妙的插入阻塞 | 理解 next-key lock 或改 RC |
| 6 | 锁等待超时 50 秒不改 | 业务卡 50 秒才报错 | 调到 5~10 秒快速失败 |
| 7 | 长事务不处理 | history list length 暴涨、undo 膨胀 | 监控 trx_started,超时 KILL |
| 8 | 5.7 配置照搬到 8.0.30+ | innodb_log_file_size 报错/失效 |
8.0.30+ 改用 innodb_redo_log_capacity |
| 9 | 冷启动 Buffer Pool 不预热 | 开机后慢查询爆发 | 开 dump/load_at_startup |
| 10 | 快照读误以为会加锁 | 业务方不敢在高峰 SELECT | 快照读 MVCC 不加锁,随便读 |
九、本文要点速记
- Buffer Pool 是第一参数:专用库 60~70% 内存,命中率 > 99%;命中率低先查 swap。
- redo log 先日志后数据 :
innodb_redo_log_capacity(8.0.30+)别太小,否则周期性刷盘毛刺;flush_log_at_trx_commit=1保持久。 - MVCC 快照读不加锁:普通 SELECT 靠 undo 读历史版本,读写不互斥;只有当前读才加锁。
- RR 用 next-key lock 防幻读,理解间隙锁;高并发写多可考虑 RC(锁更少)。
- 锁等待头号根因是 UPDATE 没走索引 :用
data_locks/data_lock_waits或sys.innodb_lock_waits定位,KILL 阻塞事务,查长事务。
十、参考资料(官方正式文档)
| 主题 | 官方链接 |
|---|---|
| MySQL 8.0 Reference Manual | https://dev.mysql.com/doc/refman/8.0/en/ |
| InnoDB Buffer Pool Configuration | https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html |
| InnoDB Redo Log | https://dev.mysql.com/doc/refman/8.0/en/innodb-redo-log.html |
| InnoDB Multi-Versioning | https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html |
| InnoDB Locking | https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html |
| Performance Schema Lock Tables | https://dev.mysql.com/doc/refman/8.0/en/performance-schema-data-locks-table.html |
| sys Schema innodb_lock_waits | https://dev.mysql.com/doc/refman/8.0/en/sys-innodb-lock-waits.html |
本文章属于《MySQL 数据库全栈运维实战系列》性能调优篇。欢迎收藏、交流。