手写数据库轮子项目 MYDB 之三 | DataManager (DM) 数据之数据页缓存与管理

本节我们介绍数据页在缓存中的定义和管理方法。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);
    }

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值