Mysql相关的一些知识
MySQL 核心架构全解析
模块一:基础架构 (连接与服务层控制)
(注:工作在 Server 层的全局和表级防御机制归属于此模块)
维度一:按"锁的粒度"划分(管辖半径与并发度 Trade-off)
1. 全局锁 (Global Lock)
- 真实落地场景(极罕见兜底):
旧系统迁移/逻辑备份:凌晨 3 点,我们需要把一个历史包袱极重的旧 MySQL 集群,全量逻辑迁移到 TiDB。为了保证导出的 SQL 脚本里,订单表和支付流水表在绝对的同一秒钟是对齐的(不出现付了钱但没订单的幽灵数据),DBA 会捏着汗执行一次 FLUSH TABLES WITH READ LOCK (FTWRL),拿到全局一致性视图后火速导出。线上核心主库绝对禁用。
2. 表级锁 (Table-level Lock)
- 普通表锁:
- 真实场景: 几乎绝迹。偶尔在一些非常边缘的、没有任何并发的内部后台跑批系统(比如月底财务对账的临时表)中,为了省事直接锁表处理数据。
- 元数据锁 ( MDL - Metadata Lock):
- 真实场景(P0事故常客): 双十一大促前的在线表结构变更(Online DDL)。产品经理要求给 5 亿数据的订单表加个
discount字段。如果直接ALTER TABLE,MDL 写锁会瞬间卡死全站的下单读写。我们的真实打法是绕开它,使用gh-ost或pt-online-schema-change这种影子表双写工具,平滑完成变更。
致命阻塞:线上突发 MDL 锁导致连接数暴增打满,怎么紧急排查和处理?
- 面试题: 线上监控突然报警,数据库连接数瞬间飙到 5000 打满了,业务全线崩溃。排查发现是大量的
Waiting for table metadata lock。你作为 On-call 架构师,怎么在 1 分钟内止损? - 解法:
-
- 根因定位: 肯定是有一个长事务或者未提交的查询,拿住了某张表的 MDL 读锁,同时刚好有个 DBA 或自动化脚本发起了一次对该表的 DDL 操作(申请 MDL 写锁)。DDL 被阻塞后,后续所有对该表的普通增删改查(申请 MDL 读锁)全都会排队,导致连接雪崩。
-
- 紧急止损: 立刻执行
SHOW PROCESSLIST或者查sys.schema_table_lock_waits,找到那个处于alter table状态的线程,以及在它之前长时间处于sleep但未提交事务的"罪魁祸首"线程,直接KILL掉,释放锁资源,恢复线上业务。
- 紧急止损: 立刻执行
维度二:Server 层核心机制的断腕与妥协
架构的断腕:为什么 MySQL 8.0 彻底砍掉了查询缓存( Query Cache)?
- 面试题: 缓存明明是提升读性能的银弹,为什么 MySQL 官方在 8.0 版本极其坚决地把 Query Cache 连根拔起、彻底废弃了?
- Trade-off 视角:普通人以为: 缓存总比查硬盘快。架构师看到的是致命缺陷: Query Cache 的失效机制极其粗暴!只要这张表有任何一次更新,这张表相关的所有查询缓存都会被瞬间清空。在互联网"读写高并发"的场景下,命中率低得可怜,反而因为维护缓存带来了极大的锁竞争和 CPU 开销。
- 解法: 缓存不应该由底层的关系型数据库来做。的规矩是:把关系型 DB 降级为纯粹的"存储引擎",将缓存的工作上浮给 Redis/Memcached 等专业的中间件去做(这叫"服务解耦")。
内存 的红线:ORDER BY 排序导致服务器 I/O 爆炸
- 面试题: 你的 SQL 里写了
ORDER BY,数据量大了之后,查询速度突然从 10 毫秒掉到了 5 秒,底层的sort_buffer经历了什么绝望的挣扎? - 解法: MySQL 会为每个线程分配一块内存叫做
sort_buffer用于排序。
- 当排序数据量小于
sort_buffer_size时,在内存中完成(极快)。- 会导致的问题: 当数据量大于这个值时,内存排不下了!MySQL 被迫利用磁盘生成多个临时文件,在磁盘上做"归并排序(Filesort)"。磁盘的随机 I/O 会把性能瞬间拉垮。
- 解法: 绝不依赖数据库做大量数据的动态排序!必须通过建立联合索引,让拿出来的数据天生就是有序的,直接绕过
sort_buffer。
模块二:存储引擎 (InnoDB 特性:行锁与 MVCC)
维度一:按"锁的范围"划分
1. 意向锁 ( Intention Lock - IS/ IX ):
- 真实场景(底层引擎的探照灯): 这是 InnoDB 的底层优化机制,业务开发感知不到。它存在的意义是,当 DBA 真的需要给整张表做结构调整时,引擎看一眼表头有没有意向锁,就知道里面有没有极其密集的行锁正在执行(相当于厕所的门口挂了个"正在使用"的牌子),直接拒绝操作,而不是傻乎乎地去遍历 5 亿行数据。
2. 行级锁 (Row-level Lock)
- Record Lock(记录锁):
- 真实场景(绝对主力): 千万级 QPS 的高并发单点更新。比如抖音用户修改自己的个性签名,或者用户下单扣减自己的账户余额。只要 SQL 精准命中了主键或唯一索引(
WHERE user_id = 123),引擎就只锁这一行,全站千万用户的并发互不干扰,TPS 拉满。
- 真实场景(绝对主力): 千万级 QPS 的高并发单点更新。比如抖音用户修改自己的个性签名,或者用户下单扣减自己的账户余额。只要 SQL 精准命中了主键或唯一索引(
- Gap Lock(间隙锁):
- 真实场景(防插队神器): 后台异步任务扫描。比如定时任务每分钟去扫一遍
status = 'WAIT_PAY'的订单进行关单操作。间隙锁会自动锁住这些订单前后的空气,防止在扫描的这几百毫秒内,有新的待支付订单"插队"进来导致漏处理。
- 真实场景(防插队神器): 后台异步任务扫描。比如定时任务每分钟去扫一遍
- Next-Key Lock(临键锁):
- 真实场景(范围发奖兜底): 直播间排行榜发奖励。运营要求给排行榜第 10 名到第 20 名的主播批量发流量券。
UPDATE ... WHERE rank >= 10 AND rank <= 20执行时,临键锁会把这个区间的记录和空气全部死死封住。哪怕发奖期间主播积分突变,也绝对进不来、出不去,保证发奖名单严丝合缝。
- 真实场景(范围发奖兜底): 直播间排行榜发奖励。运营要求给排行榜第 10 名到第 20 名的主播批量发流量券。
通俗推演:临键锁"左开右闭"区间防范灾难场景
假设数据库是一条长长的走廊,用户按积分站成一排。目前只有三个人:路人甲(90分)、路人乙(95分)、路人丙(100分)。
老板下令:UPDATE ... WHERE score > 90 AND score <= 100;(给 90 分不含到 100 分含的人发 5 块钱)。
InnoDB 保安拉起的警戒线(Next-Key Lock)长这样:
- 第一道警戒线
(90, 95]:从 90 分脚后跟拉起,封死空走廊,捆住路人乙(95)。 - 第二道警戒线
(95, 100]:从路人乙脚后跟拉起,封死空走廊,捆住路人丙(100)。 - 如果不拉这些警戒线,走廊里会发生什么可怕的事情?
- 灾难一:不防范插入(警戒线没封住空走廊 -> 幻读 )
没封走廊,发钱这半秒钟进来个打赏到 92 分的新用户。等老板来查账,发现 90-100 之间多出个人没拿到钱。警戒线的空走廊锁(Gap Lock)就是为了让 92 分的小伙子在外面排队阻塞。
- 灾难二:不防范修改(警戒线没捆住人本身 -> 不可重复读 /数据错乱)
没捆住路人乙本人。你刚要发钱,他买个礼物积分掉到 80 跑出去了。你发钱逻辑崩溃。警戒线里的 ] 闭区间(Record Lock)就是为了把他死死钉在原地。
维度二:按"锁的模式"划分(读写互斥的底层逻辑)
1. 共享锁 (Shared Lock / S锁 / 读锁 )
- 真实场景(强一致性关系约束): 父子表的外键校验替代品。在字节,我们严禁在数据库里建真实的外键约束(性能太差)。但业务上,当我们要插入一条"订单明细"时,必须保证"商品主表"里的商品没被下架删除。此时会先执行
SELECT * FROM products WHERE id = 1 FOR SHARE给商品加读锁,确保写订单明细的瞬间,谁也不能把这个商品删掉。
2. 排他锁 (Exclusive Lock / X锁 / 写锁 )
- 真实场景(资产强锁定): 金融核心交易链路。比如抖音支付的转账核心逻辑。在计算余额和生成流水之前,必须使用
SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE强行霸占这一行。在当前事务提交前,任何其他扣款请求(比如打赏、买东西)统统在外面排队等待,保证资金的绝对安全。
维度三:按"架构思想"划分(高并发实战流派)
1. 悲观锁 (Pessimistic Lock)
- 真实场景: 低并发但极高价值的内部资金流转。比如对公账户的财务大额划拨、供应链系统的核心出入库单据审批。宁可排队变慢,也要用
FOR UPDATE悲观锁把并发请求强制串行化。
2. 乐观锁 (Optimistic Lock)
- 真实场景(性能核武器): 双十一电商秒杀。10万人瞬间抢 100 个库存。如果用悲观锁,数据库直接原地爆炸。我们的打法是在表中加一个
version字段,执行UPDATE stock SET count = count - 1, version = version + 1 WHERE id = 1 AND version = 当前拿到的版本号。只要版本号对不上,SQL 就返回 0 行受影响。数据库不仅没被锁死,还帮我们高效过滤掉了 99.9% 的无效流量。
维度四:MVCC (多版本并发控制)
1. MVCC 底层原理解构:多重宇宙与照相机
你可以把 MVCC 想象成"多重宇宙"。每当有人修改数据,MySQL 不会直接把老数据抹掉,而是把它扔进另一个平行宇宙里存起来。支撑运转的三大核心组件:
- 隐藏字段(时光烙印):
DB_TRX_ID(最后修改的事务 ID) 和DB_ROLL_PTR(指向 Undo Log 老版本的指针)。 - Undo Log (版本链): 老版本数据像锁链一样串起来,形成版本链。
- Read View(一致性视图 / 照相机): 执行快照读的那一刻拍下的全局快照。包含
m_ids(活跃事务名单)、min_trx_id(活跃里最小的) 和max_trx_id(下个新事务ID)
场景代入:老板查岗与 员工打卡 (核心裁决逻辑)
下午 3 点老板进门查岗(生成 Read View)。记住三个信息:活跃名单 [12, 15],最早还在加班的 12,下一个还没来的新员工 18。
拿着桌上的报告(Undo Log 里的数据版本),套用三步判断:
- 老前辈干的 ( DB_TRX_ID < 12**):** 报告作者是 10 号。既然最小的加班狗是 12,说明 10 号早就干完溜了(已提交)。可见!
- 未来穿越者干的 ( DB_TRX_ID >= 18**):** 报告作者 20 号。人事部说新人排到 18 号,20 号肯定是我进门之后才来的。不可见!
- 刚好落在中间 ( 12 <= DB_TRX_ID < 18**):**
- 情况 A(在活跃名单里,如 15): 进门时他还在写(未提交)。不可见!
- 情况 B(不在活跃名单里,如 14): 介于 12 和 18 之间,且不在加班名单里,说明进门前他已经光速干完溜了(已提交)。可见!
面试官:详细说一下 MVCC 吧。
答:面试官,你好!对于MVCC多版本并发控制,我的理解是这样的。MVCC主要是InnoDB为了提升数据库并发性能而引入的一种机制。其核心价值在于读写、读读 冲突时无阻塞。也就是说通过保留数据的历史版本,读操作通过去读老版本,写操作去修改新版本,从而大幅提高mysql在并发高时候的效率。而对于写写 操作,事务之间仍然需要锁来进行排队等待。
而MVCC底层实现的核心机制主要靠三个组件来实现:隐藏字段、undolog、ReadView。第一是隐藏字段,mysql会在真实数据的后面增加几个隐藏的字段,其中最重要的两个字段就是trx_id(最后一次修改该行数据的事务id),以及指向版本链回滚指针。第二是undolog, 当一行数据被多个事务修改时,MySQL 会把修改前的旧状态记录在 Undo Log 中。通过刚才说的回滚指针,这些旧版本数据会首尾相连,串成一条单向的版本链。第三是 ReadView(读视图): 这是可见性判断的核心。相当于事务在执行查询的那一瞬间,系统在内存里拍下的一张'当前活跃且未提交的事务黑名单'。
最后,串联一下整个 工作流 (闭环): 当我们执行一条查询语句时,系统会拿着生成的 ReadView 去比对数据行上的 trx_id。 如果发现这行数据的最后修改人,刚好存在于 ReadView 的黑名单里,说明这个修改还没提交,或者是在我生成快照之后才发生的,那么这个最新版本对我就是不可见的(防止脏读)。 此时,系统就会顺着回滚指针,顺藤摸瓜去 Undo Log 的版本链里找更老的版本,直到找到第一个 trx_id 不在黑名单里的安全历史版本,并把它返回给用户。
这就是我理解的 MVCC 核心工作流程。"
2. 面试题与回答 ( MVCC /锁相关)
核心考点 1: RC 和 RR 隔离级别,底层都是 MVCC ,为什么表现不一样?
- 普通回答: RC 解决脏读,RR 解决不可重复读。
- 进阶答法: 核心区别仅仅在于"生成 Read View(照相机拍照)的时机不同"!RC 级别下,事务中每一次 SELECT 都会重新生成 一个全新的 Read View;RR 级别下,只有在事务的第一次 SELECT 时才会生成,之后整个事务全部复用这一张底片。
核心考点 2: MVCC 能彻底解决 幻读 吗?
- 答法: 不能一概而论。对于纯粹的快照读(普通 SELECT),MVCC 可以彻底解决幻读。但是,一旦业务代码里夹杂了当前读(如
UPDATE或FOR UPDATE),MVCC 就失效了。此时 MySQL 必须祭出 Next-Key Lock 物理武器来兜底防范。
核心考点 3:长事务 (Long Transaction) 引发的血案
- 案发现场: DBA 报警核心订单库磁盘疯狂吃紧,慢查询雪崩。
- 底层剖析: 有个长达 2 小时的长事务没有 COMMIT。导致这 2 小时内全站几千万笔订单的历史版本全部堆积在 Undo Log 里无法被 Purge 线程清理(因为长事务的 Read View 还在起效)。普通的 SELECT 被迫遍历几万层的版本链,CPU 崩溃。
- 解法:
KILL掉幽灵事务。定下铁律:严禁大事务,复杂 API 耗时操作移出事务边界。
核心考点 4: 死锁 ( Deadlock ) 的线上排查与破局
- 答法: 线上死锁不是加
try-catch重试那么简单。
- 止损与排查: 通过
SHOW ENGINE INNODB STATUS查看LATEST DETECTED DEADLOCK,精确定位是哪两句 SQL 形成了 ABBA 环路等待。 - 根因根治: 绝大多数死锁是因为跨表/跨行加锁顺序不一致。必须在代码规范层面,强制要求按照主键 ID"从小到大"的顺序依次加锁,从根本上打破环路等待。
间隙锁边界: RR 级别下,对二级索引加锁,间隙锁的范围到底是怎么确定的?
- 面试题: 表里有三个数据,
age列建了普通二级索引,值分别是 10, 20, 30。对应主键 ID 分别是 1, 2, 3。我执行SELECT * FROM users WHERE age = 20 FOR UPDATE;,请问锁住了哪些范围?插入age=15, id=4能成功吗? - 解法: 间隙锁在二级索引上的范围是根据"索引值 + 主键ID"来确定的。
它不仅锁住 age=20 这条记录,还会向左找到第一个不满足条件的值 10,拉起间隙 (10, 20);向右找到第一个不满足条件的值 30,拉起间隙 (20, 30)。
由于还需要考虑主键 ID 的排序,实际锁住的范围是 (age=10, id=1) 到 (age=30, id=3) 之间的所有空间。插入 age=15, id=4 会落在这个间隙内,被阻塞,无法成功。
模块三:索引机制 (查询速度的底座)
- 什么是索引?为什么要用 B+ 树?
- 物理直觉(查字典): 索引本质就是"以空间换时间"的目录。
- 为什么是 B+ 树? 数据库的瓶颈在磁盘 I/O。B+ 树被设计成了"又矮又胖"。非叶子节点(枝干)只存目录指针不存真实数据,一页(16KB)能塞上千个指针。千万级数据,B+ 树只有 3 层高,找一条数据最多查 3 次硬盘。叶子节点用双向链表串联,极度适合范围查询。
- 聚簇索引 vs 二级索引(回表的痛点)
- 聚簇索引 ( 主键 索引): "数据和索引长在一起"。叶子节点装的是完整的整行真实数据。
- 二级索引( 非聚簇索引 ): 叶子节点只存【索引字段的值】+【对应的主键 ID】。
- 灾难与开销:回表 (Bookmark Lookup)
执行 SELECT * FROM users WHERE name = '张三' 时:
-
去 name 索引树找,拿到主键 ID = 1001。
-
因为你要的是
*,只能带着 ID=1001 跑回主键的聚簇索引树里重查一次。 -
"跑回去重查"就是回表。如果回表数据量巨大,随机 I/O 成本极高,甚至不如直接全表扫描!
-
面试题
核心考点 1:锁粒度降级与灾难防范
- 面试题: UPDATE 语句带了 WHERE 条件,为什么执行时却把整张表锁死了?
- 答法: 根本原因是索引失效或缺失 。InnoDB 的行锁是锁在索引树节点上的。如果
WHERE字段没走索引(比如隐式类型转换),引擎被迫进行全表扫描(Full Table Scan)。为了保证扫描期间不出乱子,InnoDB 会粗暴地给表里所有行加记录锁,所有间隙加间隙锁,宏观上等于锁死了整张表。
核心考点 2:B+ 树的妥协,为什么不用 哈希表 ?
- 答法: 哈希表单点查询 O(1) 确实快,但绝对不支持范围查询(Range Query)和排序。业务充斥着
age > 18这种 SQL,哈希表直接懵逼。B+ 树的妥协是为了通用性,牺牲极致点查换取范围横扫能力。
核心考点 3:物理地址 vs 主键 ID 的极致 Trade-off
- 面试题: 为什么 InnoDB 二级索引非要存主键 ID,再去回表?像 MyISAM 一样直接存"物理地址"不是更快吗?
- 解法(一剑封喉): 在高并发写入时,B+ 树为了保持平衡会极其频繁地发生"页分裂"和"节点合并",导致真实数据在磁盘上的物理位置疯狂改变!如果存物理地址,一旦数据移动,几棵二级索引树全要跟着大改,写入性能灾难。主键 ID 永远不变,这就是"牺牲局部读性能(回表),保全全局写稳定性"的终极智慧。
极限优化:什么是 索引下推 ( ICP )?什么场景会用到?
- 面试题: 我建了联合索引
(name, age),查询SELECT * FROM users WHERE name LIKE '张%' AND age = 20。在 MySQL 5.6 之前和之后,底层的执行流程有什么巨大的性能差异? - 解法:
- 5.6 之前: 引擎层通过
name LIKE '张%'匹配到所有姓张的记录的主键 ID,然后把这些 ID 全都回表 查出完整数据,丢给 Server 层。Server 层再去判断age = 20。如果姓张的有 10 万人,就要发生 10 万次回表,磁盘直接爆炸。 - 5.6 之后的黑科技( ICP 索引下推 ): Server 层把
age = 20这个过滤条件直接"下推"给存储引擎。引擎在二级索引树遍历时,发现既然节点里本身就存了age的值,直接就在索引树上把age != 20的人过滤掉!可能最终符合条件的只有 5 个人,那就只进行 5 次回表。性能瞬间提升千万倍。
模块四:事务机制 (ACID特性与隔离性保障)
- MySQL 不同隔离级别中可能出现的问题
- 脏读 ( Dirty Read ) ------ 读到了"薛定谔的数据"
- 问题场景: 退款系统正在给张三退 50 块钱,还没 COMMIT。你的客服系统查到了变多后的余额,告诉用户退款到账。下一秒退款系统异常 Rollback 了。用户拿着假截图投诉。这就是脏读。绝对禁止使用 RU 级别。
- 不可重复读 ( Non-Repeatable Read ) ------ 同一个事务里的"背刺"
- 问题场景: 月底对账脚本跑了 10 分钟。第一分钟查张三有 1000 块。第 5 分钟张三消费了 500 块并提交。第 10 分钟对账脚本再次查张三,只剩 500 块了。同一个事务里两次读数据不一样,逻辑彻底崩溃。
- 幻读 (Phantom Read) ------ 凭空出现的"幽灵"
- 问题场景: 你统计满 100 元打赏的人发奖,查出来 5 个。准备发奖瞬间,土豪新建了一条 100 元打赏记录(INSERT并提交)。你再次查变成了 6 个。
- 四大隔离级别解构与实战
- 读未提交 ( RU ): 裸奔,绝对禁用。
- 读已提交 ( RC ): 每次 SELECT 生成新快照。防脏读。
- 真实场景(核弹级重点): 阿里、字节等核心互联网业务,经常强制把 MySQL 默认的 RR 降级为 RC ! 因为 RC 级别下 InnoDB 会大幅度削弱间隙锁(Gap Lock)。没有了间隙锁,大面积锁冲突和死锁概率断崖式下降,数据库吞吐量瞬间起飞。不可重复读问题完全交由业务层(如乐观锁)兜底。
- 可重复读 ( RR ): 第一次 SELECT 生成全局快照并复用到底。防脏读、不可重复读。
- MySQL 的"越级防御": 通过 MVCC 和 Next-Key Lock,极其生猛地把幻读也防住了。这是 MySQL 默认级别。
- 串行化 ( Serializable ): 全局排队单线程,追求效率绝对禁用。
事务拆分:为什么规范里强烈禁止大事务?除了锁时间长,对底层还有什么致命影响?
- 面试题: 把 RPC 远程调用包在数据库事务里,除了锁住行导致并发降低,还会引起什么更深层的雪崩?
- 解法:
-
Undo Log 膨胀( OOM 危机): 前文提到,长事务会导致 Read View 一直存活,历史版本数据无法被 Purge 线程回收,撑爆 ibdata1 或独立表空间。
-
连接池 耗尽( 网络层 灾难): RPC 调用往往伴随着不可控的网络延迟。事务长达几秒,意味着数据库的 Connection(连接)在这几秒内被死死霸占。突发流量一涨,应用层的连接池瞬间被打满,导致整个服务直接拒绝响应(502 Bad Gateway)。
模块五:日志系统 (持久化与 WAL 机制)
- 核心大前提:Buffer Pool (内存池)
数据库所有的增删改查,绝对不是直接操作磁盘文件,太慢了。MySQL 会把数据页加载到内存的 Buffer Pool 里飞速修改。改完后的内存数据叫"脏页(Dirty Page)"
- 日志的诞生:WAL 机制 (Write-Ahead Logging)
- 痛点: 突然停电,内存里的"脏页"全丢,毁灭性灾难。
- 解法 ( WAL ): 既然不能每次都把脏页随机写回磁盘,那就搞个"小本子"。内存改完,立刻在小本子后面追加记录:"把张三余额改成了 50"。追加写是"顺序写磁盘",极快。停电了不怕,重启照着本子重放一遍。先写日志,再刷磁盘。
- 认清两大主角:Redo Log 与 Binlog
- Redo Log (重做日志 - 物理保命):
- 谁写的: InnoDB 引擎独有。
- 记了什么: 物理层面"在第 X 页偏移量 Y 修改了值 Z"。
- 怎么写的: 固定大小、循环写(环形数组,像摩天轮)。只负责保证本机 Crash-Safe(崩溃恢复)。
- Binlog (归档日志 - 逻辑全知):
- 谁写的: Server 层,所有引擎通用。
- 记了什么: 逻辑层面"执行了 UPDATE 语句"。
- 怎么写的: 一直追加写,绝不覆盖。用于给从库同步数据,或用于回滚半个月前的数据。
组提交 (Group Commit):解决 2PC 的性能梦魇
- 解法: 如果每秒 1 万并发,每个事务提交都 fsync 刷盘,磁盘必炸。底层搞了"排队上车"机制。事务 A 先准备好,作为队长等一会儿。B 和 C 也进来了,队长 A 把三个事务的日志打包,调用一次底层 I/O 刷盘。将昂贵的 I/O 成本分摊,极大提升 TPS。
极限容灾: MySQL 崩溃恢复(Crash Recovery)的具体流程?Redo 和 Binlog 是怎么在两阶段提交中配合校验的?
- 面试题: 如果在执行两阶段提交(2PC)的半路,比如刚写完 Redo Log 处于 Prepare 状态,还没写 Binlog 机器就断电了,重启后 MySQL 怎么处理这笔烂账?
- 解法: 重启时,InnoDB 引擎会扫描 Redo Log。
- 如果发现 Redo Log 里已经有了
commit标签,说明已经彻底完成,直接恢复。 - 如果发现只有
prepare标签,说明半路夭折了!此时引擎会拿着这笔事务的 XID(事务号)去 Binlog 里查。 - 如果 Binlog 里有这个 XID 的完整记录: 说明数据已经同步给从库了,为了保证主从一致性,引擎决定继续提交(Commit)这个事务。
- 如果 Binlog 里根本没找到这个 XID: 说明不仅主库没写完,从库肯定也不知道。为了干净利落,引擎决定直接回滚(Rollback)这个事务。
- 如果发现 Redo Log 里已经有了
模块六:高可用架构 (主从复制与灾备)
千万级并发的核心是"克隆术"------一主多从与读写分离。
主库 (Master) 唯一负责写,几十个从库 (Slave) 共同分摊读压力。
- 主从复制的"三步曲"与"三个线程"
- 步骤一:主库发货 ( Log Dump 线程): 主库上有更新写入 Binlog,Log Dump 线程就像发货员,立刻打包推给从库。
- 步骤二:从库收货 ( I/O 线程): 接收 Binlog 数据,原封不动写入本地的 Relay Log(中继日志)。
- 步骤三:从库拆箱重放 ( SQL 线程): 盯着 Relay Log,有新记录就拿出来,在从库上原样执行一遍。
- 复制模式的演进
- 异步复制 (默认): 主库发完就走,不管死活。主库挂了可能丢数据。
- 半同步复制 (Semi-sync): 主库发完必须等,至少一个从库返回 ACK(我收到了)才认为成功。保住了数据,但写入变慢。
读写分离的阿喀琉斯之踵 ("读自己写的"不一致)
- 面试题: 用户下单(写主库),跳转列表页(查从库)。主从延迟 1 秒,列表页空空如也。怎么解决?
- 解法:
- 方案 A(Redis 过期标记): 下单后在 Redis 写个
order_sync:1,过期 3 秒。列表页先查 Redis,如果有标记,中间件强制把该用户的查询路由回主库。3 秒后标记消失,恢复读从库。 - 方案 B(GTID 追踪): 客户端带走写事务的 GTID,查的时候传给从库。从库核对:如果自己已经回放过这个 GTID,允许读;如果没有,阻塞等几百毫秒,或者路由回主库。
- 方案 A(Redis 过期标记): 下单后在 Redis 写个
主从延迟高达几秒甚至数小时,怎么彻底解决?
- 面试题: 除了升级硬件,当大促期间发生极其严重的延迟,甚至触发了 MTS (多线程并行复制) 都扛不住时,架构层还有什么终极杀招?
- 解法:
- 数据库层: 如果是因为大事务导致从库执行慢,回归铁律:拆分大事务。如果是大批量 DDL,必须在业务低峰期通过高可用工具(如
gh-ost)执行。 - 缓存前置削峰: 把海量的并发写请求拦截在 Redis 层,通过 MQ 异步削峰慢慢落库,从源头降低主库的 Binlog 生成速度。
- 异构数据同步(终局): 放弃 MySQL 自身的从库作为读流量主力。使用 Canal 或 Flink CDC 订阅主库 Binlog,将数据实时清洗并投递到 ElasticSearch (用于复杂检索) 或 HBase/Redis 中,实现彻底的"读写异构分离"。
- 数据库层: 如果是因为大事务导致从库执行慢,回归铁律:拆分大事务。如果是大批量 DDL,必须在业务低峰期通过高可用工具(如
MySQL 千万级全链路性能调优与架构演进
当线上业务面临极高并发时,性能瓶颈的排查和优化绝不能仅凭感觉。本 SOP(标准作业程序)将调优过程分为"榨干单机性能"与"突破物理架构"两个阶段,沉淀了在真实大促和容灾场景下的核心打法。
第一阶段:单机 SQL 与引擎层调优 (榨干 MySQL 最后一滴血)
1. 全链路监控:跳出"慢查询日志"的盲区
- 踩坑场景: 抖音直播间某头部大 V 开播,瞬间涌入百万人。此时数据库 CPU 瞬间飙到 100%,但你去查
long_query_time = 0.5s的慢查询日志,发现里面空空如也! - 真相剖析: 压垮数据库的根本不是单条耗时几秒的慢 SQL,而是几万条耗时仅仅 0.05s 的"看似正常"的 SQL 在同一秒并发砸了过来(如热点缓存击穿)。海量的并发连接导致 CPU 极度频繁地进行线程上下文切换和锁竞争,CPU 满载,但单条 SQL 依然没触发慢查询阈值。
- 落地打法: 绝对不能只看慢查询日志,必须依赖 APM(应用性能管理)监控大盘。一旦发现大盘 QPS 和 CPU 曲线出现"倒挂"(CPU 飙升但 QPS 上不去),立刻拉响 P0 警报。通过限流组件(如 Sentinel)在网关层丢弃部分请求,死保核心交易主库。
- 面试题: 数据库 CPU 满载时,为什么前端用户感知到的是网关超时或 502 报错?
- 解法: 这是一个经典的全链路雪崩。数据库 CPU 打满 -> SQL 响应变慢 -> 应用层连接池(如 HikariCP)连接迟迟无法归还 -> 新请求拿不到连接被迫阻塞 -> Tomcat/Netty 工作线程池耗尽 -> 微服务假死,网关层等待超时抛出 502。解法: 必须配置合理的数据库获取连接超时时间(
connectionTimeout),让请求快速失败。
2. 精准诊断:EXPLAIN 与执行计划的绝杀
- 踩坑场景: 负责视频评论区列表接口,加了联合索引。预发环境
EXPLAIN显示走了索引(type = range),但接口耗时高达 2 秒。 - 真相剖析: 没看
Extra字段里的Using filesort。业务要求按时间倒序排,但联合索引(video_id, create_time)建立时默认是 ASC(升序)。MySQL 不得不在内存的sort_buffer里把几十万条数据重新倒腾一遍。 - 落地打法: 利用
sys.statement_analysis视图精准定位耗时在 CPU 而非 I/O。
- MySQL 8.0 终极解法: 直接利用降序索引新特性,重建索引为
(video_id ASC, create_time DESC),彻底消灭Using filesort,耗时瞬间降至毫秒级。
3. 实战避坑:隐式转换引发的索引大雪崩
- 踩坑场景: 客服后台按手机号查异常订单。SQL 为
SELECT * FROM orders WHERE phone = 13800138000;。订单表一亿条数据,phone字段有唯一索引,结果数据库直接卡死宕机。 - 真相剖析: 低级却致命的失误!
phone是VARCHAR(20),但 SQL 传的是整型数字(没加单引号)。MySQL 遇到字符串和数字比较,会强制将字符串转为数字,底层变成SELECT * FROM orders WHERE CAST(phone AS SIGNED) = 13800138000;。对索引字段使用函数,B+ 树目录直接失效,被迫退化为一亿行的全表扫描。 - 落地打法: 严格要求在 ORM 框架层(如 MyBatis、GORM)做好类型强校验,严禁透传未经格式化的参数。
第二阶段:突破单机物理瓶颈的架构调优
当无论怎么优化 SQL 和索引,单机磁盘 I/O 和 CPU 都已经到达物理极限时,必须进行架构层面的升维打击。
1. 读写分离与主从延迟的极限拉扯
- 真实痛点: 用户刚充值完金币(写主库),马上刷新页面(读从库),主从同步有 500ms 延迟,页面显示余额没变,用户直接打客服投诉。
- 兜底措施:
- 强制路由主库: 对一致性要求极高的请求(如支付后查余额),利用中间件(如 ShardingSphere)加 Hint,强制路由到主库。
- 缓存假象兜底: 充值成功后,将最新余额写入 Redis 并设置 2 秒过期。读请求先查 Redis,查不到再去从库。利用这 2 秒的时间差完美掩盖主从延迟。
2. Redis 高并发缓存防御体系
- 真实痛点: 遇到"缓存击穿"(热点数据突然过期),瞬间万级并发请求直接打穿 Redis 砸向 MySQL,数据库秒挂。
- 兜底措施:
- 合并回源 (Singleflight) / 分布式锁 : 发现没缓存时,利用 Redis 分布式锁只放行第一个线程去查 DB 重建缓存,其余 9999 个线程阻塞等待 50ms 后直接读新缓存。
- 双写一致性保障: 坚决废弃"先更新 DB,再更新缓存"。标准方案是 Cache Aside Pattern :先更新 DB,然后删除缓存。为防并发脏数据,最优解是利用 Canal 监听 MySQL Binlog 进行异步延时双删,彻底解耦业务。
3. 分库分表 (Sharding) 的拆分与基因路由
- 真实痛点: 订单表超 5000 万,决定按
user_id取模分出 1024 个表。但商户要根据order_id查订单时,难道要并发盲搜 1024 个表? - 兜底措施:
- 基因法(ID 融合): 通俗比喻: 1024 个小柜子按客户手机号尾数放合同。老板拿"合同流水号"来找,盲查太慢。聪明做法:生成流水号时,故意把手机号最后 4 位拼接到流水号末尾。拿到流水号看最后 4 位就能找到对应柜子。此处如果是雪花算法,则不能进行简单的将后几位替换为基因片段,而是需要根据业务需求重构雪花算法。比如说:老图纸(传统雪花): 1空位 + 41车位给时间 + 10车位给机器 + 12车位给序列 = 64,新图纸(魔改): 1空位 + 41车位给时间 + 5车位给机器 + 7车位给序列 + 10车位给基因 = 64。再牺牲一些机器码和自增序列位就行了。
- 技术映射: 生成
order_id时,强行替换其最后几位 bit 为user_id的后 4 位二进制"基因"。无论拿哪个 ID 取模,都会精准落到同一个物理表。
- 技术映射: 生成
- 异构索引表: 建立一张只有
(order_id, user_id)的极简映射表(可放 Redis),先通过order_id查出user_id(路由键),再去目标分表查全量数据。
4. 复杂检索引擎降维打击 (CQRS 读写异构)
- 真实痛点: 运营后台需要根据"时间范围 + 订单状态 + 用户昵称模糊匹配 + 商品分类"进行动态组合查询。MySQL 建一万个联合索引也扛不住
LIKE '%XX%'的多维检索。 - 兜底措施:读写异构
- 将 MySQL 彻底退化为单纯的 OLTP 引擎,只负责按 ID 增删改查和事务。
- 引入 Flink CDC 或 Canal 实时监听 MySQL 的 Binlog 变更流,将数据准实时同步到 Elasticsearch 甚至 ClickHouse 中。所有的复杂后台检索,全部走 ES 的倒排索引。彻底解耦读写,保卫核心 DB。
举个例子:
- 灾难现场(没用这套架构前): 双十一大促结束了,公司的运营主管跑来提了一个需求:"给我导出一份报表,要求:订单时间在过去一周内 + 订单状态是已退款 + 用户的收货地址包含'朝阳区' + 买了'电子产品'类目"。
- 如果你让 MySQL 去干这个活,你的 SQL 里会写满各种
AND、OR和灾难性的LIKE '%朝阳区%'。 - MySQL 优化器一看直接崩溃,放弃所有索引,在拥有几亿条订单的单表(或分库分表)里进行全表扫描。结果就是,运营点了一下"查询"按钮,线上千万级用户的支付接口全挂了,因为整个数据库的 CPU 被拉爆了。
- 架构重构(应用了这套架构后):
- 各司其职: 线上用户的支付、下单,依然全部打向 MySQL (只做纯粹的 OLTP ),保障资金安全。
- 实时同步: 我们引入 Canal 。线上只要有一笔新订单生成,或者订单状态从"已支付"变成了"已退款",Canal 瞬间从 Binlog 里抓到这笔变更,并立刻把它扔进消息队列(如 Kafka),最后写入到 Elasticsearch ( ES ) 里。
- 降维查询: 运营主管再次点击刚才那个极度复杂的查询按钮。这次,请求根本不会去碰 MySQL,而是直接打向 Elasticsearch。ES 利用它的"倒排索引",在 50 毫秒内就找出了所有符合条件的单子,大功告成。
- 如果你让 MySQL 去干这个活,你的 SQL 里会写满各种
不要试图用一个组件解决所有问题,Trade-off(权衡)才是架构设计的灵魂。