MySQL 实战 45 讲 | 林晓斌

01 | 基础架构:一条 SQL 查询语句是如何执行的?

image

请求自上而下经过 Server 层 → Engine 层。Server 层承担连接、解析、优化、执行,所有引擎共享;Engine 层负责数据存取,可插拔。

组件职责备注
连接器建立 / 鉴权 / 维持连接长短连接由客户端控制,服务端 wait_timeout 兜底
查询缓存命中即直接返回8.0 已移除;表一更新整张缓存失效,不建议用
分析器词法 + 语法 + 预处理;列 / 表名校验在此发生Unknown column 'k' in 'field list' 就在此
优化器选索引、决定 join 顺序
执行器鉴权后调引擎接口;返回结果集引擎扫描行数 ≠ rows_examined(见 §05)

常用查询

SHOW PROCESSLIST;                       -- 当前连接
SELECT @@wait_timeout;                  -- 28800s = 8h
SHOW VARIABLES LIKE 'wait_timeout';
SELECT @@query_cache_type;              -- 8.0 之前可观察是否开启

清理连接占用的资源但保留连接:调接口函数 mysql_reset_connection(),不是 SQL 语句。

为什么 SELECT k FROM ... 在表没有 k 列时报错在分析器而非执行器?因为字段定义是元数据,分析器读元数据就能判断列是否存在,无需打开表。

参考:sysvar_query_cache_type

02 | 日志系统:一条 SQL 更新语句是如何执行的?

WAL(Write-Ahead Logging):先写日志,再写磁盘。日志顺序写,数据随机写。

redo log vs binlog

维度redo logbinlog
归属InnoDB 引擎专属Server 层,所有引擎共享
内容物理:哪个数据页做了什么修改逻辑:行变更的语义(语句 / 行格式)
写入方式循环写,固定大小追加写,写满切下一个
用途事务、crash-safe归档、主从同步、恢复
共享性只有 InnoDB 自己用(私有物理格式)其他数据库 / 引擎可消费(语义通用)
image

两阶段提交

binlog 写入夹在 redo log 的 prepare → commit 之间,使两个日志的提交状态保持一致。崩溃恢复决策:

崩溃时机redo log 状态binlog 状态恢复决策
binlog 写入前崩prepare未写回滚
binlog 已写、commit 前崩prepare已写提交
commit 之后崩commit已写直接提交

关键参数

SELECT @@innodb_flush_log_at_trx_commit;  -- 1 推荐:每事务持久化 redo
SELECT @@sync_binlog;                     -- 1 推荐:每事务持久化 binlog
SELECT @@max_binlog_size / 1024 / 1024;   -- MB
SELECT @@innodb_page_size / 1024;         -- 16(默认页大小 KB)
SELECT @@binlog_format;                   -- STATEMENT / ROW / MIXED

binlog 格式:STATEMENT 记 SQL(时间函数等不确定语义有风险);ROW 记前后两行内容,体积大但确定。

记录以 页(page) 为单位存取,默认 16KB —— 读一行实际读 16KB。

参考:

03 | 事务隔离:为什么你改了我还看不见?

三种读异常

异常含义出现于隔离级别
脏读读到其他事务未提交的数据READ UNCOMMITTED
不可重复读同一记录前后读取内容不一致READ UNCOMMITTED、READ COMMITTED
幻读同一范围前后读取行数不一致READ UNCOMMITTED、READ COMMITTED;InnoDB RR 因 MVCC 已避免

脏读示例:

-- session1
BEGIN;
UPDATE T1 SET k=2 WHERE id=1;        -- 未提交

-- session2
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT k FROM T1 WHERE id=1;          -- 2 ← 脏读

不可重复读示例:

-- session1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT k FROM T1 WHERE id=1;          -- 1

-- session2
UPDATE T1 SET k=2 WHERE id=1;         -- 已提交

-- session1
SELECT k FROM T1 WHERE id=1;          -- 2 ← 不可重复读

幻读示例(RC 下):

-- session1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT count(*) FROM T1 WHERE k=1;    -- 1

-- session2
INSERT INTO T1 (k) VALUES (1);

-- session1
SELECT count(*) FROM T1 WHERE k=1;    -- 2 ← 幻读

四种隔离级别

级别视图创建时机阻挡的异常
READ UNCOMMITTED无视图,直接读最新值
READ COMMITTED每条 SQL 开始时脏读
REPEATABLE READ事务首条语句执行时脏读 + 不可重复读(+ 幻读,仅 InnoDB)
SERIALIZABLE加读 / 写锁强制串行脏读 + 不可重复读 + 幻读

RR 在标准 SQL 里无法避免幻读,但 InnoDB 在 RR 下叠加 MVCC + Next-Key Lock 后能避免(仅 snapshot read;FOR UPDATE 等当前读另议,见 §08)。

常用配置

SELECT @@tx_isolation;                                  -- 当前隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET autocommit = 0;                                     -- 关闭自动提交
COMMIT WORK AND CHAIN;                                  -- 等价 commit; begin;

长事务的影响与避免

-- 持续超过 60s 的事务
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(timediff(now(), trx_started)) > 60;

避免清单:

  • 确认 autocommit = 1;框架别习惯性把所有语句 BEGIN/COMMIT 包起来。
  • SET MAX_EXECUTION_TIME 给单语句兜底。
  • 监控 information_schema.innodb_trx,设长事务阈值告警。
  • 开发期开 general_log,提前发现问题。

参考:

04 | 深入浅出索引(上)

索引数据结构对比

结构等值范围适用场景
哈希表不支持等值查询为主
有序数组静态数据
平衡 BSTO(log N)O(log N)内存索引
N 叉树(B+)高度低高度低InnoDB 磁盘索引

估算:InnoDB 一个 16KB 页放约 1170 个 BIGINT 索引项(16K / (8B 数据 + 6B 子树指针))。N 叉 B+ 树深度极浅,3 层即覆盖十亿行级别。

主键索引 vs 二级索引

维度主键索引(聚簇 / clustered)二级索引(secondary)
叶子节点整行数据主键值
查询路径一次定位命中后 回表 到主键索引

覆盖索引可避免回表,见 §05。

自增主键的两条好处

  • 写性能:始终追加,不挪记录,不触发页分裂。
  • 存储:主键短 → 二级索引叶子节点小,整体空间小。

何时业务字段适合做主键

KV 场景:只有一个唯一索引且就是它,没有二级索引带来的回表与膨胀代价。

叶子节点存的是什么

InnoDB 叶子节点 = 一个 (默认 16KB),页里存多行 行数据。“叶子存页"是结构描述;“叶子存行"是内容描述,二者并不矛盾。

为什么二级索引不直接存行地址

MyISAM 走的就是这条路(“堆组织表”)。InnoDB 选主键而非地址,是为了 避免页分裂时所有索引都得改主键记录地址

05 | 深入浅出索引(下)

覆盖索引

查询所需列全部包含在索引中 → 无需回表。

SELECT * FROM T WHERE k BETWEEN 3 AND 5;

k 列值为 1, 2, 3, 5, 6, 7:引擎实际读 3 条(3 / 5 / 6 终止),Server 拿到 2 条 —— 这就是 explain 的 rows 与引擎扫描数不等的来源。

最左前缀原则

B+ 树索引可用最左前缀定位记录。

索引顺序的第一判据:如果调整顺序能少维护一个索引,就采纳。

常查 (name, age)(age) 时,建 (name, age) + (age),不是 (age, name) + (age) —— 前者总空间小。

索引下推(Index Condition Pushdown, 5.6+)

联合索引 (name, age)

SELECT * FROM tuser WHERE name LIKE '张%' AND age = 10 AND ismale = 1;
版本行为
< 5.6仅用 name 定位前缀,符合 张% 的全部回表后再筛 age
≥ 5.6 ICP索引内部就判 age = 10,不满足直接跳过,减少回表次数
image

联合索引按列顺序排序:(name, age) 先按 name,再按 age

第一性原则:在满足查询需求前提下,尽量少访问资源。

06 | 全局锁和表锁:给表加个字段怎么这么难?

全局锁

FLUSH TABLES WITH READ LOCK;    -- FTWRL:整库只读
-- ... 备份 ...
UNLOCK TABLES;

代价:

  • 主库执行 → 业务停摆。
  • 从库执行 → 不消化主库 binlog,主从延迟。

更优解:mysqldump --single-transaction 在 RR 下开事务,靠 MVCC 拿一致性视图,过程中可正常写。仍需 FTWRL 的场景:引擎不支持 RR(如 MyISAM)。

FTWRL vs set global readonly=true

维度FTWRLreadonly
副作用仅设全局读锁readonly 常被业务用作主备判断,影响面大
异常恢复客户端断开 → 自动释放客户端断开 → 长期 readonly,库不可写

表级锁两类

LOCK TABLES

LOCK TABLES T1 READ, T2 WRITE;
-- 当前线程:只能读 T1、读写 T2,其他表禁访
-- 其他线程:写 T1、读写 T2 都被阻塞
SHOW OPEN TABLES WHERE In_use > 0;

MDL(Metadata Lock)—— 隐式

操作加锁互斥关系
增删改查MDL 读锁读读不互斥
结构变更 DDLMDL 写锁读写、写写互斥(排他)

事务中的 MDL 锁在 语句开始时申请事务提交时才释放

典型雪崩:给小表加字段卡死全库

-- session1 长事务持有 MDL 读锁未释放
BEGIN;
SELECT * FROM T;                -- MDL 读锁

-- session3 发起 DDL
ALTER TABLE T ADD COLUMN c2 VARCHAR(255);
-- ← 拿不到 MDL 写锁,阻塞,并排到 MDL 等待队列首位

-- session4 后续任何对 T 的读写
SELECT * FROM T;                -- ← 排在 session3 之后,全部阻塞
image

写锁请求会阻塞 之后所有 对该表的读写请求 —— 一个长事务足以拖垮全库。

安全加字段

  1. 先解长事务information_schema.innodb_trx 找超时事务。
  2. DDL 加超时退避
ALTER TABLE tbl_name NOWAIT  ADD COLUMN ...;     -- 拿不到锁立即放弃
ALTER TABLE tbl_name WAIT 30 ADD COLUMN ...;     -- 最多等 30s

实测:MySQL 5.7.30 不支持 NOWAIT / WAIT;腾讯云 MySQL 5.7.18-txsql 支持。参见 腾讯云 NOWAIT 语法

Online DDL 的 LOCK 选项

ALTER TABLE T ADD COLUMN c2 VARCHAR(255) NULL, LOCK=EXCLUSIVE;
LOCK 选项并发查询并发 DML
NONE
SHARED
DEFAULT尽量都允许尽量都允许
EXCLUSIVE

07 | 行锁功过:怎么减少行锁对性能的影响?

两阶段锁协议

InnoDB 事务的行锁在需要时才加,直到事务结束才释放

由此导出一条工程规则:把最易冲突的锁放最后

例:转账 + 记日志(按 ③①② 顺序执行)

顺序步骤冲突度
③ 先记交易日志 INSERT
① 中扣顾客 A 余额
② 后加影院 B 余额(很多人同时买)

把"加影院余额"放最后,热点行锁持有时间从整段事务缩到末尾一小段。

死锁与检测

image
SELECT @@innodb_lock_wait_timeout;       -- 默认 50s(图中环境设为 7200 = 2h)
SELECT @@innodb_deadlock_detect;         -- 1 开启主动检测;触发回滚 1213

主动检测的代价:每个新被阻塞的线程要做 O(n) 环路检查。1000 并发更新同行 → 百万次量级的检测,CPU 烧穿。

热点行的解决:拆分多行,把一个余额账户拆成 N 个子账户分摊并发,应用层聚合。

08 | 事务到底是隔离的还是不隔离的?

BEGIN 不等于事务起点

BEGIN;                                       -- 仅声明,未启动
START TRANSACTION WITH CONSISTENT SNAPSHOT;  -- 立即拍快照,事务真正启动

BEGIN 之后第一条对 InnoDB 表的操作才真正开启事务。

两个"视图"别混淆

名称含义
viewCREATE VIEW 定义的虚拟表,调用时执行查询语句
consistent read viewInnoDB MVCC 用的一致性读视图,支撑 RC / RR 隔离级别

RR 隔离级别下,事务启动时拍 整库 快照。

transaction id

InnoDB 每个事务有唯一 transaction id,事务开启时向事务系统申请,严格递增。MVCC 用它判断数据行版本对当前事务是否可见。

– EOF –