DS3.2 Buffer Manager 类设计
2025-4-6
| 2025-4-11
Words 3689Read Time 10 min
type
status
date
slug
summary
tags
category
icon
password

BufferManager(缓冲区管理器)

这段代码是一个功能强大且复杂的 BufferManager(缓冲区管理器) 实现,通常在数据库系统中用于管理内存中的页面缓存。它控制磁盘与内存之间的数据交互,并使用不同的页面置换策略(LRU、MRU、Clock 等)来高效利用有限内存。

核心目标:

  • 管理一定数量的 内存页帧(Frame),这些帧中缓存了磁盘页;
  • 支持页面的 读取、修改、脏页追踪、置换、刷回磁盘
  • 集成 恢复管理器(RecoveryManager)磁盘空间管理器(DiskSpaceManager)
  • 实现 线程安全 和可扩展的 页面置换策略(EvictionPolicy)

主要成员变量解释:

  • 缓冲池中的所有帧。
  • 页号到帧索引的映射,快速查找页面是否已在缓存中。
  • 指向当前第一个空闲帧的索引(通过类似链表的方式维护)。
  • 整体缓冲区管理器级别的线程锁。
  • 具体使用的页面置换策略(如 LRU、Clock)接口。

内部类:Frame

每个 Frame 表示一个缓存帧,封装了:
  • 页面数据(byte[] contents)
  • 页号(pageNum)
  • 锁(frameLock)
  • 脏页标志(dirty)
  • 是否为日志页(logPage)
  • 有效性检查、pin/unpin、flush 写回磁盘、读写操作

关键方法解析:

fetchPageFrame(long pageNum)

  • 如果页面在缓存中,直接 pin 并返回;
  • 否则,查找空帧或触发置换策略;
  • 从磁盘读取页面数据,装入新帧;
  • 更新元数据(pageToFrame 映射);
  • 设置 dirty = false,并返回新 Frame。

fetchPage() / fetchNewPage()

  • 封装对 Frame 的请求为 Page 对象;
  • 通过 LockContext 集成锁管理。

flush() & invalidate()

  • flush():如果是脏页,写回磁盘,并调用恢复系统挂钩;
  • invalidate():刷新 + 标记帧无效。

evict() & evictAll()

  • 单独或批量驱逐帧,如果未被 pin,先 flush;
  • 更新 free 链和 pageToFrame 映射。

freePage() / freePart()

  • 释放指定页面或整个分区,通常在事务结束或对象被销毁后调用。

pin / unpin 机制:

  • pin():加锁并标记此页面被某事务使用,禁止被置换
  • unpin():释放使用权,当 pinCount 为 0 时才允许置换;
  • isPinned() 在父类 BufferFrame 中实现。

注意点:

  1. 页面加载过程线程安全(managerLock 和 frameLock 双锁)
  1. 使用 Java 位运算(~)实现自由帧的链式管理
  1. 调用恢复管理器(RecoveryManager)记录 WAL 和 flush hook
  1. 通过 EvictionPolicy 接口实现可扩展替换策略
  1. 封装一致性好,兼容恢复、事务和锁机制

总结类的职责结构:

组件
作用
Frame
表示一个缓存帧,支持读写/flush/pin等操作
BufferManager
管理所有帧、置换策略、磁盘读写、事务接口
EvictionPolicy
策略接口,支持 Clock/LRU/MRU 等
DiskSpaceManager
提供页分配、释放、读写服务
RecoveryManager
日志记录,支持崩溃恢复

内部的 Frame

我们现在来详细剖析 BufferManager 内部的 Frame —— 它是缓冲池中每个**缓存帧(frame)**的核心实现,负责管理一个内存页的生命周期、并发控制、读写、脏页追踪、回写磁盘等一整套功能。

Frame 类的定位:

每个 Frame 表示缓冲池中的一个缓存帧,它可能装载着一个磁盘页,管理着该页的生命周期。

类结构简述:

继承自抽象类 BufferFrame,提供了以下主要职责:
作用
功能
页面管理
持有页数据(byte[] contents)、页号(pageNum
引用控制
pin/unpin 管理(阻止被驱逐)
并发控制
frameLock 保证读写互斥
脏页管理
dirty 表示是否被修改,需要写回
刷盘操作
flush() 写入磁盘、调用恢复模块
读写操作
readBytes()writeBytes() 实现内存访问
有效性管理
是否被 evict(isValidinvalidate)或 freed

字段解析:

并发控制与 Pin Count

  • frameLock.lock()/unlock():在多线程下保护对帧的读写和状态更新。
  • pin():表示当前帧正在被使用,不可被置换;
  • unpin():释放 pin;
  • isPinned():由父类 BufferFrame 记录 pinCount 实现;
  • 当 pinCount > 0 → 不能被驱逐。

有效性状态管理:

isValid()

  • 表示帧当前是否处于 激活状态(即还在缓冲池中);
  • 如果 index ≥ 0,表示有效。

invalidate()

  • 如果是有效帧,先 flush()
  • 然后标记为已驱逐(index = INVALID_INDEX),contents = null

setFree() / setUsed()

  • 用于维护空闲帧链表(用位运算 ~ 和 index 实现)。

写入磁盘 & 脏页管理

flush()

  • 如果帧是脏的(dirty = true),写回磁盘:
    • 非日志页时,调用 recoveryManager.pageFlushHook(getPageLSN())
    • 然后通过 diskSpaceManager.writePage() 写入;
    • 最后将 dirty = false

读写操作逻辑

readBytes(short position, short num, byte[] buf)

  • contents + offset 中读取数据到 buf;
  • 记录一次缓存命中:evictionPolicy.hit(this)
  • pin/unpin 包裹整个过程。

writeBytes(short position, short num, byte[] buf)

  • 类似于 read,先 pin()
  • 比较写入内容与原数据是否不一致:
    • 如果变更,生成 beforeafter 内容;
    • 通过 recoveryManager.logPageWrite() 生成 WAL 日志;
  • 修改 contents 数据,设为 dirty = true
  • 通知置换策略(命中);
  • 最后 unpin()

其他辅助方法:

requestValidFrame()

  • 返回一个可用帧对象:
    • 如果已被驱逐 → 重新加载页面;
    • 否则返回当前对象(并 pin);
    • 用于避免非法访问已驱逐页面。

getChangedBytes()

  • 用于日志记录:对比旧数据和写入数据,生成哪些区域发生变化的 (offset, length) 对。

生命周期状态图(概念)

总结:

功能类别
方法或字段
页面控制
contents, pageNum, index, isValid
引用控制
pin(), unpin(), isPinned()
并发安全
frameLock, 加锁保护
脏页处理
dirty, flush(), invalidate()
数据访问
readBytes(), writeBytes()
日志记录
getChangedBytes(), logPageWrite()
回收与释放
setFree(), setUsed()

空闲帧实现方式

如何用位运算 ~index 字段来实现空闲帧的链表管理,这其实是一个非常巧妙、简洁的实现方式

问题背景:如何表示“哪些帧是空闲的”?

传统做法可能是:
  • 维护一个 List<Integer>Stack<Integer> 来表示空闲帧索引;
  • 或者使用布尔数组 isUsed[]
但这个实现直接 复用了 Frame.index 字段 + 位运算 实现了一个 单链表结构

核心逻辑:用 ~index 表示“下一个空闲帧的索引”

状态
index 字段含义
使用中
index = 实际帧位置(例如 index = 2)
空闲状态
index = ~下一个空闲帧索引(例如 index = ~3)
驱逐状态
index = Integer.MIN_VALUE

为什么用 ~

~x 是按位取反(bitwise NOT):
因此可以:
  • 把一个正整数 i 转换成“标记为空闲”;
  • 又能从空闲的 index 中恢复出“下一个空闲帧的位置”。

空闲链表的管理

🟩 标记为 FreesetFree()

🟦 标记为 UsedsetUsed()

举个例子:

假设:
  • 当前空闲帧链:[2] -> [4] -> [6]
  • firstFreeIndex = 2
  • 每个空闲帧的 index 分别是:
    • frame[2].index = ~4 = -5
    • frame[4].index = ~6 = -7
    • frame[6].index = ~-1(假设末尾)

当我们调用 setUsed() 获取 frame[2]:

链变成:[4] -> [6]

总结:优点

优点
说明
🌱 节省空间
无需额外链表/列表,index 字段复用
🧠 结构简单
不引入额外数据结构
⚡️ 操作高效
所有操作都是常数时间(O(1))
🎯 可区分状态
index 的符号和大小区分帧状态(使用中、空闲、驱逐)

为什么在数据库系统中需要设计 Page 来包装 Frame

这个设计不只是“封装”,而是为了权限控制、抽象分层、锁机制、事务隔离、安全访问、可扩展性等多个系统级目标。

回顾类的关系

  • BufferManager.Frame:代表缓存池中真实的内存帧,拥有数据 + 管理状态(锁、脏位、刷盘等);
  • Page:代表“逻辑页面”句柄,暴露给上层模块使用(如表、索引、事务等);
  • Page 通过 frame 来访问实际数据,但对外隐藏了 Frame 的细节;
  • PageBuffer:包装类,实现对页面内容的 Buffer 风格访问接口。

设计 Page 的原因

1. 职责分离:Page 负责对外接口,Frame 负责缓存管理

类名
作用
Frame
是 BufferManager 内部用来管理缓存帧的底层结构,包括锁、脏页标志、刷盘等
Page
是提供给**数据库上层模块(如 B+ 树 / Table)**使用的页面接口,屏蔽 Frame 细节
Frame 是“底层”的,Page 是“用户可见”的。

1. 屏蔽 Frame 底层细节,控制使用权限

Frame 管理很多内部状态(如锁、pinCount、dirty、flush、invalid),直接暴露给上层会非常危险!
  • 外部无法直接拿到 byte[] contents,必须通过受控接口;
  • 错误操作(如修改一个 invalidated frame)会被屏蔽;
  • Page 负责在访问前 pin 页面、访问后 unpin。

2. 集成锁机制(LockContext)

每个 Page 都有:
在执行读写操作时:
  • 这意味着每个 Page 自动和事务锁系统绑定
  • 上层模块访问 Page 时,不用自己显式加锁,统一控制并发。

3. 事务恢复与 WAL 支持的入口

例如:
恢复系统可以安全地通过 Page 接口设置页面日志序号(Page LSN),而不暴露脆弱的 Frame 内部逻辑。

4. 提供安全的 Buffer 读写接口(PageBuffer)

  • PageBuffer 是对数据内容的高层抽象,符合 Java NIO 风格;
  • 它是“受限视图”:支持 slice、duplicate,支持位置管理;
  • 所有读写都回调 Page.readBytes() / writeBytes(),再次加锁 & 校验。

5. 支持生命周期管理和访问自动刷新

  • page.pin() 会重新加载帧(如果失效);
  • page.unpin() 会释放对页的使用权;
  • page.flush() 明确写回;
  • page.wipe() 方便清零操作(例如初始化表页或索引页);
这一切都通过 Page 接口完成,外部不需要关心 Frame 的状态或存在性。

6. 为扩展设计留出空间(多版本、只读页等)

未来可以轻松扩展:
  • MVCCPage extends Page:实现只读快照页面;
  • ReadOnlyPage:只允许 get,不允许 put;
  • IndexPage extends Page:带特定格式解析能力;
  • 无需改变底层 BufferManager / Frame 代码

小结:Page 的定位与价值

功能
说明
🔒 访问控制
屏蔽 Frame 内部细节,提供安全 API
🔗 并发控制
集成 LockContext,自动加锁校验
🔁 生命周期管理
支持 pin/unpin/flush/wipe
📦 数据访问
提供 Buffer 风格的数据操作(get/put/slice)
💥 事务兼容
支持 LSN 设置、日志记录
🔧 易于扩展
后续支持 Snapshot、MVCC、只读页等

类图视图(简化)

 

核心方法 fetchPageFrame(long pageNum)

fetchPageFrame(long pageNum)BufferManager 类的核心方法之一,它的作用是:
返回指定页号的 Frame(内存帧),并确保页面已加载到内存中且被 pin(固定)了,不能被驱逐。
它是 页面请求处理的入口,实现了:
  • 缓存命中判断
  • 缺页加载
  • 页框分配 / 页面置换
  • 线程安全与锁保护
  • 脏页刷盘(如果有驱逐)
  • 磁盘读取

方法结构:

整体流程结构如下:

  1. 加锁 managerLock,确保线程安全
  1. 检查页面是否已经加载
  1. 选帧(优先找空帧,否则触发置换)
  1. 创建新 Frame 对象并更新元数据
  1. 刷出旧帧数据(如果需要)
  1. 从磁盘读取目标页
  1. 返回已 pin 的新 Frame

步骤详解(逐段解读):

1. 锁定 BufferManager

防止多个线程同时操作帧分配或替换,保证线程安全。

2. 页面是否已在内存?

如果页号已存在于缓存中:
  • 直接 pin 它,表示正在使用;
  • 这是一个命中(hit),无需 I/O;
  • 直接返回该 Frame。

3. 找空帧 or 执行页面置换

  • 优先使用空帧:空帧链表中的第一个 firstFreeIndex
  • 否则触发置换策略
    • 调用 EvictionPolicy.evict() 返回一个可替换的帧;
    • pageToFrame 映射中移除旧页;
    • 调用策略的 cleanup() 执行额外清理。

4. 构建新 Frame,更新元数据

  • 创建新的 Frame 对象(重用旧的 contents 数组);
  • 将其放入缓存帧数组;
  • 调用策略的 init() 方法通知新页面加载;
  • 更新 page → frame 映射。

5. Flush 并 Invalidate 被替换的帧

  • 如果被替换帧是脏页 → 则写回磁盘;
  • 然后标记为无效,表示其内容不可再用。

6. 从磁盘读取新页面数据

  • 从磁盘中读取页面内容,写入 contents
  • IO 计数器自增。

7. 返回新 Frame

  • Pin 住该页面;
  • 返回 Frame,供调用者操作。

总结图示流程:

补充说明:

  • frame.pin() 与 frame.unpin() 的管理非常关键,确保不会驱逐正在被使用的页面;
  • 线程安全性设计优秀managerLock + 每个 frameLock 实现读写隔离;
  • 可与 EvictionPolicy 插拔使用,支持 LRU、Clock、MRU 等策略;
  • 充分复用了旧帧的 byte[] contents,减少内存分配,效率更高。
 
  • database
  • Database System(四)DS3.1 Clock Policy(时钟算法)
    Loading...
    Catalog
    0%