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类的设计
对比传统数据库的文件,我们进行结构的简化:
- 保留文件头,去掉SME(空间管理段)。
- 由于是类Mongo的nosql存储,我们去掉表的概念,把一个个页存到文件里。(相当于一个表)
- 暂时不用B树类索引,通过stl库的map函数做简易的映射索引。
- 文件作为数据模型的基本单位,页作为存储的基本单位设置为4M。我们设置页的最大数量为256K,因此存储的量最多可以到4M*256K=1T。
- 由于之前阐述过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 ;
}