深浅色
MySQL(背诵版)
1. B+ 树为什么适合做索引
- 非叶子节点只存 key + 指针,一个 16KB 页能放上千个 key,3 层就能覆盖千万行,树矮 -> 磁盘 IO 次数少。
- 叶子节点用双向链表串联,范围查询和
order by天然友好。 - 对比 B 树:非叶子也存数据,单页放的 key 更少、树更高,范围查询要跨层中序遍历。
- 对比哈希:只支持等值,不支持范围。
- 一句话:矮胖树 + 叶子链表 = 等值和范围都快。
2. 聚簇索引与二级索引
- InnoDB 主键即聚簇索引,叶子直接存整行;没主键用唯一非空索引,再没有就生成 6 字节 rowid。
- 二级索引叶子存主键值,查非索引列要回表再走一次聚簇索引。
- 覆盖索引:要查的列都在索引里,不用回表,Extra 显示
Using index。 - 实践:别
select *,把常用查询列做成联合索引实现覆盖。 - 追问:为什么推荐自增主键?-> 随机主键会页分裂和产生碎片,自增是顺序插入。
3. 最左前缀与索引失效
- 联合索引 (a,b,c):能用 a、a+b、a+b+c;跳过 a 直接用 b 不行。
- 失效场景:对索引列做函数/运算、隐式类型转换(字符串列传数字)、
like '%x'、or连非索引列、范围查询后面的列用不上。 - 优化:范围条件放最后;
like 'x%'可以用;必要时做覆盖索引或改写 SQL。 - 追问:
where a=1 and c=3能用 (a,b,c) 吗?-> 只用到 a,c 用不上。
4. explain 关键字段
type:好到坏system > const > eq_ref > ref > range > index > ALL,出现 ALL 要警惕。key:实际用的索引;possible_keys是候选。rows:预估扫描行数,越小越好。Extra:Using index(覆盖索引,好)、Using filesort(额外排序,差)、Using temporary(临时表,差)。- 追问:
Using filesort一定慢吗?-> 不一定,结果集小时无感,大了才要优化。
5. 隔离级别与 MVCC
- 四种:读未提交、读已提交(RC)、可重复读(RR,InnoDB 默认)、串行化。
- MVCC:每行有隐藏的
trx_id和roll_pointer指向 undo log 里的旧版本。 - Read View 决定哪个版本可见;RC 每次 select 都新建,RR 只在第一次建后复用——这就是 RR 可重复读的原因。
- 幻读:快照读靠 MVCC 避免,当前读靠间隙锁避免。
- 追问:RR 还会有幻读吗?-> 快照读不会;当前读靠临键锁也能避免。
6. 快照读与当前读
- 快照读:普通
select,走 MVCC,不加锁。 - 当前读:
for update/lock in share mode/update/delete/insert,读最新版本并加锁。 - 加锁规则:唯一索引等值命中 -> 行锁;非唯一索引或范围 -> 间隙锁/临键锁;没走索引 -> 扫描到的所有行都加锁。
- 追问:
update ... where id=5且 id 不存在会加什么锁?-> 加间隙锁,防止别的事务插入。
7. 行锁 / 间隙锁 / 临键锁与死锁
- Record Lock:锁索引记录本身。Gap Lock:锁记录之间的间隙,防插入,只在 RR 下存在。Next-Key Lock:记录 + 前面的间隙,RR 默认。
- 死锁排查:
show engine innodb status看LATEST DETECTED DEADLOCK;8.0 用performance_schema.data_locks。 - 预防:按固定顺序访问资源、缩小事务范围、条件必须走索引、
innodb_lock_wait_timeout兜底。 - 追问:死锁会自动解决吗?-> 会,InnoDB 检测后回滚代价小的事务。
8. redo / undo / binlog 与两阶段提交
- redo log:InnoDB 层,物理日志,循环写,保证崩溃恢复(WAL 先写日志再刷盘)。
- undo log:逻辑日志,记录反向操作,用于回滚和 MVCC。
- binlog:Server 层,逻辑日志,追加写,用于主从复制和数据恢复。
- 两阶段提交:写 redo(prepare)-> 写 binlog -> 提交 redo(commit),目的是让两份日志一致。
- 追问:不做两阶段提交会怎样?-> 两份日志可能不一致,主从数据出错。
9. 主从复制与延迟
- 流程:主库写 binlog -> dump 线程发送 -> 从库 IO 线程写 relay log -> SQL 线程重放。
- 延迟原因:从库单线程重放(5.7+ 支持并行复制)、大事务、从库压力大。
- 解决:半同步复制、MGR 组复制、写后读强制走主库、加缓存。
- 追问:怎么保证写完立刻能读到?-> 会话粘性走主库,或等 GTID 同步。
10. 分库分表
- 时机:单表数千万行、或磁盘/连接数到瓶颈(看 QPS 和延迟,不是硬指标)。
- 垂直拆分:按业务拆库、大字段拆表。水平拆分:按 user_id 取模/范围/一致性哈希。
- 带来的问题:跨库 join、分布式事务、全局 ID、分页排序、扩容迁移。
- 中间件:ShardingSphere、Vitess,或应用层路由。
- 追问:为什么不要一上来就分?-> 复杂度高,先靠索引、缓存、读写分离撑。
11. 慢查询定位与优化
- 开
slow_query_log+long_query_time,用pt-query-digest汇总。 - 流程:explain 看执行计划 -> 加/改索引 -> 改写 SQL -> 最后才动结构。
- 大表加索引:5.6+ Online DDL 加二级索引不锁表;8.0 加列可用
ALGORITHM=INSTANT;极端情况用gh-ost。 - 追问:为什么加了索引反而慢?-> 优化器选错索引、回表代价高、区分度太低。
12. Go 侧连接池
SetMaxOpenConns:最大连接数,默认无限,必须设。SetMaxIdleConns:建议与 MaxOpen 接近,避免频繁建连。SetConnMaxLifetime:要小于 MySQL 的wait_timeout(默认 8h),否则会用到被服务端断掉的连接。- 常见坑:忘了
rows.Close()、事务里用了db而不是tx。 - 追问:MaxIdle 太小会怎样?-> 每次请求都要重新 TCP 建连 + 认证,延迟高。