一起写个数据库 —— 3. 数据页的缓存与管理

本文原载于我的博客:https://ziyang.moe/article/mydb3.html

本章涉及代码都在 https://github.com/CN-GuoZiyang/MYDB/tree/master/src/main/java/top/guoziyang/mydb/backend/dm/pageCachehttps://github.com/CN-GuoZiyang/MYDB/tree/master/src/main/java/top/guoziyang/mydb/backend/dm/page 中。

前言

本节主要内容就是 DM 模块向下对文件系统的抽象部分。DM 将文件系统抽象成页面,每次对文件系统的读写都是以页面为单位的。同样,从文件系统读进来的数据也是以页面为单位进行缓存的。

页面缓存

这里参考大部分数据库的设计,将默认数据页大小定为 8K。如果想要提升向数据库写入大量数据情况下的性能的话,也可以适当增大这个值。

上一节我们已经实现了一个通用的缓存框架,那么这一节我们需要缓存页面,就可以直接借用那个缓存的框架了。但是首先,需要定义出页面的结构。注意这个页面是存储在内存中的,与已经持久化到磁盘的抽象页面有区别。

定义一个页面如下:

public class PageImpl implements Page {
    private int pageNumber;
    private byte[] data;
    private boolean dirty;
    private Lock lock;
    
    private PageCache pc;
}

其中,pageNumber 是这个页面的页号,该页号从 1 开始。data 就是这个页实际包含的字节数据。dirty 标志着这个页面是否是脏页面,在缓存驱逐的时候,脏页面需要被写回磁盘。这里保存了一个 PageCache(还未定义)的引用,用来方便在拿到 Page 的引用时可以快速对这个页面的缓存进行释放操作。

定义页面缓存的接口如下:

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);
}

页面缓存的具体实现类,需要继承抽象缓存框架,并且实现 getForCache()releaseForCache() 两个抽象方法。由于数据源就是文件系统,getForCache() 直接从文件中读取,并包裹成 Page 即可:

@Override
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();
    return new PageImpl(pgno, buf.array(), this);
}

private static long pageOffset(int pgno) {
    // 页号从 1 开始
    return (pgno-1) * PAGE_SIZE;
}

releaseForCache() 驱逐页面时,也只需要根据页面是否是脏页面,来决定是否需要写回文件系统:

@Override
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();
    }
}

PageCache 还使用了一个 AtomicInteger,来记录了当前打开的数据库文件有多少页。这个数字在数据库文件被打开时就会被计算,并在新建页面时自增。

public int newPage(byte[] initData) {
    int pgno = pageNumbers.incrementAndGet();
    Page pg = new PageImpl(pgno, initData, null);
    flush(pg);  // 新建的页面需要立刻写回
    return pgno;
}

提一点,同一条数据是不允许跨页存储的,这一点会从后面的章节中体现。这意味着,单条数据的大小不能超过数据库页面的大小。

数据页管理

第一页

数据库文件的第一页,通常用作一些特殊用途,比如存储一些元数据,用来启动检查什么的。MYDB 的第一页,只是用来做启动检查。具体的原理是,在每次数据库启动时,会生成一串随机字节,存储在 100 ~ 107 字节。在数据库正常关闭时,会将这串字节,拷贝到第一页的 108 ~ 115 字节。

这样数据库在每次启动时,就会检查第一页两处的字节是否相同,以此来判断上一次是否正常关闭。如果是异常关闭,就需要执行数据的恢复流程。

启动时设置初始字节:

public static void setVcOpen(Page pg) {
    pg.setDirty(true);
    setVcOpen(pg.getData());
}

private static void setVcOpen(byte[] raw) {
    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.compare(raw, OF_VC, OF_VC+LEN_VC, raw, OF_VC+LEN_VC, OF_VC+2*LEN_VC) == 0;
}

似乎就是这个 Arrays.compare() 方法不兼容 JDK8,可以使用其他等价的方法替换。

普通页

MYDB 对于普通数据页的管理就比较简单了。一个普通页面以一个 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());
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
好的,让我们开始Python模拟登录imooc.com的练习。 首先,我们需要导入需要的库,包括requests和BeautifulSoup。requests库用于发送http请求,BeautifulSoup库用于解析html文本。 ```python import requests from bs4 import BeautifulSoup ``` 接下来,我们需要获取登录面的html代码。我们可以使用requests库的get()方法来发送一个get请求,并将返回的html代码存储在一个变量中。 ```python login_url = 'https://www.imooc.com/' login_page = requests.get(login_url) login_soup = BeautifulSoup(login_page.text, 'html.parser') ``` 接下来,我们需要从登录面中获取登录所需的信息,包括表单的action、method、以及需要提交的表单数据。我们可以使用BeautifulSoup库来解析html代码,并使用find()方法来查找表单元素。 ```python # 获取登录表单元素 login_form = login_soup.find('form', attrs={'id': 'signup-form'}) # 获取表单的action和method action_url = login_form['action'] method = login_form['method'] # 获取需要提交的表单数据 email = input('请输入邮箱:') password = input('请输入密码:') data = { 'email': email, 'password': password } ``` 接下来,我们可以使用requests库的post()方法来发送一个post请求,将表单数据提交到服务器。需要注意的是,我们需要将表单数据和请求头一起提交到服务器。 ```python # 构造请求头 headers = { 'Referer': login_url, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'} # 发送post请求 response = requests.post(action_url, data=data, headers=headers) ``` 最后,我们可以判断登录是否成功,可以通过检查返回的html代码中是否包含登录成功的提示信息来判断。 ```python # 判断登录是否成功 if '个人中心' in response.text: print('登录成功!') else: print('登录失败!') ``` 完整代码如下: ```python import requests from bs4 import BeautifulSoup # 获取登录面html代码 login_url = 'https://www.imooc.com/' login_page = requests.get(login_url) login_soup = BeautifulSoup(login_page.text, 'html.parser') # 获取登录表单元素 login_form = login_soup.find('form', attrs={'id': 'signup-form'}) # 获取表单的action和method action_url = login_form['action'] method = login_form['method'] # 获取需要提交的表单数据 email = input('请输入邮箱:') password = input('请输入密码:') data = { 'email': email, 'password': password } # 构造请求头 headers = { 'Referer': login_url, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'} # 发送post请求 response = requests.post(action_url, data=data, headers=headers) # 判断登录是否成功 if '个人中心' in response.text: print('登录成功!') else: print('登录失败!') ``` 注意,此代码仅供学习参考。在实际应用中,请勿使用模拟登录的方式获取个人信息,以免造成不必要的风险。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值