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
中实现。
注意点:
- 页面加载过程线程安全(managerLock 和 frameLock 双锁)
- 使用 Java 位运算(~)实现自由帧的链式管理
- 调用恢复管理器(RecoveryManager)记录 WAL 和 flush hook
- 通过
EvictionPolicy
接口实现可扩展替换策略
- 封装一致性好,兼容恢复、事务和锁机制
总结类的职责结构:
组件 | 作用 |
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( isValid 、invalidate )或 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()
;
- 比较写入内容与原数据是否不一致:
- 如果变更,生成
before
和after
内容; - 通过
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 中恢复出“下一个空闲帧的位置”。
空闲链表的管理
🟩 标记为 Free:setFree()
🟦 标记为 Used:setUsed()
举个例子:
假设:
- 当前空闲帧链:
[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(固定)了,不能被驱逐。
它是 页面请求处理的入口,实现了:
- 缓存命中判断
- 缺页加载
- 页框分配 / 页面置换
- 线程安全与锁保护
- 脏页刷盘(如果有驱逐)
- 磁盘读取
方法结构:
整体流程结构如下:
- 加锁
managerLock
,确保线程安全
- 检查页面是否已经加载
- 选帧(优先找空帧,否则触发置换)
- 创建新 Frame 对象并更新元数据
- 刷出旧帧数据(如果需要)
- 从磁盘读取目标页
- 返回已 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
,减少内存分配,效率更高。