数据页缓存
约 1150 字大约 4 分钟
2025-01-15
1.设计思路
数据页缓存:将页面数据保存在内存中,减小频繁的磁盘I/O操作,将默认数据页的大小定为8KB
2.Page类
com.peng.minidb.backend.dm.page.Page类
com.peng.minidb.backend.dm.page.PageImpl类
- 定义:页面(Page)是数据库中存储数据的基本单元
- 结构:
- pageNumber:页号
- data:字节数据
- dirty:是否脏页面数据;脏页面在释放页面缓存时需要被写回到磁盘
- lock:锁,用于保证页面操作的线程安全
- PageCache:引用页面缓存
3.PageCache页面缓存接口
com.peng.minidb.backend.dm.pageCache.PageCache
定义了页面缓存的接口,包括新建页面、获取页面、释放页面、关闭缓存、根据最大页号截断缓存、获取当前页面数量以及刷新页面等方法
public interface PageCache {
// 新建页面
int newPage(byte[] initData);
Page getPage(int pgno) throws Exception;
void close();
void release(Page page);
void truncateByBgno(int maxPgno);
int getPageNumber();
void flushPage(Page pg);
}
4.PageCacheImpl页面缓存实现类
4.1 继承抽象缓存框架
实现以下两个方法
getForCache()
:用于从文件中读取页面数据,并将其包装成Page
对象。releaseForCache()
:用于在驱逐页面时根据页面是否为脏页面,决定是否将其写回文件系统。
4.1.1 getForCache()
/**
* 根据pageNumber从数据库文件中读取页数据,并包裹成Page
*/
@Override
protected Page getForCache(long key) throws Exception {
// 将key转换为页码
int pgno = (int) key;
// 计算页码对应的偏移量
long offset = PageCacheImpl.pageOffset(pgno);
// 分配一个大小为PAGE_SIZE的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(PAGE_SIZE);
// 锁定文件,确保线程安全
fileLock.lock();
try {
// 设置文件通道的位置为计算出的偏移量
fc.position(offset);
// 从文件通道读取数据到ByteBuffer
fc.read(buf);
} catch (IOException e) {
// 如果发生异常,调用Panic.panic方法处理
Panic.panic(e);
}
// 无论是否发生异常,都要解锁
fileLock.unlock();
// 使用读取到的数据、页码和当前对象创建一个新的PageImpl对象并返回
return new PageImpl(pgno, buf.array(), this);
}
public PageImpl(int pageNumber, byte[] data, PageCache pc) {
this.pageNumber = pageNumber; // 设置页面的页号
this.data = data; // 设置页面实际包含的字节数据
this.pc = pc; // 设置页面缓存
lock = new ReentrantLock(); // 初始化一个新的可重入锁
}
4.1.2 releaseForCache
当页面不再需要时,releaseForCache()
方法会检查页面是否是脏页面,如果页面被标记为脏页面(即其数据已被修改但尚未写回磁盘),则需要通过 flush()
方法将其内容写入磁盘。
@Override
protected void releaseForCache(Page pg) {
if (pg.isDirty()) {
flush(pg);
pg.setDirty(false);
}
}
private void flush(Page pg) {
int pgno = pg.getPageNumber(); // 获取Page的页码
long offset = pageOffset(pgno); // 计算Page在文件中的偏移量
fileLock.lock(); // 加锁,确保线程安全
try {
ByteBuffer buf = ByteBuffer.wrap(pg.getData()); // 将Page的数据包装成ByteBuffer
fc.position(offset); // 设置文件通道的位置
fc.write(buf); // 将数据写入到文件中
fc.force(false); // 强制将数据从操作系统的缓存刷新到磁盘
} catch (IOException e) {
Panic.panic(e); // 如果发生异常,调用Panic.panic方法处理
} finally {
fileLock.unlock(); // 最后,无论是否发生异常,都要解锁
}
}
4.2 新建页面
public int newPage(byte[] initData) {
int pgno = pageNumbers.incrementAndGet();
Page pg = new PageImpl(pgno, initData, null);
flush(pg); // 新建的页面需要立刻写回
return pgno;
}
4.3 数据恢复操作
数据恢复操作是为了确保在系统崩溃后,能够从日志或其他持久化存储中恢复数据。recoverInsert()
和 recoverUpdate()
方法用于在数据库崩溃后重新插入数据和恢复修改
recoverInsert()
:将数据插入到指定的偏移位置,并更新空闲空间的偏移量
recoverUpdate()
:在指定的偏移位置直接更新数据,而不更新空闲空间的偏移量
// 将raw插入pg中的offset位置,并将pg的offset设置为较大的offset
public static void recoverInsert(Page pg, byte[] raw, short offset) {
pg.setDirty(true); // 将pg的dirty标志设置为true,表示pg的数据已经被修改
System.arraycopy(raw, 0, pg.getData(), offset, raw.length); // 将raw的数据复制到pg的数据中的offset位置
short rawFSO = getFSO(pg.getData()); // 获取pg的当前空闲空间偏移量
if (rawFSO < offset + raw.length) { // 如果当前的空闲空间偏移量小于offset + raw.length
setFSO(pg.getData(), (short) (offset + raw.length)); // 将pg的空闲空间偏移量设置为offset + raw.length
}
}
// 将raw插入pg中的offset位置,不更新update
public static void recoverUpdate(Page pg, byte[] raw, short offset) {
pg.setDirty(true); // 将pg的dirty标志设置为true,表示pg的数据已经被修改
System.arraycopy(raw, 0, pg.getData(), offset, raw.length); // 将raw的数据复制到pg的数据中的offset位置
}