实现一个数据库(8)数据模型

github链接(更新中)

https://github.com/pourtheworld/mydb


大纲(更新中)
(0)mydb的架构设想


数据模型的预备知识

数据模型的设计从传统数据库的存储格式(对于定长数据项和非定长数据项的思考)->
传统数据库的存储(slot+record)->
传统数据库文件的存储(SME+tablemap+record+index)

传统数据库的存储格式

传统数据库的存储方式:定长的数据项被存在4K大小的数据页里。
在这里插入图片描述
Nosql:不定长的数据。数据页还是存储的最小单位,但是数据块可以由多个数据页组成,每个数据项可以跨页但是不能跨块。由于Mmap文件的映射区域无法更改,我们每次更新文件的大小时都重新映射一个128M定长的数据段
即 数据块可以由多个数据页构成但大小不能超过一个数据段。数据项可以跨页但不能跨块
在这里插入图片描述

传统数据库页的存储

传统数据页里的存储:
每个数据记录record会在页内对应一个slot,slot记录了该data在页内的偏移等情况。slot从前往后存储,data从页的结尾往前存储。这样做的好处在于无需考虑slot要预留的个数。

在这里插入图片描述
当我们删除某个记录时,无需直接更改记录,只要把偏移置为-1即可。
在这里插入图片描述

当删除记录过多,我们需要对数据页进行重组,去掉无用的数据记录。
在这里插入图片描述
我们会挨个检查slot对应data的偏移是否为-1,比如data1的偏移为-1,我们跳过;
比如data2的偏移不为-1,我们将slot2的data2数据复制到最近的偏移为-1的记录块,并将slot2指向data1。

在这里插入图片描述
重组完以后只剩下data2以及data4:
在这里插入图片描述

传统数据库文件的存储

传统数据库文件的存储方式:
由SME+表的映射+其他数据页构成。
SME:空间管理段,存放了该文件中数据页的可用情况


传统的数据库一个文件中可以存放多个表,我们建立一个表的映射
一个表内会存放表的元数据、数据记录与存放页的映射、索引与存放页的映射。
一般索引为B树的结构,里面的key又会指向对应value数据记录存放的页。
在这里插入图片描述

数据模型的具体设计与实现

首先我们会提出数据模型的设计方案,再细化到文件头、页头的定义;
之后由于要把文件放到内存中去,因此会介绍一下内存映射函数;
最后是数据模型类的核心函数实现。

data model类的设计

对比传统数据库的文件,我们进行结构的简化:

  1. 保留文件头,去掉SME(空间管理段)。
  2. 由于是类Mongo的nosql存储,我们去掉表的概念,把一个个页存到文件里。(相当于一个表)
  3. 暂时不用B树类索引,通过stl库的map函数做简易的映射索引。
  4. 文件作为数据模型的基本单位,页作为存储的基本单位设置为4M。我们设置页的最大数量为256K,因此存储的量最多可以到4M*256K=1T。
  5. 由于之前阐述过nosql的不定长record概念,我们将数据段设置为128M,每个文件一次映射128M的数据段到内存。一个数据段=32个数据页。

在这里插入图片描述
文件头设计
在这里插入图片描述

//当前文件头
struct dmsHeader
{
    char _eyeCatcher[DMS_HEADER_EYECATCHER_LEN];
    unsigned int _size; //数据页数量
    unsigned int _flag;
    unsigned int _version;
};

页面头设计
我们自定义的数据页结构:
在这里插入图片描述

struct dmsPageHeader
{
    char _eyeCatcher[DMS_PAGE_EYECATCHER_LEN];
    unsigned int _size; //页大小
    unsigned int _flag; //页状态
    unsigned int _numSlots; //slot个数
    unsigned int _slotOffset; //slot开始偏移
    unsigned int _freeSpace;    //空闲空间的大小
    unsigned int _freeOffset;   //空闲空间的偏移
    char _data[0];
};

现在我们插入记录A进行模拟:

在这里插入图片描述

观察各个项的改变:
在这里插入图片描述

mmap内存映射

我们在收到一个对于某数据记录的操作时,首先要将保存该记录的文件通过mmap映射到内存
注意mmap一次只能将文件的一部分映射到内存,当我们需要用到文件的其他部分时,需要重新映射一次。我们将每一次映射的文件内容称为segment段
所以一个_ossMmapFile的实例会有一个vector< Segment >。
而我们之后的data model也是以文件作为单位存储的,因此是该类的子类

class _ossMmapFile
{
protected:
    //一个文件可能需要映射到内存多次,因为当文件大小改变时
    //不能直接修改映射到内存的大小,需要重新映射一次
    class _ossMmapSegment
    {
    public:
        //文件段指向内存的指针
        void *_ptr;
        //文件需要映射的长度
        unsigned int _length;
        //文件所在位置的偏移
        unsigned long long _offset;
        _ossMmapSegment(void *ptr,unsigned int length,unsigned long long offset)
        {
            _ptr=ptr;
            _length=length;
            _offset=offset;
        }
    };
    typedef _ossMmapSegment ossMmapSegment;
    
    ossPrimitiveFileOp _fileOp;
    ossXLatch _mutex;
    bool _opened;   //文件是否开着
    std::vector<ossMmapSegment> _segments;
    char _fileName[OSS_MAX_PATHSIZE];

public:
    //定义一个映射段vector的迭代器
    typedef std::vector<ossMmapSegment>::const_iterator CONST_ITR;

    inline CONST_ITR begin()
    {
        return _segments.begin();
    }

    inline CONST_ITR end()
    {
        return _segments.end();
    }

    inline unsigned int segmentSize()
    {  
        return _segments.size();
    }
public:
    _ossMmapFile()
    {
        _opened=false;
        memset(_fileName,0,sizeof(_fileName));
    }
    ~_ossMmapFile()
    {
        close();
    }
    int open(const char *pFilename,unsigned int options);
    void close();
    int map(unsigned long long offset,unsigned int length,void **pAddress);
};
typedef class _ossMmapFile ossMmapFile;

data model类核心函数实现

在这里插入图片描述
扩展文件

注意我们每次扩展文件都需要是64K的倍数
在这之前我们定义了文件的头大小为16字节,但是为了之后的扩充,我们将头实际大小设置为64K。
因此每次扩展文件的大小时,我们都需要是64K的倍数。
实际情况扩展文件可能会发生的几种情况:初始化文件需要扩展文件,大小为文件头64K;需要扩展一个新的数据段,大小为128M。
(PS:这里的扩展是磁盘中文件的扩展。)

//先将文件分配的区域置空
int dmsFile::_extendFile(int size)
{
    int rc=EDB_OK;
    char temp[DMS_EXTEND_SIZE]={0}; //先定义64K的文件头大小
    memset(temp,0,DMS_EXTEND_SIZE);

    //如果size不是64K的倍数,那么是无效的输入
    if ( size % DMS_EXTEND_SIZE != 0 )
    {
      rc = EDB_SYS ;
      PD_LOG ( PDERROR, "Invalid extend size, must be multiple of %d",
               DMS_EXTEND_SIZE ) ;
      goto error ;
    }

    //将分配的空间置空,每次置空64K
    for ( int i = 0; i < size; i += DMS_EXTEND_SIZE )
    {
      _fileOp.seekToEnd () ;
      rc = _fileOp.Write ( temp, DMS_EXTEND_SIZE ) ;
      PD_RC_CHECK ( rc, PDERROR, "Failed to write to file, rc = %d", rc ) ;
    }
done :
   return rc ;
error :
   goto done ;
}

初始化空文件

初始化过程会包含:扩展文件头部,将文件头部映射到内存,初始化头部信息。

//initialize发现size为0时进行
int dmsFile::_initNew()
{
    int rc=EDB_OK;
    rc=_extendFile(DMS_FILE_HEADER_SIZE);       //先扩展文件头部
    PD_RC_CHECK ( rc, PDERROR, "Failed to extend file, rc = %d", rc ) ;
    rc = map ( 0, DMS_FILE_HEADER_SIZE, ( void **)&_header ) ;  //头部映射到内存
    PD_RC_CHECK ( rc, PDERROR, "Failed to map, rc = %d", rc ) ;

    //初始头部信息
    strcpy ( _header->_eyeCatcher, DMS_HEADER_EYECATCHER ) ;
   _header->_size = 0 ;
   _header->_flag = DMS_HEADER_FLAG_NORMAL ;
   _header->_version = DMS_HEADER_VERSION_CURRENT ;

done :
   return rc ;
error :
   goto done ;
}

//根据需要的空间大小,在空闲空间的map中找到最合适的PAGE
PAGEID dmsFile::_findPage(size_t requiredSize)
{
    std::multimap<unsigned int,PAGEID>::iterator findIter;
    findIter=_freeSpaceMap.upper_bound(requiredSize);   //找到Map中比requiredSize刚好大一点的迭代器

    if ( findIter != _freeSpaceMap.end() )
    {
      return findIter->second ;
    }
    return DMS_INVALID_PAGEID ;
}

增加数据段

增加数据段包含:扩展128M文件大小,映射到内存中,将段中每个页加上页头,将每个页映射到freeSpaceMap,最后将这个segment放入_body中。

int dmsFile::_extendSegment()
{
    int rc=EDB_OK;
    char *data=NULL;
    int freeMapSize=0;
    dmsPageHeader pageHeader;
    offsetType offset=0;

    //让我们先获得文件已经扩张的大小
    rc=_fileOp.getSize(&offset);
    PD_RC_CHECK ( rc, PDERROR, "Failed to get file size, rc = %d", rc ) ;

    rc=_extendFile(DMS_FILE_SEGMENT_SIZE);//增加128M
    PD_RC_CHECK ( rc, PDERROR, "Failed to extend segment rc = %d", rc ) ;

    //从原来的end到新的end这一段映射过去
    //调用mmap进行内存映射,data为内存映射的指针
    rc=map(offset,DMS_FILE_SEGMENT_SIZE,(void**)&data);
    PD_RC_CHECK ( rc, PDERROR, "Failed to map file, rc = %d", rc ) ;

    //接下来我们要将段中每一个页初始化
    //自然我们先把页的头给定义好
    strcpy(pageHeader._eyeCatcher,DMS_PAGE_EYECATCHER);
    pageHeader._size = DMS_PAGESIZE ;
    pageHeader._flag = DMS_PAGE_FLAG_NORMAL ;
    pageHeader._numSlots = 0 ;
    pageHeader._slotOffset = sizeof ( dmsPageHeader ) ;
    pageHeader._freeSpace = DMS_PAGESIZE - sizeof(dmsPageHeader) ;
    pageHeader._freeOffset = DMS_PAGESIZE ;

    //把页头给每一页补上
    for(int i=0;i<DMS_FILE_SEGMENT_SIZE;i+=DMS_PAGESIZE)//一共128M,每次增加4M
    {
        memcpy(data+i,(char*)&pageHeader,sizeof(dmsPageHeader));
    }

    //页的空闲空间和PAGEID的映射更新
    _mutex.get();
    freeMapSize=_freeSpaceMap.size();//为了获取映射PAGEID现在到哪了
    for(int i=0;i<DMS_PAGES_PER_SEGMENT;++i)
    {
        _freeSpaceMap.insert(pair<unsigned int,PAGEID>(pageHeader._freeSpace,i+freeMapSize));
    }

    //将新的segment放到Body中
    _body.push_back(data);
    _header->_size+=DMS_PAGES_PER_SEGMENT;
    _mutex.release();
done:
    return rc;
error:
    goto done;
}

装载数据

发生在初始化文件之后,文件头已经被映射到内存。
文件已经存在,将文件的各个segment映射到内存,随后更新_body,_freeSpaceMap,映射表即可。

//装载数据,将文件头,各个sements映射到内存,并将_body,_freeSpaceMap,index更新
//注意文件头,页头等内容已经存在了
int dmsFile::_loadData()
{
    int rc=EDB_OK;
    int numPage=0;
    int numSegments=0;
    dmsPageHeader *pageHeader=NULL;
    char *data=NULL;
    BSONObj bson;
    SLOTID slotID=0;
    SLOTOFF slotOffset=0;
    dmsRecordID recordID;

    //检查文件头是否存在
    if(!_header)
    {
        rc = map ( 0, DMS_FILE_HEADER_SIZE, ( void **)&_header ) ;
        PD_RC_CHECK ( rc, PDERROR, "Failed to map file header, rc = %d", rc ) ;
    }

    numPage=_header->_size;
    if ( numPage % DMS_PAGES_PER_SEGMENT )//检查页数是否为segment倍数
    {
         rc = EDB_SYS ;
         PD_LOG ( PDERROR, "Failed to load data, partial segments detected" ) ;
         goto error ;
    }

    numSegments=numPage/DMS_PAGES_PER_SEGMENT;  //获得段数
    if ( numSegments > 0 )
    {
      for ( int i = 0; i < numSegments; ++i )   //
      {
         // 将已经存在的段映射到内存中
         rc = map ( DMS_FILE_HEADER_SIZE + DMS_FILE_SEGMENT_SIZE * i,
                    DMS_FILE_SEGMENT_SIZE,
                    (void **)&data ) ;
         PD_RC_CHECK ( rc, PDERROR, "Failed to map segment %d, rc = %d",
                       i, rc ) ;
         _body.push_back ( data ) ;//更新body
         // 将每页映射到freeSpaceMap
         for ( unsigned int k = 0; k < DMS_PAGES_PER_SEGMENT; ++k )
         {//获得每页的头
            pageHeader = ( dmsPageHeader * ) ( data + k * DMS_PAGESIZE ) ;
            _freeSpaceMap.insert (
                  pair<unsigned int, PAGEID>(pageHeader->_freeSpace, k ) ) ;
            slotID = ( SLOTID ) pageHeader->_numSlots ;
            recordID._pageID = (PAGEID) k ;
            /*
            for ( unsigned int s = 0; s < slotID; ++s )
            {
               slotOffset = *(SLOTOFF*)(data+k*DMS_PAGESIZE +
                            sizeof(dmsPageHeader ) + s*sizeof(SLOTID) ) ;
               if ( DMS_SLOT_EMPTY == slotOffset )
               {
                  continue ;
               }
               bson = BSONObj ( data + k*DMS_PAGESIZE +
                                slotOffset + sizeof(dmsRecord) ) ;
               recordID._slotID = (SLOTID)s ;
               rc = _ixmBucketMgr->isIDExist ( bson ) ;
               PD_RC_CHECK ( rc, PDERROR, "Failed to call isIDExist, rc = %d", rc ) ;
               rc = _ixmBucketMgr->createIndex ( bson, recordID ) ;
               PD_RC_CHECK ( rc, PDERROR, "Failed to call ixm createIndex, rc = %d", rc ) ;
            }
            */ //更新index
         }
      } // for ( int i = 0; i < numSegments; ++i )
   } // if ( numSegments > 0 )
done :
   return rc ;
error :
   goto done ;

}

查找合适大小的数据页

根据给定的大小去freeSpaceMap找到适合的页,返回页ID。

//根据需要的空间大小,在空闲空间的map中找到最合适的PAGE
PAGEID dmsFile::_findPage(size_t requiredSize)
{
    std::multimap<unsigned int,PAGEID>::iterator findIter;
    findIter=_freeSpaceMap.upper_bound(requiredSize);   //找到Map中比requiredSize刚好大一点的迭代器

    if ( findIter != _freeSpaceMap.end() )
    {
      return findIter->second ;
    }
    return DMS_INVALID_PAGEID ;
}

查找指定页的slot

根据给定的page地址,以及记录id,找到对应的slot偏移。

//根据给定的page地址和记录ID(pageid slotid)找到slot的偏移
int dmsFile::_searchSlot(char *page,dmsRecordID &rid,SLOTOFF &slot)
{
   int rc = EDB_OK ;
   dmsPageHeader *pageHeader = NULL ;
   //检查page地址
   if ( !page )
   {
      rc = EDB_SYS ;
      PD_LOG ( PDERROR, "page is NULL" ) ;
      goto 
      
      error ;
   }

    //检查RID是否合法
   if ( 0 > rid._pageID || 0 > rid._slotID )
   {
      rc = EDB_SYS ;
      PD_LOG ( PDERROR, "Invalid RID: %d.%d",
               rid._pageID, rid._slotID ) ;
      goto error ;
   }

   pageHeader = (dmsPageHeader *)page ; //得到page地址
   //检查slotID是否合法
   if ( rid._slotID > pageHeader->_numSlots )
   {
      rc = EDB_SYS ;
      PD_LOG ( PDERROR, "Slot is out of range, provided: %d, max: %d",
               rid._slotID, pageHeader->_numSlots ) ;
      goto error ;
   }

    //slot的偏移为 当前页地址+页头+slotID*slot大小
   slot=*(SLOTOFF*)(page+sizeof(dmsPageHeader)+rid._slotID*sizeof(SLOTOFF));
done :
   return rc ;
error :
   goto done ;
}

更新剩余空间

给定一个大小,通过freeSpaceMap找到大小合适的page,将其去除并更新映射。

//将changeSize指定大小的页更新到freespace里(仅仅是映射更新)
void dmsFile::_updateFreeSpace(dmsPageHeader *header,
                               int changeSize,PAGEID pageID)
{   //首先根据给定的header获取需要page的freespace
    unsigned int freeSpace=header->_freeSpace;
    //pair保存了_freespacemap类型的迭代器
    std::pair<std::multimap<unsigned int,PAGEID>::iterator,
              std::multimap<unsigned int,PAGEID>::iterator> ret;
    //根据freeSpace的大小,在_freeSpaceMap中寻找符合key值的迭代器范围
    ret=_freeSpaceMap.equal_range(freeSpace);

    //起点为pair中PAGEID最小的迭代器first,终点为PAGEID最大的迭代器second
    for(std::multimap<unsigned int,PAGEID>::iterator it=ret.first;
        it!=ret.second;++it)
    {
        if(it->second==pageID)  //如果PAGEID和形参给定一致
        {   //我们先将这个映射抹去
            _freeSpaceMap.erase(it);
            break;
        }
    }

    freeSpace+=changeSize;
    header->_freeSpace=freeSpace;
    //将修改过的freespace重新加入映射
    _freeSpaceMap.insert(pair<unsigned int,PAGEID>(freeSpace,pageID));

}

回收某页的无用空间

正如之前提到如果在一个页中进行过多次delete导致slot为-1的记录很多,我们应该进行重组,回收这些空间。

//根据每个slot标记,对一个页里的无用记录进行整理
void dmsFile::_recoverSpace(char *page)
{
   char *pLeft               = NULL ;
   char *pRight              = NULL ;
   SLOTOFF slot              = 0 ;
   int recordSize            = 0 ;
   bool isRecover            = false ;  //是否需要重组
   dmsRecord *recordHeader   = NULL ;
   dmsPageHeader *pageHeader = NULL ;

   //pLeft先到该页的头之后,也就是第一个slot的位置
   pLeft=page+sizeof(dmsPageHeader);
   //pRight直接到该页的尾部
   pRight=page+DMS_PAGESIZE;
   pageHeader=(dmsPageHeader*)page;//获得该页的头位置

   for ( unsigned int i = 0; i < pageHeader->_numSlots; ++i )
   {    //获得该slot对应的记录偏移
      slot = *((SLOTOFF*)(pLeft + sizeof(SLOTOFF) * i ) ) ;
      if ( DMS_SLOT_EMPTY != slot ) //slot标记不为-1,数据有用
      {
         recordHeader = (dmsRecord *)(page + slot ) ;
         recordSize = recordHeader->_size ;//获取记录长度
         pRight -= recordSize ;
         if ( isRecover )   //注意如果没有过标记为-1的slot,是不需要进行move的,只需要移动pRight指针
         {//只要出现了标记为-1的slot,后续所有的有效记录都要开始move
            memmove ( pRight, page + slot, recordSize ) ;
            //更新slot对应的记录偏移
            *((SLOTOFF*)(pLeft + sizeof(SLOTOFF) * i ) ) = (SLOTOFF)(pRight-page) ;
         }
      }
      else//slot标记为-1,该记录需要使得该页进行重组,后续都要开始move
      {
         isRecover = true ;
      }
   }
   //将页头的空闲偏移更新
   pageHeader->_freeOffset = pRight - page ;


}

待实现的对外操作

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值