本节我们介绍数据页在缓存中的定义和管理方法。DM 将文件系统抽象为一个个的页面,每次对磁盘中数据文件的读写是以页面为单位的。从磁盘中读进来的数据也是以页面为单位缓存的。
一、页面
缓存中的一个页面定义如下。pageNumber 是这个页的页号,页号从1开始。data 是这个页实际记录的字节数据。 dirty 标志这个页面是否为脏页。pc 是对 PageCache 的引用,为了可以通过 page 方便地对缓存进行操作。
public class PageImpl implements Page {
private int pageNumber;
private byte[] data;
private boolean dirty;
private Lock lock;
private PageCache pc; // 接口引用指向实现类的对象
}
二、页面缓存
1. 页面缓存接口
PageCache接口定义了一些方法供其实现类使用,具体包括:
public interface PageCache {
public static final int PAGE_SIZE = 1 << 13;
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);
}
接口另外还提供了两个静态方法分别用于创建一个新的 db 文件并返回 PageCache 对象,和从一个已有的 db 文件创建 PageCache 对象。
public static PageCacheImpl create(String path, long memory) {
File f = new File(path+PageCacheImpl.DB_SUFFIX);
try {
if(!f.createNewFile()) {
Panic.panic(Error.FileExistsException);
}
} catch (Exception e) {
Panic.panic(e);
}
if(!f.canRead() || !f.canWrite()) {
Panic.panic(Error.FileCannotRWException);
}
FileChannel fc = null;
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(f, "rw");
fc = raf.getChannel();
} catch (FileNotFoundException e) {
Panic.panic(e);
}
return new PageCacheImpl(raf, fc, (int)memory/PAGE_SIZE);
}
public static PageCacheImpl open(String path, long memory) {
File f = new File(path+PageCacheImpl.DB_SUFFIX);
if(!f.exists()) {
Panic.panic(Error.FileNotExistsException);
}
if(!f.canRead() || !f.canWrite()) {
Panic.panic(Error.FileCannotRWException);
}
FileChannel fc = null;
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(f, "rw");
fc = raf.getChannel();
} catch (FileNotFoundException e) {
Panic.panic(e);
}
return new PageCacheImpl(raf, fc, (int)memory/PAGE_SIZE);
}
2. 页面缓存接口的实现类
(1). 属性
pageNumbers 用来记录了当前打开的数据库 db 文件中有多少个数据页。
private static final int MEM_MIN_LIM = 10;
public static final String DB_SUFFIX = ".db";
private RandomAccessFile file;
private FileChannel fc;
private Lock fileLock;
private AtomicInteger pageNumbers;
(2). 构造方法
pageNumbers 在数据库文件被打开时就会被计算。并在新建页面时自增(下文 newPage 方法)。
PageCacheImpl(RandomAccessFile file, FileChannel fileChannel, int maxResource) {
super(maxResource);
if(maxResource < MEM_MIN_LIM) {
Panic.panic(Error.MemTooSmallException);
}
long length = 0;
try {
length = file.length();
} catch (IOException e) {
Panic.panic(e);
}
this.file = file;
this.fc = fileChannel;
this.fileLock = new ReentrantLock();
this.pageNumbers = new AtomicInteger((int)length / PAGE_SIZE);
}
(3). 重写父类方法
接口的实现类不仅实现了接口,也继承了上一节中的缓存框架,实现 getForCache() 和 releaseForCache() 这两个抽象方法。
getForCache() 从磁盘文件中读取,并包裹成 Page 。
protected Page getForCache(long key) throws Exception {
int pgno = (int)key;
long offset = PageCacheImpl.pageOffset(pgno);
ByteBuffer buf = ByteBuffer.allocate(PAGE_SIZE);
fileLock.lock();
try {
fc.position(offset);
fc.read(buf);
} catch(IOException e) {
Panic.panic(e);
}
fileLock.unlock();
//注意参数中有this,也就是PageCache对象,方便Page对象操作缓存
return new PageImpl(pgno, buf.array(), this);
}
private static long pageOffset(int pgno) {
// 页号从 1 开始
return (pgno-1) * PAGE_SIZE;
}
releaseForCache() 驱逐页面时,也只需要根据页面是否是脏页面,来决定是否需要写回数据库。
protected void releaseForCache(Page pg) {
if(pg.isDirty()) {
flush(pg);
pg.setDirty(false);
}
}
private void flush(Page pg) {
int pgno = pg.getPageNumber();
long offset = pageOffset(pgno);
fileLock.lock();
try {
ByteBuffer buf = ByteBuffer.wrap(pg.getData());
fc.position(offset);
fc.write(buf);
fc.force(false);
} catch(IOException e) {
Panic.panic(e);
} finally {
fileLock.unlock();
}
}
(4). 重写接口方法
newPage() 方法
pageNumbers 在新建页面时自增。其中返回 page 对象时,缓存引用为 null。flush() 在新建页面时立刻写回磁盘。
public int newPage(byte[] initData) {
int pgno = pageNumbers.incrementAndGet();
Page pg = new PageImpl(pgno, initData, null);
flush(pg);
return pgno;
}
private void flush(Page pg) {
int pgno = pg.getPageNumber();
long offset = pageOffset(pgno);
fileLock.lock();
try {
ByteBuffer buf = ByteBuffer.wrap(pg.getData());
fc.position(offset);
fc.write(buf);
fc.force(false);
} catch(IOException e) {
Panic.panic(e);
} finally {
fileLock.unlock();
}
}
getPage() 方法
调用上一章中的 get() 方法从缓存中得到 page 对象。
public Page getPage(int pgno) throws Exception {
return get((long)pgno);
}
close() 方法
调用上一章中的 close() 方法关闭缓存。
public void close() {
super.close();
try {
fc.close();
file.close();
} catch (IOException e) {
Panic.panic(e);
}
}
release() 方法
调用上一章中的 relsease() 方法释放缓存。release() 方法会使引用减一,当为0时释放。又调用了本类中的 releaseForCache() 方法。
public void release(Page page) {
release((long)page.getPageNumber());
}
truncateByBgno() 方法
将文件截断到指定页号(maxPageno参数所指定的页号)的末尾,同时更新页号信息。
public void truncateByBgno(int maxPgno) {
long size = pageOffset(maxPgno + 1);
try {
file.setLength(size);
} catch (IOException e) {
Panic.panic(e);
}
pageNumbers.set(maxPgno);
}
getPageNumber() 方法
获得页号
public int getPageNumber() {
return pageNumbers.intValue();
}
flushPage() 方法
页数据写回磁盘
public void flushPage(Page pg) {
flush(pg);
}
三、数据页管理
1. 第一页 PageOne
数据库文件的第一页,通常用作一些特殊用途,比如存储一些元数据,用来启动检查什么的.MYDB 的第一页,只是用来做启动检查。具体的原理是,在每次数据库启动时,会生成一串随机字节,存储在 100 ~ 107 字节。在数据库正常关闭时,会将这串字节,拷贝到第一页的 108 ~ 115 字节。
这样数据库在每次启动时,就会检查第一页两处的字节是否相同,以此来判断上一次是否正常关闭。如果是异常关闭,就需要执行数据的恢复流程。
提供了一个由静态方法构成的工具类,用来对第一页管理。
启动时设置初始字节:
public static void setVcOpen(Page pg) {
pg.setDirty(true);
setVcOpen(pg.getData());
}
private static void setVcOpen(byte[] raw) {
//将随机数拷贝至raw中的第100-107字节
System.arraycopy(RandomUtil.randomBytes(LEN_VC), 0, raw, OF_VC, LEN_VC);
}
关闭时拷贝字节:
public static void setVcClose(Page pg) {
pg.setDirty(true);
setVcClose(pg.getData());
}
private static void setVcClose(byte[] raw) {
System.arraycopy(raw, OF_VC, raw, OF_VC+LEN_VC, LEN_VC);
}
校验字节:
public static boolean checkVc(Page pg) {
return checkVc(pg.getData());
}
private static boolean checkVc(byte[] raw) {
return Arrays.equals(Arrays.copyOfRange(raw, OF_VC, OF_VC+LEN_VC), Arrays.copyOfRange(raw, OF_VC+LEN_VC, OF_VC+2*LEN_VC));
}
2. 普通页 PageX
提供了一个由静态方法构成的工具类,用来对普通页管理。
一个普通页面以一个 2 字节无符号数起始,表示这一页指针的偏移(即这一页写到了哪个位置)。剩下的部分都是实际存储的数据。所以对普通页的管理,基本都是围绕着对 FSO(Free Space Offset),也就是接下来往哪里插入数据进行的。
向页面插入数据:
// 将raw插入pg中,返回插入位置
public static short insert(Page pg, byte[] raw) {
pg.setDirty(true);
short offset = getFSO(pg.getData());
System.arraycopy(raw, 0, pg.getData(), offset, raw.length);
setFSO(pg.getData(), (short)(offset + raw.length));
return offset;
}
在写入之前获取 FSO,来确定写入的位置,并在写入之后更新 FSO。FSO 的操作如下:
private static void setFSO(byte[] raw, short ofData) {
System.arraycopy(Parser.short2Byte(ofData), 0, raw, OF_FREE, OF_DATA);
}
// 获取pg的FSO
public static short getFSO(Page pg) {
return getFSO(pg.getData());
}
private static short getFSO(byte[] raw) {
return Parser.parseShort(Arrays.copyOfRange(raw, 0, 2));
}
// 获取页面的空闲空间大小
public static int getFreeSpace(Page pg) {
return PageCache.PAGE_SIZE - (int)getFSO(pg.getData());
}
剩余两个函数 recoverInsert()
和 recoverUpdate()
用于在数据库崩溃后重新打开时(即第一页的校验不对时),恢复例程直接插入数据以及修改数据使用。
// 将raw插入pg中的offset位置,并将pg的offset设置为较大的offset
public static void recoverInsert(Page pg, byte[] raw, short offset) {
pg.setDirty(true);
System.arraycopy(raw, 0, pg.getData(), offset, raw.length);
short rawFSO = getFSO(pg.getData());
if(rawFSO < offset + raw.length) {
setFSO(pg.getData(), (short)(offset+raw.length));
}
}
// 将raw插入pg中的offset位置,不更新update
public static void recoverUpdate(Page pg, byte[] raw, short offset) {
pg.setDirty(true);
System.arraycopy(raw, 0, pg.getData(), offset, raw.length);
}