脏页刷盘后,内存就释放了吗?重新读懂InnoDB的三张管理链表

📅 发布时间:2026/10/20 5:08:13
脏页刷盘后,内存就释放了吗?重新读懂InnoDB的三张管理链表
磁盘里的某个页内容是10。把它加载到缓存改成20再把20写回数据文件。这时有两个不同的问题数据文件是否已经收到20缓存里是否还保留着这个页第一个可以是“是”第二个也可以是“是”。写回让数据页的修改得到同步不要求顺便把缓存副本丢掉。我原来的三链表笔记却把它们连成了一个动作“刷盘后释放内存”。如果这是一个接下来还会频繁读取的页写回一次就赶走下次又重新加载缓存的作用反而被破坏了。下面从这个页的变化开始再解释Free List、LRU List、Flush List各自管什么。概念按MySQL 8.4文档核对附录是Python顺序教学模型不是MySQL实例或磁盘I/O实测。1. 一个页的两条变化轴第一条轴是“这个页还在不在缓存里”第二条轴是“缓存里的修改是否还没写回数据文件”。把它们合成一个状态就会误以为变干净等于变空闲。动作缓存槽位内容状态简化解释加载页被占用干净现在有缓存副本修改页仍被占用脏缓存中的数据发生改变完成一次写回期间没有新修改仍被占用干净这份缓存仍可继续使用淘汰可安全移出的干净页可复用不再缓存该页解除该页与槽位的映射例如磁盘页内容是10加载后缓存也是10修改成20页变脏把20写回后缓存仍然是20。这个时候“最新内容已经写回”与“缓存还在”完全可以同时成立。先遮住“模型磁盘”那一列看从修改到写回缓存值一直是20再遮住缓存列看模型磁盘是写回时才从10变20。只有后面的淘汰才解除A与槽位的映射。图对应附录顺序模型不是InnoDB运行截图。这里淘汰后“空出来”的是缓冲池内部的可复用槽位不代表把这块内存立刻归还操作系统。Buffer Pool容量和某个页是否仍驻留也要分开看。官方将脏页定义为已修改但尚未写入磁盘数据文件的页并说明后台线程会执行写回。这里的“干净”是数据页写回状态不等于“事务已提交”也不等于“所有恢复工作都结束了”。Buffer Pool刷盘说明。2. 三张表分别问什么结构主要问题不要误解为Free List哪里还有可复用的缓存块业务表里的空记录链LRU List哪些缓存页是淘汰候选磁盘页的物理排列顺序Flush List哪些脏页需要安排写回提交事务名单同一缓存页既可能是LRU中的成员也可能是Flush List中的成员。既要考虑它还值得不值得留在缓存也要考虑它的修改是否需要写回。还拿A页看它驻留且变脏时既有缓存保留问题也有写回责任在没有新修改的这次写回完成后脏页责任可以结束但缓存保留问题仍在。直到后来决定淘汰、并满足安全移出的条件才会出现可复用槽位。因此这些链表不是三份页数据也不是必须依次经过的三个站点。它们从不同职责管理同一批缓存页。这也回答了我原来另一个问题“数据页里没有内存链表指针怎么把页串起来”持久化的页内容和管理这些内容的内存元数据不必是同一个结构。不能把页内记录的next_record当成Buffer Pool管理链的下一个节点。MySQL 8.4文档明确说每个Buffer Pool实例管理自己的free、flush、LRU等结构。具体控制块有哪些字段、如何分配内存需要对应版本源码这里不把旧图里三个框升级成完整C结构体。Buffer Pool实例参数说明。3. 那么脏页能不能直接淘汰在这个教学模型里我们不允许直接扔掉脏页否则还没写回的20会消失磁盘上只剩10。先完成写回才允许下一步淘汰。真实InnoDB还要考虑页正在被使用、I/O进行中、日志先行等约束。因此“干净”也不是立刻能够淘汰的充分条件这张表只是隔离几个概念不是完整的并发状态机。还要区分两种目的后台写回可以为了控制脏页和推进检查点缓存紧张时也可能为了腾出可复用块而安排相关写回。后者可能接着淘汰但不能反推“任何写回都必然淘汰”。4. LRU不是“只要读过就永远留下”InnoDB使用改进的LRU包含young和old部分新读入页采用中点插入。这样做与降低一次性扫描、预读对热点缓存的影响有关提升到young的行为还受策略参数影响不能只写“读一次就一定进新链表头”。Buffer Pool管理说明抗扫描策略。所以LRU回答的是保留价值与淘汰候选而Flush List回答的是写回责任。热点页也可以很脏冷页也可以是干净的。这两个维度没有必要捆绑。5. 用一个模型把两步真正拆开附录故意只放一个缓存槽位磁盘也只是Python字典没有文件I/O。这样输出不会被性能、后台线程和配置淹没。load A residentA value10 dirtyFalse disk10 modify A residentA value20 dirtyTrue disk10 reject evict residentA value20 dirtyTrue disk10 flush A residentA value20 dirtyFalse disk20 evict A residentNone valueNone dirtyFalse disk20 reload A residentA value20 dirtyFalse disk20 MODEL_CHECKS_PASSED重点看flush A驻留页仍是A值仍是20只有dirty变成False。再执行evict A槽位才空出来。模型还单独检查“写回后再修改”20写回后把缓存值改成30页再次变脏模型磁盘仍是20再次写回模型磁盘才变成30。这个过程没有淘汰A。所以干净是当前状态不是页从此不会再变脏。这只演示两次顺序修改不包含真实数据库在I/O期间再次修改页的并发情况。模型还检查重复写回干净页不会改变状态、拒绝淘汰脏页不丢修改、淘汰后重新加载能读到20。为了验证断言有效我们另跑一个错误变体让flush顺手清掉缓存槽位“flush必须保留驻留页”的断言应立即失败。另一个变体允许直接淘汰脏页会在“脏页淘汰必须失败”的检查处被拒绝。这类模型验证的是我们自己写下的概念与程序是否一致。要验证真实InnoDB还需要隔离实例、固定版本和可观察证据不能把这里的输出改个标题就叫“MySQL实测结果”。6. 真正值得记住的区别Clean不是从未修改过Free也不是内容一定被清零。写回解决数据页内容同步淘汰解决缓存槽位复用。真实页的状态比三种颜色丰富但先把这两件事分清就不会再沿着旧图推导出“刷盘必然释放缓存”了。附录一旧图的两个说法分别错在哪里旧笔记里有一张三链表图其中“Clean Page未被修改”和“刷盘后释放内存”两处文字容易误读。这次不再照搬先把它们改准确“Clean Page未被修改”应理解成当前没有尚未写回的数据页修改不是这个页从来没有被改过。“刷盘后释放内存”把写回和淘汰混在一起。写回完成的页可以继续留在缓存释放缓存槽位需要另一个淘汰步骤。旧图仍有值得保留的意思LRU里有干净页也有脏页Flush List则追踪需要写回的脏页。这些不是三份数据副本而是从不同职责管理缓存页的结构。附录二完整教学模型Python 3运行即可不连接数据库不改变任何配置。程序使用显式检查不受python -O关闭assert的影响。class OneFrame: def __init__(self): self.disk {A: 10} self.page None self.value None self.dirty False def load(self, page): if self.page is not None: raise RuntimeError(frame occupied) self.value self.disk[page] self.page page self.dirty False def modify(self, value): if self.page is None: raise RuntimeError(no resident page) self.value value self.dirty True def flush(self): if self.page is None: raise RuntimeError(no resident page) if self.dirty: self.disk[self.page] self.value self.dirty False def evict(self): if self.dirty: raise RuntimeError(dirty page needs writeback) self.page None self.value None def check(ok, message): if not ok: raise AssertionError(message) def show(label, frame): print(f{label:12} resident{frame.page} value{frame.value} fdirty{frame.dirty} disk{frame.disk[A]}) f OneFrame() f.load(A) show(load A, f) f.modify(20) show(modify A, f) try: f.evict() except RuntimeError as error: check(str(error) dirty page needs writeback, wrong rejection) else: raise AssertionError(dirty eviction must fail) check((f.page, f.value, f.dirty, f.disk[A]) (A, 20, True, 10), rejection must preserve pending changes) show(reject evict, f) f.flush() check((f.page, f.value, f.dirty, f.disk[A]) (A, 20, False, 20), flush must retain resident page) show(flush A, f) f.flush() check((f.page, f.value, f.dirty, f.disk[A]) (A, 20, False, 20), clean flush must preserve state) f.evict() check((f.page, f.value, f.dirty) (None, None, False), eviction releases frame) show(evict A, f) f.load(A) check(f.value 20 and not f.dirty, reload must retain written value) show(reload A, f) g OneFrame() g.load(A) g.modify(20) g.flush() g.modify(30) check((g.page, g.value, g.dirty, g.disk[A]) (A, 30, True, 20), modification after flush must make page dirty again) g.flush() check((g.page, g.value, g.dirty, g.disk[A]) (A, 30, False, 30), second flush must retain new resident value) print(MODEL_CHECKS_PASSED)