MySQL · Q & A with Arwen

1. 请求发出的背后

客户端发出sql —> 连接器校验身份 ---> 服务层 --->

  • 语法分析

    • 词法分析和语法分析是两步。词法分析把字符串拆成token (识别出SELECT、FROM、WHERE这些关键字),语法分析检查这些token的组合是否符合SQL语法规则, 做这件事的组件叫解析器(Parser)。它最终产出一棵语法树(AST)。
  • 查询缓存

    • 表一次更新缓存全部作废, 写多命中率低, 维护缓存成本大
  • 优化器

    • 优化器评估的是每种执行方案的IO成本和CPU成本。读多少页数据、 比较多少行,综合算出一个数值,选最小的那个方案。
    • 小表接大表
      • 假设A表10行,B表1000行。 A驱动B:外层扫10行,每行去B表查一次,查10次。 B驱动A:外层扫1000行,每行去A表查一次,查1000次。 如果被驱动表的连接字段上有索引,每次查的代价差不多。那外层循环次数越 少,总代价越小。 所以小表驱动大表本质上是在减少外层循环次数。
    • Explain
代码块PLAINTEXT · 31 行收起展开
	EXPLAIN SELECT * FROM user WHERE name = '张三';

  +----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
  | id | select_type | table | type | possible_keys | key      | key_len | ref   | rows | Extra |
  +----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
  |  1 | SIMPLE      | user  | ref  | idx_name      | idx_name | 102     | const |    3 | NULL  |
  +----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
  
  ┌───────────────┬────────────────────────────────────────────────────────────────────────────────────┐
  │     字段      │                                        含义                                         │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ id            │ 查询编号,子查询或UNION时会有多行,id越大越先执行                                         │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ select_type   │ 查询类型,SIMPLE=简单查询,SUBQUERY=子查询,DERIVED=派生表                               │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ table         │ 当前行在访问哪张表                                                                    │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ type          │ 访问类型,性能从好到差:const > eq_ref > ref > range > index > ALL                     │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ possible_keys │ 这条查询可能用到的索引有哪些                                                            │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ key           │ 优化器最终选了哪个索引,NULL就是没用索引                                                 │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ key_len       │ 用到了索引的多少字节,联合索引时可以判断用了几个字段                                        │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ ref           │ 索引列跟什么比较,const=常量,字段名=关联查询的连接字段                                    │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ rows          │ 预估扫描行数,越小越好                                                                 │
  ├───────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  │ Extra         │ 额外信息,重点关注:Using index(好)、Using filesort(差)、Using temporary(差)        │
  └───────────────┴────────────────────────────────────────────────────────────────────────────────────┘

2. 锁

间隙锁

假设有一张表 t,其中 id 是唯一索引,表里现在有 id 为 5, 10, 15 的三条记录。 当事务A在RR隔离级别下执行 SELECT * FROM t WHERE id = 7 FOR UPDATE; 时, 会加上一个间隙锁 (5, 10) 间隙锁必须要在索引上才生效, 索引是有序的节点列表, 而间隙锁的范围依靠节点来界定, 只有索引才能提供可计算的顺序区间。

记录锁

同样是基于上题的表 t (id 为 5, 10, 15 的唯一索引)。 如果在RR隔离级别下执行 SELECT * FROM t WHERE id = 10 FOR UPDATE;, 实际生效的锁是记录锁,仅锁住id=10这一条记录

意向锁

它是一个表级标记,用于让后续请求表锁的操作能快速判断是否冲突,避免逐行扫描。

  • 意向共享锁(IS):事务打算对某些行加共享锁前,先在表上加 IS
  • 意向排他锁(IX):事务打算对某些行加排他锁前,先在表上加 IX

事务在给行记录加X锁之前,必须先在表级别加上IX锁 这就是意向锁的定义。

3. 幻读

  • 行锁 = Record Lock = 锁具体某条索引记录
  • 间隙锁 = Gap Lock = 锁两条记录之间的区间

比如索引里有:

10 20 30

如果只加行锁:

锁住 20
锁住 30

那只是把这两条“点”锁住了。

但:

(20,30)

这个区间还是开的。
别人还能插入 25。

所以范围查询:

代码块PLAINTEXT · 1 行收起展开
SELECT * FROM t WHERE id BETWEEN 20 AND 30 FOR UPDATE;

如果没有 Gap Lock,第二个事务插入 25,你再查一次就多了一条新记录——幻读出现。/