数据库:存放变长记录的Page类代码实现

原文链接:https://littlefish33.cn/DataBase/SimplePage/

设计思路

文章实现的思路大致参考教材Database Management Systems, 3rd Edition by Raghu Ramakrishnan (Author), Johannes Gehrke (Author)

下面内容摘自教材:

​ 对变长记录最灵活的组织方式是为每一页维护一个槽目录,每个槽有( record offset, record length) 对组成,第一部分(record offset) 是指向记录的“指针“。如下图所示,它表示从页上数据部分的开始处到记录的开始处的字节偏移量。删除操作通过设置记录的偏移量为-1很容易完成。记录能在页内以移动,因为由页号和槽号组成的rid(即目录中的位置)在记录移动时不会发生改变,只有存储在槽中的记录的偏移量改变了。

​ 因为页没有被预先格式化成槽,所以,为新记录分配空间必须小心管理。管理空间的一种方法是维护一个指向空闲空间开始处的指针(即从数据部分开始处的偏移量)。当一个新记录太大而不能放在剩余的空闲空间时,就不得不在页内移动记录以收回先前已被删除记录释放的空间。其思想是确保重新组织后,所有记录是连续的,后面是可分配的空闲空间。

​ 值得注意的是,被删除记录所在的槽不能从槽目录中移出,因为槽号用于标识记录,如果删除一个槽,将改变槽目录中后续槽的槽号,以至于导致后续槽所指向的记录的rid的改变。从槽目录中删除槽的唯一方法是当最后一个槽所指向的记录被删除后,可以移出最后一个槽。然而,当插入一个记录时,应该首先扫描槽目录已寻找目前未指向任何目录的槽,并把新记录存于该槽。只有当所有存在的槽都指向记录时,才能向槽目录中增加新的槽。如果插入操作比删除操作更普遍(这是比较典型的情况),槽目录中的目录项数将非常接近页上的实际记录数。

​ 上述组织方法对于需要频繁移动的定长记录的情况也是很有用的,如需要维护记录的某种排列顺序的情况。事实上,当所有记录长度相同时,可以不再每个记录的槽中存储这些公共长度的信息,而只在系统目录中存储一次。

代码实现

Page类的初始化结构

​ 在没有插入任何数据项的情况下,页内的数据结构如下图所示,首先,存放的是页的配置信息,然后是指向第一个槽的空间,剩下的空间是空闲空间,之后的记录项将倒着插入这个空间内;槽结构包括槽的长度和对应记录的起始地址

//Page.hpp
class Page
{
private:
    //槽
    struct Slot_t
    {
        Offset offset;
        /* 如果槽是空的,length的长度为-1 */
        Length length;
    };
    /* 页的配置项所占用的空间,总大小 - DPFIXED = 数据的空间 */
    static const int DPFIXED = sizeof(Slot_t) + sizeof(SlotNumber) + sizeof(LocationIndex) * 2 + sizeof(PageId) * 3 + sizeof(bool) * 4; 
    SlotNumber slotNumber;      /* 页里面的槽的数目 */
    LocationIndex   pd_lower;   /* 空闲空间的起始地址,终止地址 - 起始地址 = 剩余空闲空间的大小 */
    LocationIndex   pd_upper;   /* 空闲空间的终止地址,终止地址 - 起始地址 = 剩余空闲空间的大小 */

    PageId prevPage;    /* 上一页的页ID */
    PageId curPage;     /* 当前的页ID */
    PageId nextPage;    /* 下一页的页ID */

    bool full;

    Slot_t slot[1];                  /* 槽数据的第一个地址 */
    char data[MAX_SPACE - DPFIXED];  /* 空闲空间 */

    //......这里省略了页面的功能函数
}

插入记录的一种情况

​ 当插入记录的时候,首先检查记录的长度是否超过了空闲空间的大小,如果是的话,将会返回信息——页面已满,反之,如果空闲空间大于记录的长度,那么将会把记录倒着插入空闲空间;由于slot[0]的空间后紧跟着就是空闲空间的起始地址,(这里是C++提供的便利,就不用像上面教材里说的一样使用一个指向空闲空间的指针了,︿( ̄︶ ̄)︿),因此即使我们在构造Page类的时候使Slot目录申请为大小为1的数组,但是我们依然可以使用slot[1],slot[2]…..指向槽目录下面的空间,因为它们将指向空闲空间,我们只需要插入记录的时候同时移动指向空闲空间的起始位置的指针,那么由于数据项是倒着插入的,那么数据项将不会和槽同时占用一个空间导致内存访问冲突;需要注意的是,槽的数目永远都比记录项多一;

如图:

/*
 * 文件: Page.cpp
 * 函数:Status Page::insertRecord(char* recPtr, int recLen)
 */

if (recLen > pd_upper - pd_lower) {
    //这里省略了插入数据的另外一种情况,将在下面介绍完删除记录后介绍
}
else {
    //如果空闲空间足够大,将新数据直接插入空闲空间,同时更新页面属性
    pd_upper -= recLen;
    pd_lower += sizeof(Slot_t);
    slot[slotNumber].offset = pd_upper;
    slot[slotNumber].length = recLen;
    memcpy(data + pd_upper, recPtr, recLen);
    slotNumber++;
    int insertKey;
    memcpy(&insertKey, data + slot[slotNumber-1].offset, 4);
    //排序
    sort_slot(insertKey,recLen,pd_upper);
    return SUCCESS;
}

删除记录

删除记录的情况就很简单了,直接将对应槽号的长度设为0,表示该记录失效;

/*
 * 文件: Page.cpp
 * 函数:Status Page::deleteRecord(int key)
 */
Status Page::deleteRecord(int key)
{
    int slotNo;

    if (!isExist(key, slotNo)) {
        return RECORDNOTFOUND;
    }   
    //删除记录,直接将slot的长度置为0
    slot[slotNo].length = 0;
    full = false;
    return SUCCESS;

}

插入记录的另一种情况

​ 因为删除记录的时候我们只是简单地将对应槽号的长度设置为0,因此即使pd_upper - pd_lower < 记录的长度,但是也存在有的槽记录是无效的情况,那么我们就需要遍历所有的槽,检查是否有槽是无效的,如果有,那么插入记录;反之,返回页已满。

/*
 * 文件: Page.cpp
 * 函数:Status Page::insertRecord(char* recPtr, int recLen)
 */
if (recLen > pd_upper - pd_lower) {
    //如果不存在空闲空间,那么判断是否有槽的数据是没用的
    for (size_t i = 0; i < slotNumber; i++)
    {
        //如果槽内的记录的长度为0,表示记录无效,将新纪录覆盖该记录
        if (slot[i].length == 0) {
            slot[i].length = recLen;
            memmove(data + slot[i].offset, recPtr, recLen);
            int insertKey;
            memcpy(&insertKey, recPtr, 4);
            //排序
            sort_slot(i,insertKey, recLen, slot[i].offset);
            return SUCCESS;
        }
    }
    full = true;
    return PAGEFULL;
}

排序

​ 我们的页面里存放的是有序的记录,因此对于每次插入的数据,我们需要对页面的内容进行排序,但是,因为记录项很可能比槽的大小大很多,如果频繁地移动记录的话,很可能造成很大的消耗,因此,我们只需要移动槽的内容就可以了,这样通过遍历槽取出数据的时候,就可以保证数据是有序的。


/*
 * 文件: Page.cpp
 * 函数:void Page::sort_slot(int insertKey,int length,int offset)
 */
void Page::sort_slot(int insertKey,int length,int offset)
{
    int curKey;
    //对页面的记录进行排序,排序的过程实际上移动的是槽的值
    for (size_t i = 0; i < slotNumber; i++)
    {
        if (slot[i].length != 0) {
            memcpy(&curKey, data + slot[i].offset, 4);
            if (curKey > insertKey) {
                for (size_t j = slotNumber - 1; j > i; j--)
                {
                    slot[j].offset = slot[j - 1].offset;
                    slot[j].length = slot[j - 1].length;
                }
                slot[i].length = length;
                slot[i].offset = offset;
                return;
            }
        }
    }
}

总结

整个Page类的实现过程就是这样了,比较有意思的部分是slot的实现方式,其他的只要根据教材的设计思路实现就没什么问题了,啦啦啦

整个Page类代码下载:SimplePage

项目中main.cpp为测试代码,运行效果如下:

rel.hpp里可以设置Page的大小,测试代码里是100B

猜你喜欢

转载自blog.csdn.net/qiuxy23/article/details/81266062