00-总结
B+树做索引的优点
- 树的高度更矮、更胖,磁盘I/O次数极少:B+树的所有数据都存储在叶子节点,非叶子节点只存储索引键值和指向子节点的指针,MySQL每次查找数据最多只需进行3到4次磁盘I/O。
- 叶子节点是双向链表:如果是范围查询,比如查询id在10到20的数据,据库只需先快速定位到
10的位置,然后顺着链表找数据就可以。 - 查询性能极其稳定:在B+树中,所有真实的数据记录都存放在同一层(叶子节点),且叶子节点之间的距离相等。B+树的查询时间复杂度严格锁定为 O(log n),不会出现“运气好查得快,运气差查得慢”。
MVCC解决幻读了吗
结论:MVCC的首要目的就是解决“读写不阻塞”,MVCC没有彻底解决幻读它只解决了一部分。
为什么说MVCC没有彻底解决幻读它只解决了一部分呢?
- 普通 SELECT(快照读)—— MVCC 解决了幻读
- 在
REPEATABLE READ级别下,事务开启后执行的第一条SELECT会生成一个 执行update where id >10 此后该事务内所有的普通SELECT都基于这个快照查询。 - 例子:事务 A 开始,第一次查询
id>10得到 2 条记录;事务 B 此时插入一条id=11并提交;事务 A 再次查询id>10,因为读的是历史快照,依然只看到 2 条。 - 结论:在此场景下,幻读被 MVCC 完美避免了.
- 在
- SELECT ... FOR UPDATE / UPDATE / DELETE(当前读)—— MVCC 无效,靠间隙锁解决
- 当你执行
UPDATE、DELETE或加锁的SELECT ... FOR UPDATE时,读的是当前最新的数据版本(当前读),而不是快照。
总结:
- 当你执行
- 底层实现:MySQL InnoDB 的 RR 隔离级别 = MVCC(实现一致性非锁定读) + 间隙锁(Gap Lock,实现当前读的范围封锁)。
- 能否解决幻读:能! 在 MySQL InnoDB 里,RR 级别完全解决了幻读问题。
- 唯一遗留的坑:只有一种极其特殊的情况——“先快照读,再当前读”。如果事务 A 先用普通 SELECT 看到旧数据(假设 2 条),然后事务 B 插入新数据并提交,事务 A 再执行
UPDATE(当前读),此时UPDATE会影响到事务 B 新插入的那条数据。但这里发生的不是“读”出幻影行,而是“更新”了幻影行,这在业务逻辑上可能引发数据异常。虽然严格意义上不算“读幻读”,但这是 RR 下唯一需要注意的“半幻读”现象。
读写冲突问题
写-写冲突:多个事务修改同一条数据,任何隔离级别下都会阻塞,且是串行化的,这个阻塞与事务的隔离级别完全无关。这是 InnoDB 存储引擎保证数据物理一致性的底层强制规则。当事务 A 执行
UPDATE或DELETE时,会对涉及的行加上排他锁(X锁)。事务 B 如果也想修改这一行,必须申请 X 锁。由于 X 锁与 X 锁互斥,B 必须等待事务 A 提交或回滚释放锁后,才能继续执行。读写冲突:是否阻塞取决于隔离级别,这涉及“读写锁”和“MVCC(多版本并发控制)”的配合。
| 隔离级别 | 修改事务(加X锁) | 读取事务(普通SELECT) | 是否阻塞? | 读取到的数据 |
|---|---|---|---|---|
| 读未提交 | 未提交,持有排他锁(X锁) | 不加锁(脏读) | 不阻塞 | 未提交的脏数据 |
| 读已提交 | 未提交,持有排他锁(X锁) | 不加锁(一致性读) | 不阻塞 | readview每次查询重新生成。 |
| 可重复读 | 未提交,持有排他锁(X锁) | 不加锁(一致性读) | 不阻塞 | readview第一次查询生成。 |
| 串行化 | 未提交,持有排他锁(X锁) | 自动加共享锁(S锁) | 阻塞 | 必须等待修改事务提交后才能读 |
readview
Read View 生成之前已提交的事务,以及事务自身的修改。
执行引擎对比
| 对比维度 | InnoDB (MySQL 5.5+ 默认) | MyISAM (5.5之前默认) |
|---|---|---|
| 核心定位 | 支持事务、高并发、数据安全的企业级引擎 | 轻量级、读性能优先的非事务引擎 |
| 事务 (ACID) | 完全支持,有完整的提交、回滚和崩溃恢复机制 | 不支持,无事务概念 |
| 锁粒度 | 行级锁 (Row-Level Locking),并发写入性能高,冲突少 | 表级锁 (Table-Level Locking),写操作会锁全表,并发写入性能差 |
| MVCC | 支持,实现非锁定一致性读,读写互不阻塞 | 不支持,读操作也会阻塞写操作 |
| 外键约束 | 支持,强制数据参照完整性 | 不支持,语法兼容但引擎层不生效 |
| 数据存储 | 聚簇索引结构,数据和主键索引存储在一起(.ibd文件) | 非聚簇索引结构,数据(.MYD)和索引(.MYI)分开存储 |
| 缓存机制 | 通过缓冲池 (Buffer Pool) 同时缓存数据和索引页,内存利用率高 | 仅通过键缓存 (Key Cache) 缓存索引页,数据页依赖操作系统缓存 |
| 崩溃恢复 | 通过Redo/Undo Log,具备自动恢复能力,数据安全可靠 | 无崩溃安全机制,异常宕机易损坏,需手动修复 |
COUNT(*)性能 | 需扫描索引才能返回全表行数,数据量大时较慢 | 内部维护行计数器,直接返回,速度极快 |
事务的AIDC
| CID 特性 | 单机 MySQL 实现 |
|---|---|
| 原子性(A) | Undo Log |
| 一致性(C) | 约束 + 业务逻辑 |
| 隔离性(I) | 锁 + MVCC |
| 持久性(D) | Redo Log + Binlog |
| 隔离级别 | 读操作(SELECT)底层实现 | 写操作(UPDATE/DELETE/FOR UPDATE)底层实现 |
|---|---|---|
| 读未提交(RU) | 直接读数据页最新物理值 不生成ReadView 不走MVCC版本链 | 加行锁(X锁),语句结束立即释放 不加间隙锁 |
| 读已提交(RC) | MVCC + 每次查询生成新ReadView 通过Undo Log回溯可见版本 不加锁 | 只加行锁(Record Lock) 不加间隙锁 语句结束立即释放 |
| 可重复读(RR) | MVCC + 事务内复用首次生成的ReadView 通过Undo Log回溯可见版本 不加锁 | 加 Next-Key Lock(行锁+间隙锁) 事务提交才释放 |
| 串行化(SERIALIZABLE) | 自动加共享锁(S-Lock) 不走MVCC(除非显式指定) 事务提交才释放 | 加 Next-Key Lock(X锁) 事务提交才释放 |
undolog、redolog、binlog
1,一条更新语句MySQL 可以分为 Server 层和存储引擎层两部分。
2, Server 层的执行引擎调用引擎层 innoDB 将数据加载到buffer pool中 修改buffer pool中的数据,产生undo log
3, 这个时候是脏数据,因为改了buffer pool中的数没有改磁盘中的数据,
提交事务:
- 先写redolog buffer pool中的脏数据同步到Redo buffer,Redo buffer根据
innodb_flush_log_at_trx_commit判断刷盘策略 - 写完redolog再写binlog
- binlog 写完在将redolog的prepare标记改为Commit 标记
| 日志类型 | 所处层级 | 核心目的 | 写入内存时机 | 写入/刷盘时机(关键参数) | 空间管理方式 |
|---|---|---|---|---|---|
| Undo Log | InnoDB引擎 | 事务回滚、MVCC | 修改数据之前,实时写入 | 随数据修改写入Undo表空间,事务提交后由Purge线程清理 | 由Purge线程回收 |
| Redo Log | InnoDB引擎 | 持久性、崩溃恢复 | 修改数据之后,实时写入redo log buffer | 事务提交时,由innodb_flush_log_at_trx_commit控制刷盘 | 循环写入,固定大小,空间满时触发Checkpoint并覆盖 |
| Binlog | MySQL Server | 主从复制、数据恢 | 事务提交时写入binlog cache | 事务提交时,由sync_binlog控制刷盘 | 追加写入,可轮转切换,空间可不断增长 |
事务提交时的“两阶段提交”
事务提交时的“两阶段提交”(保证一致的核心)
在事务提交时,Redo Log 和 Binlog 的写入顺序被严格设计为一个“两阶段提交”过程,你可以把它理解为一次“投票”和“归档”:
Prepare 阶段(准备/投票):事务的 Redo Log 被写入磁盘(状态为
prepare)。此时,数据改动已经在内存中完成,但事务还未正式提交。Commit 阶段(提交/归档):
- 写 Binlog:将事务的 Binlog 写入磁盘。
- 写 Redo Log(Commit):将刚才那条 Redo Log 的状态从
prepare改为commit,标记事务正式提交。
这个顺序保证了,一个完整事务的 Binlog 和 Redo Log 总是一起出现。如果在第1步和第2步之间发生崩溃,就会产生不一致,这正是“崩溃恢复”需要处理的核心场景。
| 场景 | Redo Log 状态 | Binlog 状态 | 崩溃恢复的处理动作 | 最终一致性结果 |
|---|---|---|---|---|
| 场景一:正常情况 | commit | 已完整写入 | 事务被视为已提交,直接应用 Redo Log 的重做数据。 | 一致 |
| 场景二:崩溃在写 Binlog 之前 | prepare | 不存在或不完整 | 事务被视为未提交,回滚该事务在 Redo Log 中的修改。 | 一致(Binlog 和 Redo Log 都没有这个事务) |
| 场景三:崩溃在写 Binlog 之后 | prepare | 完整写入(XID 匹配) | 事务被视为已提交,提交该事务(补上 Commit 标记),并应用 Redo Log。 | 一致(Binlog 已完整,Redo Log 补上 Commit) |
XID(事务ID)的作用:每条 Binlog 和 Redo Log 都会记录同一个事务的 XID。在恢复时,MySQL 就通过比对这个 XID,来判断 Binlog 里是否完整记录了该事务。XID(事务ID)的作用:每条 Binlog 和 Redo Log 都会记录同一个事务的 XID。在恢复时,MySQL 就通过比对这个 XID,来判断 Binlog 里是否完整记录了该事务。