11/17/2025

MySQL存储与事务机制完全指南

## 目录 1. [MySQL索引与数据的存储位置](#1-mysql索引与数据的存储位置) 2. [Buffer Pool:MySQL的内存管理核心](#2-buffer-pool-mysql的内存管理核心) 3. [热点数据识别:改进的LRU算法](#3-热点数据识别改进的lru算法) 4. [全表扫描的工作机制](#4-全表扫描的工作机制) 5. [数据传输:从磁盘到客户端](#5-数据传输从磁盘到客户端) 6. [写操作与Buffer Pool](#6-写操作与buffer-pool) 7. [脏页管理与刷盘机制](#7-脏页管理与刷盘机制) 8. [大批量更新的挑战与解决方案](#8-大批量更新的挑战与解决方案) 9. [Redo Log:崩溃恢复的保障](#9-redo-log崩溃恢复的保障) 10. [Binlog:复制与恢复](#10-binlog复制与恢复) 11. [Undo Log与MVCC机制](#11-undo-log与mvcc机制) 12. [事务隔离级别详解](#12-事务隔离级别详解) --- ## 1. MySQL索引与数据的存储位置 ### 1.1 核心结论 **索引和数据最终存储在磁盘,但运行时的热点部分会被缓存在内存的Buffer Pool中。** ``` 永久存储:磁盘(.ibd文件) ├─ 数据页(16KB为单位) ├─ 索引页(B+树结构) └─ 容量大,持久化,但访问慢 运行时缓存:内存(Buffer Pool) ├─ 热点数据页 ├─ 热点索引页 └─ 容量小,易失,但访问快(比磁盘快1000-10000倍) ``` ### 1.2 工作原理 ```sql -- 执行查询时 SELECT * FROM users WHERE id = 10; -- 内部流程: 1. 检查Buffer Pool中是否有id=10所在的数据页 ├─ 命中 → 直接从内存读取(极快) └─ 未命中 → 从磁盘加载到Buffer Pool(较慢) 2. 返回数据给客户端 ``` ### 1.3 关键配置 ```sql -- 查看Buffer Pool大小 SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; -- 推荐设置:服务器物理内存的50-80% -- 例如:32GB内存的服务器 SET GLOBAL innodb_buffer_pool_size = 24G; ``` --- ## 2. Buffer Pool:MySQL的内存管理核心 ### 2.1 Buffer Pool的组成 ``` ┌──────────────────────────────────────────┐ │ InnoDB Buffer Pool │ ├──────────────────────────────────────────┤ │ │ │ 【数据页缓存】 │ │ - 表数据(16KB/页) │ │ │ │ 【索引页缓存】 │ │ - B+树的索引节点 │ │ │ │ 【Change Buffer】 │ │ - 非唯一二级索引的变更缓冲 │ │ │ │ 【自适应哈希索引】 │ │ - 热点数据的快速访问路径 │ │ │ └──────────────────────────────────────────┘ ``` ### 2.2 页面的状态 ``` Buffer Pool中的页面有三种状态: 1. Free Page(空闲页) - 未被使用的页面 - 可以直接分配 2. Clean Page(干净页) - 内存数据 = 磁盘数据 - 可以直接淘汰(丢弃) 3. Dirty Page(脏页)⭐ - 内存数据 ≠ 磁盘数据(已修改但未刷盘) - 淘汰前必须先刷盘 ``` ### 2.3 监控Buffer Pool ```sql -- 查看Buffer Pool状态 SHOW ENGINE INNODB STATUS\G -- 关键指标: Buffer pool size -- 总页数 Free buffers -- 空闲页数 Database pages -- 使用中的页数 Modified db pages -- 脏页数量 Buffer pool hit rate -- 命中率(应>99%) -- 查看命中率 SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'; 命中率 = (read_requests - reads) / read_requests -- 理想值:>99% -- <95%需要调优 ``` --- ## 3. 热点数据识别:改进的LRU算法 ### 3.1 为什么需要改进LRU **经典LRU的问题:** ``` 问题1:预读失效 - InnoDB会预读相邻的数据页 - 但这些预读页可能不会被真正使用 - 却占据了LRU链表头部,挤掉真正的热数据 问题2:Buffer Pool污染 - 全表扫描会读取大量一次性使用的数据 - 这些冷数据会把真正的热数据全部淘汰 ``` ### 3.2 InnoDB的分代LRU ``` ┌─────────────────────────────────────────────┐ │ InnoDB Buffer Pool LRU List │ ├─────────────────────────┬───────────────────┤ │ Young区域 (热数据) │ Old区域 (冷数据) │ │ 约占 5/8 (63%) │ 约占 3/8 (37%) │ │ │ │ │ 最近频繁访问的页 │ 新加载的页 & │ │ "真正的热数据" │ 很久没访问的页 │ └─────────────────────────┴───────────────────┘ ↑ ↑ 头部(Hot) Midpoint中点 尾部(Cold) ``` ### 3.3 核心规则 ``` 规则1:新页插入位置 - 新读取的页不放在链表头部 - 而是放在Old区的头部(midpoint位置) - 需要"考验期"才能晋升 规则2:晋升条件(Old区 → Young区) - 在Old区停留时间 > innodb_old_blocks_time(默认1秒) - 并且在此之后被再次访问 - 才能晋升到Young区 规则3:Young区内的移动 - 不是每次访问都移到头部(成本太高) - 只有在Young区后1/4部分时,才移到头部 - 前3/4部分的访问不移动位置 ``` ### 3.4 相关配置 ```sql -- Old区占比(默认37%) SHOW VARIABLES LIKE 'innodb_old_blocks_pct'; innodb_old_blocks_pct = 37 -- Old区停留时间(默认1000ms) SHOW VARIABLES LIKE 'innodb_old_blocks_time'; innodb_old_blocks_time = 1000 -- 调整示例(减少全表扫描影响) SET GLOBAL innodb_old_blocks_time = 5000; -- 延长到5秒 ``` ### 3.5 效果对比 ``` 场景:100GB大表全表扫描,Buffer Pool 8GB 【简单LRU】: 扫描页进入 → 头部 原有热数据 → 被挤到尾部 → 被淘汰 结果:Buffer Pool被完全污染 ✗ 【改进LRU】: 扫描页进入 → Old区头部 扫描页快速流转 → 在Old区就被淘汰 Young区热数据 → 大部分保留 结果:热数据得到一定保护 ✓ ``` --- ## 4. 全表扫描的工作机制 ### 4.1 表大于Buffer Pool的场景 ``` 场景设定: - 表大小:100GB(640万个16KB的页) - Buffer Pool:8GB(50万个页) - 执行:SELECT * FROM huge_table; ``` ### 4.2 滚动读取过程 ``` 第一批(0-8GB): 磁盘: [====读取中====][........未读........] 内存: [数据页1-50万] ← Buffer Pool填满 第二批(8-16GB): 磁盘: [已读][====读取中====][......未读......] 内存: [数据页50万-100万] └→ 页1-50万被逐渐淘汰(LRU尾部) 第三批(16-24GB): 磁盘: [已读][已读][====读取中====][...未读...] 内存: [数据页100万-150万] └→ 页50万-100万被淘汰 ... 以此类推,像传送带一样滚动 ... 最后一批(92-100GB): 磁盘: [=======全部读过=======][====最后一批====] 内存: [数据页590万-640万] └→ 前面的页早已被淘汰 ``` ### 4.3 性能影响 ``` 磁盘I/O计算: - 数据总量:100GB - 顺序读速度:200 MB/s(机械盘)或 500 MB/s(SSD) 耗时: - 机械盘:100GB ÷ 200MB/s ≈ 512秒 ≈ 8.5分钟 - SSD:100GB ÷ 500MB/s ≈ 205秒 ≈ 3.4分钟 影响: 1. 扫描期间:大量磁盘I/O,查询极慢 2. 扫描之后:热数据被淘汰,其他查询变慢 3. 资源竞争:占用磁盘带宽,影响其他业务 ``` ### 4.4 优化措施 ```sql -- ❌ 不推荐 SELECT * FROM huge_table; -- ✓ 推荐方案 -- 方案1:分批处理 SELECT * FROM huge_table WHERE id BETWEEN 1 AND 100000; -- 暂停,让系统恢复 SELECT * FROM huge_table WHERE id BETWEEN 100001 AND 200000; -- 方案2:添加索引,避免全表扫描 CREATE INDEX idx_column ON huge_table(column); SELECT * FROM huge_table WHERE column = 'value'; -- 方案3:只查询需要的列 SELECT id, name FROM huge_table WHERE condition; -- 方案4:使用从库 -- 在从库执行大查询,不影响主库 ``` --- ## 5. 数据传输:从磁盘到客户端 ### 5.1 完整的数据流 ``` ┌────────┐ ┌──────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ 磁盘 │→→→│ Buffer │→→→│ Server │→→→│ 网络层 │→→→│ 客户端 │ │ .ibd │ │ Pool │ │ 层 │ │ Socket │ │ 应用 │ └────────┘ └──────────┘ └────────┘ └────────┘ └────────┘ 100GB 8GB 临时处理 16KB ??? 持久化 滚动窗口 即时发送 流式传输 取决于模式 ``` ### 5.2 各层的角色 ``` 【InnoDB层 - Buffer Pool】 - 作用:数据的"源头"和"工作台" - 大小:GB级别(如8GB) - 行为:滚动加载数据页 - 不参与:数据打包和网络传输 【Server层】 - 作用:数据处理和格式化 - 流程: 1. 从Buffer Pool读取行数据 2. 应用WHERE条件 3. 投影需要的列 4. 格式化成MySQL协议包 5. 立即发送(不堆积) 【网络层 - net_buffer】 - 大小:16KB(net_buffer_length) - 作用:临时缓冲,积累一定数据后发送 - 行为:填满16KB → flush → 继续积累 【客户端】⚠️ 关键差异 - 模式1:缓冲模式(默认,危险) → 接收所有数据到内存 → 100GB数据会占满客户端内存! - 模式2:流式模式(推荐) → 边接收边处理 → 内存占用恒定(只保存当前处理的行) ``` ### 5.3 客户端的两种模式 #### 缓冲模式(Store Result) ```python # Python - 默认模式(危险) import pymysql cursor = conn.cursor() # 默认缓冲模式 cursor.execute("SELECT * FROM huge_table") rows = cursor.fetchall() # ⚠️ 全部加载到内存! # 如果是100GB数据,客户端内存爆炸! for row in rows: process(row) ``` #### 流式模式(Use Result / SSCursor) ```python # Python - 流式模式(推荐) import pymysql cursor = conn.cursor(pymysql.cursors.SSCursor) # ✓ 流式 cursor.execute("SELECT * FROM huge_table") for row in cursor: # 逐行读取 process(row) # 处理完这一行,内存被回收 # 然后获取下一行 # 内存占用:恒定,只有当前行的大小 ``` #### 不同语言的流式实现 ```java // Java - JDBC Statement stmt = conn.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY ); stmt.setFetchSize(Integer.MIN_VALUE); // MySQL特殊值 ResultSet rs = stmt.executeQuery("SELECT * FROM huge_table"); while (rs.next()) { process(rs); } ``` ```go // Go - database/sql(默认就是流式) rows, err := db.Query("SELECT * FROM huge_table") defer rows.Close() for rows.Next() { var data SomeStruct rows.Scan(&data) process(data) } ``` ### 5.4 重要总结 ``` Buffer Pool的角色澄清: ✓ 参与:数据读取和准备阶段 - 从磁盘加载数据页 - 提供数据给Server层处理 ✗ 不参与:数据传输阶段 - 不是网络传输的通道 - 不是客户端接收的缓冲区 数据流向: 磁盘 → Buffer Pool(暂存) → Server层(提取并处理行数据) → net_buffer(打包) → 网络(传输) → 客户端(接收) Buffer Pool是"工作台",不是"传送带" ``` --- ## 6. 写操作与Buffer Pool ### 6.1 UPDATE操作的完整流程 ```sql UPDATE users SET age = 31 WHERE id = 10; ``` **执行步骤:** ``` Step 1: 读取数据到Buffer Pool ├─ 查找id=10所在的数据页 ├─ 如果不在Buffer Pool → 从磁盘加载 └─ 可能还需加载相关索引页 Step 2: 在Buffer Pool中修改数据 ├─ 直接在内存中修改age字段 ├─ 此时内存数据 ≠ 磁盘数据 └─ 这个页变成"脏页"(Dirty Page) Step 3: 写入Undo Log ⭐ ├─ 记录旧值:age = 30 ├─ 用于事务回滚 └─ 用于MVCC(其他事务读取历史版本) Step 4: 写入Redo Log ⭐ ├─ 记录物理修改:"在Page X的偏移Y写入值Z" ├─ 先写入Redo Log Buffer(内存) ├─ 事务提交时刷到磁盘(ib_logfile) └─ 顺序I/O,速度快 Step 5: 事务提交 ├─ Redo Log持久化到磁盘 ├─ 向客户端返回"成功" └─ 此时.ibd数据文件可能还没更新! Step 6: 后台异步刷脏 ├─ Page Cleaner线程定期工作 ├─ 将脏页写入磁盘(.ibd文件) └─ 推进Checkpoint ``` ### 6.2 WAL机制(Write-Ahead Logging) ``` 核心思想:先写日志,再写数据 优势: ┌─────────────────────────────────────────┐ │ 1. 内存操作代替磁盘操作 │ │ - 修改在Buffer Pool中完成(快) │ │ │ │ 2. 顺序I/O代替随机I/O │ │ - 写Redo Log是顺序写(快) │ │ - 写数据文件是随机写(慢) │ │ │ │ 3. 批量写入提高效率 │ │ - 多个修改积累后一次性刷盘 │ │ - 减少I/O次数 │ │ │ │ 4. 快速响应客户端 │ │ - Redo Log落盘即可返回成功 │ │ - 用户感知延迟低 │ └─────────────────────────────────────────┘ ``` ### 6.3 Change Buffer优化 ``` 针对非唯一二级索引的特殊优化: 场景: UPDATE users SET city = 'Shanghai' WHERE id = 10; -- city字段上有非唯一索引 传统流程: 1. 更新数据页(id=10的行) 2. 更新二级索引页(city索引) └─ 需要加载索引页到Buffer Pool(可能触发磁盘I/O) Change Buffer优化: 1. 更新数据页 2. 不立即更新索引页 3. 将"索引需要修改"这个信息记录在Change Buffer中 4. 等将来索引页被其他查询加载时,再合并(Merge) 优势: - 避免了随机读索引页的磁盘I/O - 将多次索引修改合并成一次操作 - 适合写多读少的场景 ``` --- ## 7. 脏页管理与刷盘机制 ### 7.1 Buffer Pool的两个关键链表 ``` ┌─────────────────────────────────────────┐ │ InnoDB Buffer Pool │ ├─────────────────────────────────────────┤ │ │ │ LRU List(管理页面淘汰) │ │ ├─ Young区 │ │ │ ├─ 页A(干净) │ │ │ ├─ 页B(脏) ←┐ │ │ │ └─ 页C(脏) ←┼─┐ │ │ └─ Old区 │ │ │ │ ├─ 页D(干净) │ │ │ │ └─ 页E(脏) ←┼─┼─┐ │ │ │ │ │ │ │ Flush List(管理脏页刷盘) │ │ ├─ 页B (LSN=1000) ─┘ │ │ │ │ ├─ 页C (LSN=1050) ───┘ │ │ │ └─ 页E (LSN=1100) ─────┘ │ │ │ └─────────────────────────────────────────┘ ``` ### 7.2 刷脏的触发条件 ``` 条件1:脏页比例达到阈值 SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct%'; innodb_max_dirty_pages_pct = 75 -- 上限75% innodb_max_dirty_pages_pct_lwm = 10 -- 低水位10% 行为: 0% ────── 10% ────── 75% ────── 90% ────── 100% ↑ ↑ ↑ 开始温和 加速刷脏 疯狂刷脏 刷脏 可能阻塞 条件2:Redo Log空间不足 - Redo Log是循环使用的(如2GB) - 只有对应的脏页刷盘后,Redo Log空间才能重用 - 如果Redo Log快写满 → 必须刷脏推进Checkpoint 条件3:Buffer Pool空间不足 - 需要淘汰页面但都是脏页 - 必须同步刷脏(阻塞用户操作) 条件4:定期刷新 - 后台线程每秒检查一次 - 温和地刷一部分脏页 ``` ### 7.3 Page Cleaner线程 ```python # 伪代码:Page Cleaner的工作逻辑 while True: # 1. 检查脏页比例 dirty_ratio = get_dirty_pages_ratio() # 2. 检查Redo Log使用情况 redo_usage = get_redo_log_usage() # 3. 决定刷脏强度 if redo_usage > 90%: flush_mode = 'AGGRESSIVE' # 激进模式 pages_to_flush = 1000 elif dirty_ratio > 75%: flush_mode = 'ACTIVE' # 活跃模式 pages_to_flush = 500 elif dirty_ratio > 10%: flush_mode = 'GENTLE' # 温和模式 pages_to_flush = 100 else: sleep(1) continue # 4. 从Flush List获取最老的脏页 pages = get_oldest_dirty_pages(pages_to_flush) # 5. 刷盘 for page in pages: flush_page_to_disk(page) mark_as_clean(page) advance_checkpoint() sleep(interval) ``` ### 7.4 脏页与全表扫描 ``` 问题:全表扫描会淘汰脏页吗? 保护机制: 1. Redo Log是根本保证 - 脏页即使被淘汰,数据也不会丢 - 崩溃后通过Redo Log可以恢复 2. 优先淘汰干净页 - LRU淘汰时,优先选择干净页 - 脏页需要先刷盘才能淘汰 3. 控制脏页比例 - 脏页比例超过阈值 → 加速刷脏 - 确保始终有足够的干净页可淘汰 4. 同步刷脏(最后手段) - 如果实在没有干净页可淘汰 - 同步刷脏(阻塞查询) - 性能下降,但数据安全 结论:性能可以牺牲,数据绝不能丢! ``` ### 7.5 监控脏页 ```sql -- 查看脏页情况 SHOW ENGINE INNODB STATUS\G -- 关键指标: Modified db pages: 45678 -- 当前脏页数 Buffer pool size: 524288 -- 总页数 脏页比例 = 45678 / 524288 ≈ 8.7% Pages flushed up to: 1234567890 -- Checkpoint LSN Log sequence number: 1234600000 -- 当前LSN 未刷脏的Redo Log = 1234600000 - 1234567890 = 32110 Pending writes: LRU: 5 -- LRU淘汰等待刷脏的页 flush list: 10 -- Flush List等待刷脏的页 ``` --- ## 8. 大批量更新的挑战与解决方案 ### 8.1 全表更新的问题 ```sql -- 危险操作 UPDATE huge_table SET status = 1 WHERE category = 'old'; -- 假设匹配80GB数据,Buffer Pool只有8GB ``` **面临的挑战:** ``` 挑战1:脏页爆炸 - 修改产生大量脏页 - 脏页比例快速上升 → 触发疯狂刷脏 - 可能导致周期性卡顿 挑战2:Redo Log空间限制 ⭐ 最严重 - Redo Log总大小有限(如2GB) - 80GB的修改会产生大量Redo Log - Redo Log空间用完 → 必须等待刷脏 → 阻塞 - 性能严重下降 挑战3:Undo Log膨胀 - 需要保存所有旧值以支持回滚 - 80GB修改可能产生80GB Undo Log - 占用磁盘空间,影响查询性能 挑战4:锁等待 - 如果是一个大事务,所有修改的行都被锁定 - 其他查询可能被阻塞 - 超时或死锁风险 挑战5:回滚噩梦 - 如果UPDATE执行到一半失败 - 回滚可能比更新本身还慢 - 可能需要几小时 ``` ### 8.2 分批更新方案 #### 方案1:按主键分批 ```sql -- ✓ 推荐 -- 第一批 UPDATE huge_table SET status = 1 WHERE category = 'old' AND id BETWEEN 1 AND 100000; COMMIT; -- 暂停1秒 -- 让系统刷脏、清理Redo Log -- 第二批 UPDATE huge_table SET status = 1 WHERE category = 'old' AND id BETWEEN 100001 AND 200000; COMMIT; -- 继续... ``` #### 方案2:使用LIMIT ```sql -- 循环执行,直到affected_rows = 0 UPDATE huge_table SET status = 1 WHERE category = 'old' LIMIT 10000; -- 每次只更新1万行 ``` #### 方案3:应用层控制 ```python import pymysql import time conn = pymysql.connect(host='localhost', user='root', db='test') cursor = conn.cursor() batch_size = 100000 start_id = 0 while True: sql = """ UPDATE huge_table SET status = 1 WHERE category = 'old' AND id BETWEEN %s AND %s """ cursor.execute(sql, (start_id, start_id + batch_size)) affected = cursor.rowcount conn.commit() # 每批都提交,变成小事务 print(f"Updated {affected} rows") if affected == 0: break start_id += batch_size + 1 time.sleep(0.5) # 暂停500ms,让系统喘口气 ``` ### 8.3 优化配置(大批量更新场景) ```sql -- 增大Redo Log(MySQL 8.0.30+) SET GLOBAL innodb_redo_log_capacity = 8G; -- 增大I/O容量(告诉InnoDB磁盘的IOPS能力) SET GLOBAL innodb_io_capacity = 2000; -- 正常 SET GLOBAL innodb_io_capacity_max = 4000; -- 峰值 -- 增加Page Cleaner线程数 SET GLOBAL innodb_page_cleaners = 8; -- 降低脏页上限(更积极刷脏) SET GLOBAL innodb_max_dirty_pages_pct = 50; ``` ### 8.4 监控长事务 ```sql -- 查找运行时间过长的事务 SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec, trx_rows_modified, trx_isolation_level, trx_state FROM information_schema.INNODB_TRX WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60 ORDER BY trx_started; -- 如果发现长事务,考虑kill掉 KILL <trx_mysql_thread_id>; ``` --- ## 9. Redo Log:崩溃恢复的保障 ### 9.1 Redo Log的本质 **Redo Log是物理日志,记录数据页的字节级修改。** ``` Redo Log不记录: ❌ SQL语句:"UPDATE users SET age=31 WHERE id=10" ❌ 逻辑修改:"第10行的age字段从30改成31" Redo Log记录: ✓ 物理修改:"在表空间5的第1234号数据页的偏移量567处, 写入2字节数据0x001F(十进制31)" ``` ### 9.2 一条UPDATE生成多少Redo Log? ```sql UPDATE users SET age = age + 1 WHERE city = 'Beijing'; -- 假设匹配10万行 ``` **生成过程:** ``` 对每一行的修改: ┌────────────────────────────────────┐ │ Mini-Transaction (MTR) #1 │ ├────────────────────────────────────┤ │ - Redo: 修改Page 100, offset 200 │ │ - Redo: 更新Page 100的页头 │ │ - Redo: 写入Undo Log(也产生Redo)│ └────────────────────────────────────┘ ┌────────────────────────────────────┐ │ Mini-Transaction (MTR) #2 │ ├────────────────────────────────────┤ │ - Redo: 修改Page 100, offset 300 │ │ - Redo: 更新Page 100的页头 │ │ - Redo: 写入Undo Log │ └────────────────────────────────────┘ ... 重复10万次 ... 总Redo Log量估算: - 10万行 × 30字节/行 ≈ 3MB - 加上索引、Undo等 ≈ 5-10MB ``` ### 9.3 Redo Log的结构 ``` ┌────────────────────────────────────┐ │ Redo Log Record │ ├────────────────────────────────────┤ │ Type: MLOG_WRITE_STRING │ │ Space ID: 5 │ │ Page Number: 1234 │ │ Offset: 567 │ │ Length: 2 │ │ Data: 0x001F │ └────────────────────────────────────┘ ``` **常见类型:** ```c MLOG_1BYTE // 修改1字节 MLOG_2BYTES // 修改2字节 MLOG_4BYTES // 修改4字节 MLOG_WRITE_STRING // 写入字节串 MLOG_REC_INSERT // 插入记录 MLOG_REC_UPDATE_IN_PLACE // 原地更新 MLOG_REC_DELETE // 删除记录 ``` ### 9.4 Redo Log的循环使用 ``` ┌──────────────────────────────────────┐ │ Redo Log Files (如2GB) │ ├──────────────────────────────────────┤ │ │ │ ib_logfile0 (1GB) │ │ ib_logfile1 (1GB) │ │ │ │ 循环写入: │ │ ┌─────────────────────────────────┐│ │ │[已刷脏可覆盖][新写入][待刷脏] ││ │ │ ↑ ││ │ │ Write Pos ││ │ │ ↑ ││ │ │ Checkpoint(已刷脏的位置) ││ │ └─────────────────────────────────┘│ │ │ │ 关键:只有脏页刷盘后, │ │ Checkpoint才能推进, │ │ Redo Log空间才能重用 │ └──────────────────────────────────────┘ ``` ### 9.5 Redo Log与崩溃恢复 ``` 崩溃恢复流程: 1. MySQL启动,检测到上次未正常关闭 2. 读取Redo Log,找到Checkpoint位置 3. 从Checkpoint开始重放(Replay)Redo Log ├─ 读取每条Redo Log记录 ├─ 根据Space ID找到表空间文件 ├─ 根据Page Number定位数据页 ├─ 根据Offset和Data重写数据 └─ 恢复所有已提交事务的修改 4. 通过Undo Log回滚未提交的事务 5. 恢复完成,数据库可用 优势: - Redo Log是顺序写,重放也很快 - 物理日志,不依赖SQL执行环境 - 保证已提交事务的持久性(D) ``` ### 9.6 配置参数 ```sql -- 查看Redo Log配置 SHOW VARIABLES LIKE 'innodb_log%'; -- 关键参数: innodb_log_file_size = 1G -- 单个文件大小 innodb_log_files_in_group = 2 -- 文件数量 -- 总大小 = 1G × 2 = 2GB -- MySQL 8.0.30+可动态调整 SET GLOBAL innodb_redo_log_capacity = 8G; -- 刷盘策略(重要!) innodb_flush_log_at_trx_commit = 1 -- 0: 每秒刷一次(快,但可能丢失1秒数据) -- 1: 每次提交都刷(最安全,默认)⭐ -- 2: 每次提交写OS缓存,每秒fsync(折中) ``` --- ## 10. Binlog:复制与恢复 ### 10.1 Redo Log vs Binlog | 对比项 | Redo Log | Binlog | |--------|----------|--------| | **层面** | InnoDB引擎层 | MySQL Server层 | | **格式** | 物理日志 | 逻辑日志(可配置) | | **内容** | 页的物理修改 | SQL语句或行变更 | | **用途** | 崩溃恢复 | 主从复制、备份恢复 | | **大小** | 固定,循环使用 | 不断增长,可归档 | | **刷盘** | 事务提交时 | 事务提交时(可配置) | ### 10.2 Binlog的三种格式 #### STATEMENT格式 ```sql -- Binlog记录SQL语句本身 BEGIN; UPDATE users SET age = age + 1 WHERE city = 'Beijing'; COMMIT; 优点: ✓ Binlog文件小 ✓ 可读性好 缺点: ✗ 非确定性函数(NOW(), RAND(), UUID())可能导致主从不一致 ✗ LIMIT无ORDER BY可能导致不一致 ✗ 已不推荐使用 ``` #### ROW格式(推荐)⭐ ```sql -- Binlog记录每一行的实际变更 BEGIN; -- 记录的是变更前后的镜像 Table_map: users Update_rows: Before: {id:1, name:'Tom', age:30} After: {id:1, name:'Tom', age:31} Update_rows: Before: {id:2, name:'Jerry', age:25} After: {id:2, name:'Jerry', age:26} ... COMMIT; 优点: ✓ 最安全,绝对保证主从一致 ✓ 不受SQL执行环境影响 ✓ MySQL 5.7.7+默认格式 缺点: ✗ Binlog文件较大(尤其大批量更新) ✗ 可读性差 ``` #### MIXED格式 ```sql -- 自动选择STATEMENT或ROW 安全的SQL → STATEMENT格式(文件小) 不安全的SQL → ROW格式(数据一致) 例如: UPDATE users SET name = 'Alice' WHERE id = 1; → STATEMENT格式 UPDATE users SET update_time = NOW() WHERE id = 1; → ROW格式(NOW()不确定) ``` ### 10.3 非幂等操作的处理 ```sql -- 问题:NOW()等函数在不同时间执行结果不同 UPDATE users SET last_update = NOW() WHERE id = 1; ``` **STATEMENT格式的处理:** ```sql -- Binlog实际记录: SET TIMESTAMP = 1716343200; -- ⭐ 记录主库的时间戳 UPDATE users SET last_update = NOW() WHERE id = 1; -- 从库应用时: -- 1. 先执行SET TIMESTAMP -- 2. 再执行UPDATE,NOW()使用设定的时间戳 -- 结果:主从一致 ``` **ROW格式的处理(更彻底):** ```sql -- Binlog记录: Table_map: users Update_rows: Before: {id:1, last_update:'2024-05-20 10:00:00'} After: {id:1, last_update:'2024-05-22 15:30:45'} -- 已计算好的值 -- 从库直接应用After值,无需重新计算NOW() -- 天然保证一致性 ``` ### 10.4 配置Binlog ```sql -- 查看Binlog配置 SHOW VARIABLES LIKE 'log_bin%'; SHOW VARIABLES LIKE 'binlog%'; -- 关键参数: log_bin = ON -- 启用Binlog binlog_format = ROW -- 格式(推荐ROW)⭐ sync_binlog = 1 -- 每次提交都刷盘(最安全) binlog_cache_size = 32K -- 事务Binlog缓存 max_binlog_size = 1G -- 单个Binlog文件最大1GB -- 查看Binlog文件 SHOW BINARY LOGS; SHOW MASTER STATUS; ``` ### 10.5 主从复制中的应用 ``` 主库(Master): 1. 执行UPDATE 2. 写入Redo Log(InnoDB) 3. 写入Binlog(Server层) 4. 两阶段提交(2PC)保证一致性 5. 返回客户端成功 从库(Slave): 1. I/O线程从主库拉取Binlog 2. 写入本地Relay Log 3. SQL线程读取Relay Log 4. 重放(Replay)操作 ├─ ROW格式:直接应用行变更 └─ STATEMENT格式:重新执行SQL 5. 数据同步完成 ``` --- ## 11. Undo Log与MVCC机制 ### 11.1 为什么没开事务也有Undo Log? **两大用途:** ``` 1. 事务回滚(Transaction Rollback) - 记录修改前的旧值 - ROLLBACK时恢复数据 2. MVCC(Multi-Version Concurrency Control)⭐ 更重要 - 为其他事务提供数据的历史版本 - 实现一致性读(快照读) - 支持不同隔离级别 ``` ### 11.2 隐式事务 ```sql -- 即使没有显式BEGIN,MySQL也会创建事务 -- 查看自动提交 SHOW VARIABLES LIKE 'autocommit'; -- autocommit = ON(默认) -- 执行UPDATE UPDATE users SET age = 30 WHERE id = 1; -- MySQL内部实际执行: BEGIN; -- 隐式开启 UPDATE users SET age = 30 WHERE id = 1; ├─ 生成Undo Log(旧值age=29) └─ 生成Redo Log(新值age=30) COMMIT; -- 隐式提交 -- 所以:"没有开启事务"这个说法不存在 ``` ### 11.3 MVCC的核心机制 #### 隐藏字段 ``` 用户看到的数据: ┌────┬──────┬─────┐ │ id │ name │ age │ ├────┼──────┼─────┤ │ 1 │ Tom │ 30 │ └────┴──────┴─────┘ 实际存储(含隐藏字段): ┌────┬──────┬─────┬───────────┬────────────┬───────────┐ │ id │ name │ age │ DB_TRX_ID │ DB_ROLL_PTR│ DB_ROW_ID │ ├────┼──────┼─────┼───────────┼────────────┼───────────┤ │ 1 │ Tom │ 30 │ 100 │ 0x12AB... │ (自增) │ └────┴──────┴─────┴───────────┴────────────┴───────────┘ ↑ ↑ 最后修改的事务ID 指向Undo Log的指针 ``` #### 版本链 ``` 当前数据(最新版本): ┌────────────────────────────────┐ │ age=30, TRX_ID=100 │ │ ROLL_PTR ─────┐ │ └───────────────┼───────────────┘ ↓ Undo Log (版本2) ┌───────────────────────┐ │ age=29, TRX_ID=98 │ │ ROLL_PTR ─────┐ │ └───────────────┼───────┘ ↓ Undo Log (版本1) ┌──────────────────┐ │ age=25, TRX_ID=95│ │ ROLL_PTR = NULL │ └──────────────────┘ 通过ROLL_PTR形成版本链 ``` #### Read View ``` 事务开始时(或第一次SELECT),创建Read View: ┌───────────────────────────────────┐ │ Read View │ ├───────────────────────────────────┤ │ m_ids: [97, 99, 101] │ 当前活跃事务列表 │ min_trx_id: 97 │ 最小活跃事务ID │ max_trx_id: 102 │ 下一个要分配的事务ID │ creator_trx_id: 99 │ 创建此视图的事务ID └───────────────────────────────────┘ ``` #### 可见性判断 ```python def is_visible(data_trx_id, read_view): # 1. 很早之前提交的 if data_trx_id < read_view.min_trx_id: return True # 可见 # 2. 未来的事务(在Read View创建之后) if data_trx_id >= read_view.max_trx_id: return False # 不可见 # 3. 在活跃事务列表中 if data_trx_id in read_view.m_ids: if data_trx_id == read_view.creator_trx_id: return True # 自己的修改可见 else: return False # 其他活跃事务的修改不可见 # 4. 不在活跃列表,说明已提交 return True ``` #### 读取流程 ``` 会话B要读取id=1的数据: 1. 获取当前行的最新版本 age=30, TRX_ID=100 2. 用Read View判断可见性 is_visible(100, read_view) → False 3. 顺着ROLL_PTR找Undo Log中的上一版本 age=29, TRX_ID=98 4. 再次判断 is_visible(98, read_view) → True 5. 返回 age=29 ``` ### 11.4 MVCC实战 ```sql -- 准备数据 CREATE TABLE test (id INT PRIMARY KEY, value INT); INSERT INTO test VALUES (1, 100); -- 会话A SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SELECT value FROM test WHERE id = 1; -- 结果:100 -- 此时创建Read View -- 会话B UPDATE test SET value = 200 WHERE id = 1; -- 生成Undo Log:{id:1, value:100} -- 立即提交(autocommit=1) -- 回到会话A SELECT value FROM test WHERE id = 1; -- 结果:仍然是100! -- 因为会话B的事务ID对会话A的Read View不可见 -- 从Undo Log读取了旧版本 COMMIT; -- 新事务 BEGIN; SELECT value FROM test WHERE id = 1; -- 结果:200(新Read View能看到已提交的修改) ``` ### 11.5 Undo Log的清理(Purge) ``` 生命周期: ┌─────────────────────────────────────┐ │ 1. 事务修改数据 → 生成Undo Log │ │ 2. 事务提交 → 标记为"可清理" │ │ 3. Purge线程检查: │ │ - 是否有活跃Read View需要它? │ │ - 检查所有活跃事务的Read View │ │ 4. 所有Read View都不需要 │ │ → 物理删除Undo Log │ └─────────────────────────────────────┘ 长事务的危害: - 占着Read View不释放 - 导致大量Undo Log无法清理 - History List Length不断增长 - 查询需要遍历很长的版本链 - 性能下降 ``` **监控Undo:** ```sql SHOW ENGINE INNODB STATUS\G -- 关键指标: History list length: 1234 -- 值很大说明有大量未清理的Undo Log -- 查找长事务 SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration, trx_rows_modified FROM information_schema.INNODB_TRX WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60; ``` --- ## 12. 事务隔离级别详解 ### 12.1 MySQL支持的四个隔离级别 ```sql -- 查看当前隔离级别 SELECT @@transaction_isolation; -- MySQL默认:REPEATABLE-READ ``` | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | |---------|------|-----------|------|------| | READ UNCOMMITTED | ✗ | ✗ | ✗ | ⭐⭐⭐⭐⭐ | | READ COMMITTED | ✓ | ✗ | ✗ | ⭐⭐⭐⭐ | | REPEATABLE READ | ✓ | ✓ | ✓* | ⭐⭐⭐ | | SERIALIZABLE | ✓ | ✓ | ✓ | ⭐ | *注:MySQL的REPEATABLE READ通过MVCC和Next-Key Lock基本解决了幻读。 ### 12.2 READ UNCOMMITTED(读未提交) **问题:脏读** ```sql -- 会话A BEGIN; UPDATE accounts SET balance = 900 WHERE id = 1; -- 未提交 -- 会话B SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN; SELECT balance FROM accounts WHERE id = 1; -- 读到900(脏读!) -- 会话A回滚 ROLLBACK; -- 会话B读到的数据是错误的 ``` **使用场景:几乎不用** ### 12.3 READ COMMITTED(读已提交) **特点:** - 只能读到已提交的数据(解决脏读) - 每次SELECT创建新的Read View **问题:不可重复读** ```sql -- 会话A SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; SELECT balance FROM accounts WHERE id = 1; -- 第一次:1000 -- 会话B UPDATE accounts SET balance = 900 WHERE id = 1; COMMIT; -- 会话A(同一事务内) SELECT balance FROM accounts WHERE id = 1; -- 第二次:900(不可重复读) ``` **使用场景:** - 高并发互联网应用 - 阿里巴巴部分业务 - 需要读最新数据的场景 ### 12.4 REPEATABLE READ(可重复读)⭐ **MySQL默认隔离级别** **特点:** - 保证同一事务内多次读取结果一致 - 事务开始时创建Read View(或首次SELECT) - 通过MVCC实现 **基本解决幻读:** ```sql -- 快照读(无幻读) BEGIN; SELECT * FROM test; -- 3条记录 -- 另一会话插入数据并提交 -- INSERT INTO test VALUES (4, 40); SELECT * FROM test; -- 仍然3条记录(通过MVCC,读取快照) -- 当前读(加锁防幻读) SELECT * FROM test WHERE id > 0 FOR UPDATE; -- 使用Next-Key Lock锁定范围 -- 其他会话无法在范围内插入 ``` **Next-Key Lock机制:** ``` Next-Key Lock = Record Lock + Gap Lock 例如索引值:1, 5, 10 SELECT * FROM test WHERE id > 5 FOR UPDATE; 锁定范围: - (5, 10] - Next-Key Lock - (10, +∞) - Gap Lock 效果:其他事务无法在(5, +∞)范围内插入 ``` **使用场景:** - 大多数OLTP业务(默认) - 转账、订单处理等 ### 12.5 SERIALIZABLE(串行化) **特点:** - 最高隔离级别 - 所有SELECT自动变成SELECT ... LOCK IN SHARE MODE - 完全串行化 ```sql -- 会话A SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN; SELECT * FROM accounts WHERE id = 1; -- 自动加共享锁(S锁) -- 会话B UPDATE accounts SET balance = 900 WHERE id = 1; -- 被阻塞(等待会话A释放S锁) ``` **锁互斥关系:** ``` S锁 X锁 S锁 ✓ ✗ X锁 ✗ ✗ 导致大量阻塞,性能极差 ``` **使用场景:** - 极少使用 - 金融核心账务 - 关键报表生成 ### 12.6 隔离级别的设置 ```sql -- 全局设置(新连接生效) SET GLOBAL transaction_isolation = 'READ-COMMITTED'; -- 会话级设置 SET SESSION transaction_isolation = 'READ-COMMITTED'; -- 单个事务设置 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; -- 只影响这个事务 COMMIT; -- 配置文件(永久) [mysqld] transaction-isolation = READ-COMMITTED ``` ### 12.7 不同数据库的默认值 ``` MySQL/InnoDB: REPEATABLE READ Oracle: READ COMMITTED PostgreSQL: READ COMMITTED SQL Server: READ COMMITTED SQLite: SERIALIZABLE ``` **MySQL选择REPEATABLE READ的历史原因:** ``` 早期MySQL的Binlog只支持STATEMENT格式 - STATEMENT在READ COMMITTED下可能导致主从不一致 - 例如:UPDATE ... LIMIT无ORDER BY 主库和从库可能选中不同的行 REPEATABLE READ + 间隙锁可避免这个问题 现代MySQL(5.1+): - Binlog支持ROW格式(推荐) - ROW格式不受隔离级别影响 - 理论上可改用READ COMMITTED - 但为兼容性,仍保持REPEATABLE READ为默认 ``` --- ## 总结:知识体系图 ``` MySQL存储与事务机制 │ ├─ 存储层 │ ├─ 磁盘存储(.ibd文件) │ │ └─ 数据页和索引页的永久存储 │ │ │ └─ 内存缓存(Buffer Pool)⭐ │ ├─ 热点数据页缓存 │ ├─ 改进的LRU算法(Young/Old区) │ ├─ 脏页管理(Flush List) │ └─ Change Buffer优化 │ ├─ 读操作 │ ├─ 全表扫描的滚动读取 │ ├─ Buffer Pool的工作台角色 │ ├─ 数据传输(磁盘→Buffer Pool→Server→网络→客户端) │ └─ 客户端流式读取的重要性 │ ├─ 写操作 │ ├─ WAL机制(Write-Ahead Logging) │ ├─ Buffer Pool中修改(产生脏页) │ ├─ Undo Log生成(支持回滚和MVCC) │ ├─ Redo Log生成(崩溃恢复保障) │ ├─ Binlog生成(复制和备份) │ ├─ 后台异步刷脏 │ └─ 大批量更新的分批策略 │ ├─ 日志系统 │ ├─ Redo Log(物理日志,InnoDB层) │ │ ├─ 记录数据页的物理修改 │ │ ├─ 循环使用,固定大小 │ │ ├─ 支持崩溃恢复 │ │ └─ 顺序I/O,性能优秀 │ │ │ ├─ Binlog(逻辑日志,Server层) │ │ ├─ STATEMENT/ROW/MIXED格式 │ │ ├─ 支持主从复制 │ │ ├─ 支持备份恢复 │ │ └─ 处理非幂等操作 │ │ │ └─ Undo Log(逻辑日志,InnoDB层) │ ├─ 支持事务回滚 │ ├─ 支持MVCC(更重要) │ ├─ 形成版本链 │ └─ Purge线程清理 │ └─ 事务机制 ├─ MVCC(多版本并发控制) │ ├─ 隐藏字段(TRX_ID, ROLL_PTR) │ ├─ Read View │ ├─ 版本链 │ └─ 可见性判断 │ └─ 隔离级别 ├─ READ UNCOMMITTED(几乎不用) ├─ READ COMMITTED(高并发场景) ├─ REPEATABLE READ(MySQL默认)⭐ │ ├─ MVCC实现 │ ├─ Next-Key Lock │ └─ 基本解决幻读 └─ SERIALIZABLE(极少使用) ``` --- ## 快速复习检查清单 ### 核心概念 - [ ] Buffer Pool是什么?作用是什么? - [ ] 索引和数据最终存储在哪里? - [ ] 热点数据如何识别?(LRU算法) - [ ] 脏页是什么?如何管理? ### 读操作 - [ ] 全表扫描时Buffer Pool如何工作? - [ ] 数据如何从磁盘到达客户端? - [ ] Buffer Pool参与数据传输吗? - [ ] 客户端的两种读取模式是什么? ### 写操作 - [ ] UPDATE操作的完整流程? - [ ] WAL机制是什么?有什么优势? - [ ] Change Buffer的作用? - [ ] 大批量更新为什么要分批? ### 日志系统 - [ ] Redo Log记录什么?格式是什么? - [ ] Binlog的三种格式及其区别? - [ ] 如何处理NOW()等非幂等操作? - [ ] Undo Log的双重用途? - [ ] 三种日志的区别和联系? ### 事务机制 - [ ] MVCC的核心机制? - [ ] 四个隔离级别及其特点? - [ ] MySQL默认隔离级别是什么?为什么? - [ ] 如何解决幻读问题? - [ ] 长事务的危害? ### 性能调优 - [ ] 如何监控Buffer Pool命中率? - [ ] 如何监控脏页比例? - [ ] 如何发现长事务? - [ ] 全表扫描如何优化? - [ ] 大批量更新的最佳实践? --- ## 关键配置参数速查 ```sql -- Buffer Pool innodb_buffer_pool_size = 24G -- 核心参数,建议物理内存50-80% innodb_old_blocks_pct = 37 -- Old区占比 innodb_old_blocks_time = 1000 -- Old区停留时间(ms) -- 脏页管理 innodb_max_dirty_pages_pct = 75 -- 脏页上限 innodb_max_dirty_pages_pct_lwm = 10 -- 低水位 innodb_page_cleaners = 4 -- Page Cleaner线程数 innodb_io_capacity = 2000 -- I/O能力 innodb_io_capacity_max = 4000 -- 最大I/O能力 -- Redo Log innodb_redo_log_capacity = 8G -- MySQL 8.0.30+ innodb_log_file_size = 1G -- 旧版本 innodb_log_files_in_group = 2 -- 旧版本 innodb_flush_log_at_trx_commit = 1 -- 刷盘策略(1最安全) -- Binlog log_bin = ON binlog_format = ROW -- 推荐ROW格式 sync_binlog = 1 -- 每次提交刷盘 -- 事务 transaction_isolation = REPEATABLE-READ -- 默认隔离级别 autocommit = ON -- 自动提交 -- 网络 net_buffer_length = 16384 -- 16KB max_allowed_packet = 64M -- 最大包大小 ```

数据迁移一般是什么场景需求,有哪些难点

  数据迁移(Data Migration)是IT领域中一项高风险、高复杂度的核心工作。简单来说,就是将数据从一个存储系统转移到另一个存储系统。 以下详细解析数据迁移的**典型场景**以及面临的**核心难点**。 --- ### 一、 数据迁移的常见场景需求 数据迁移通常...