您的位置:首页 > 数据库

diy数据库(九)--diydb的数据持久化和存储格式

2016-07-08 15:30 344 查看
一、数据持久化

       diydb是一个实际上是文档型数据库(并不是内存型数据库),他需要将数据持久化,那么就需要 读写磁盘上的数据。怎样读写磁盘上的数据更高效呢?目前linux上的方法就是使用mmap,即内存映射机制。

        为什么说mmap高效呢?我们知道,当我们在进程中读文件时,一般都是先将磁盘上的文件的相应数据块复制到进程的内核空间,然后从内核空间将需要的数据复制到用户空间。你会发现,数据经过了内核空间的转存,对于应用程序来说,这个过程是没必要的,也是很消耗资源的。mmap正是省略了数据在内核的转存,他使得磁盘上的数据直接映射到进程的虚拟内存空间,而且是虚拟内存空间中的用户空间,当我们读取由mmap映射过的磁盘数据时,相应的数据块会直接复制到进程的用户空间,这样一来就不用经过内核空间的转存了。下面是mmap映射后的进程地址空间分布图:



         下面,我们就来通过源码分析一下diydb中管理内存映射的类。

#ifndef OSSMMAPFILE_HPP_
#define OSSMMAPFILE_HPP_

#include "core.hpp"
#include "ossLatch.hpp"
#include "ossPrimitiveFileOp.hpp"

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 :
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 () ;//回收所有映射的内存空间,遍历_segments,对每个内存段进行反映射
}

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 ;

#endif

        这里可以看到,我们的映射文件类_ossMmapFile实际上是管理一个文件的内存映射,因为有的数据库文件可能非常大,如果要把数据库文件直接映射到一个连续的虚拟地址空间,很可能会映射失败,所以_ossMmapFile是把文件映射到多个内存段,每个内存段对应的是一个_ossMmapSegment类型对象。所有映射到了内存的数据段放在一个集合中(std::vector<ossMmapSegment> _segments)。上面是ossMmapFile.hpp的代码,ossMmapFile.cpp这里不细说,最后会将带注释的代码po出来。

二、数据的存储

       上面说了diydb数据持久化时,是通过内存映射的方式去高效读写磁盘文件的,那么数据库数据是以什么格式存放在磁盘上面,我们又是怎样去操作这些格式化的数据的呢?

1、数据库文件结构总览



头:存放数据库文件的元数据。包括字符串标识(相当于一个魔数,用来表示这是一个diydb的数据库文件)、数据页数量、数据           库状态、版本信息。

数据页:我们的数据库文件时分成一个一个大小一致的数据页的,而空闲空间的管理都放到了数据页内部。另外,由于每条数据不                能跨数据页,这里每个数据页的大小为4M,所以diydb中一条数据的大小不能超过4M。

数据段:数据段由多个数据页构成,表示数据库文件中由mmap映射到虚拟内存中的连续的一段数据。所以数据段是只存在于内存                中的一个单位,数据库文件中没有数据段。

注:diydb的数据库文件比较简化,她只包含一个数据库,而且索引没有持久化存储。

2、数据页的结结构



长度:数据页的长度,以便以后扩展数据页的长度。

标识:标识数据页的状态,比如:是否可用

槽数量:该数据页中包含的槽的数量。

最后一个槽所在的偏移:数据页的数据部分的前面放的是槽,后面部分放的是数据块(两者中间就是空闲空间)。这个属性就是该                                             数据页中最后一个槽的偏移。

空闲空间大小:表示该数据页中没有使用的空间,即槽区域和数据区域之间的部分。

空闲空间起始地址偏移:在数据页中数据块是从后往前分配空间的,所以存放数据块的区域的位于数据页的尾部区域,本属性就表                                            示该尾部区域的起始位置的地址偏移。

注:数据页的大小固定为4M,槽的大小为4B

3、数据记录的结构



数据记录长度:一个数据记录的整体长度。

数据记录标识:表示该数据记录是否可用(即是否被删除了)。

数据:存放真正的一条数据(这里是一个BSON对象)。

4、对外操作

(1)数据插入( insert)

(2)数据删除( remove)

(3)数据查找( find)

(4)初始化( initialize)

5、内部操作

(1)增加数据段( _extendSegment)

           1、扩展文件 2、把扩展的文件映射到内存里面

  (2)初始化空文件( _initNew)

           当没有数据文件的时候,创建新的数据库文件,扩展文件,填入数据库文件的头信息,然后把文件映射到内存里

(3)扩展文件( _extendFile)    

           把文件延长128M,即将磁盘上的文件扩展128M(一个段的长度)

(4)装载数据( _loadData)

         启动一个数据库时,如果已经有一个数据库文件,则需要把这个数据库文件装载进去。1、将数据库文件的头装载进去 2、将数据库中的每个段映射到内存中 3、计算每个数据页中的空闲空间,把结果保存到一个std::map里面,这个map对象就是空闲空间管理容器

(5)搜索槽( _searchSlot)

         给定一个数据页,给定一个RID,这个函数算出这个槽的偏移是多少

(6)回收空间( _recoeverSpace)

         即页内重组

(7)更新剩余空间( _updateFreeSpace)

         将页内插入数据后,页的空闲空间就少了,这样就得更新空闲空间管理容器

(8)查找数据页( _findPage)

          给定一个数据的长度,通过这个这个方法去找到一个有合适空闲空间的页

6、带着上面的介绍,我们来看看代码实现

(1)每条数据记录的id由页id和槽id组成,即每次找一条记录时,我们先找记录所在的页,然后找记录所在的槽,然后根据槽去找数据记录

typedef unsigned int PAGEID ;//页号
typedef unsigned int SLOTID ;//槽号
//每个记录id由页id和槽id组成
struct dmsRecordID
{
PAGEID _pageID ;
SLOTID _slotID ;
} ;

(2)每条记录的结构

struct dmsRecord//数据记录
{
unsigned int _size ;
unsigned int _flag ;
char         _data[0] ;
} ;


(3)数据库文件的头

//数据库文件的首部
struct dmsHeader
{
char         _eyeCatcher[DMS_HEADER_EYECATCHER_LEN] ;//数据库文件的魔数
unsigned int _size ;
unsigned int _flag ;
unsigned int _version ;
} ;


(4)数据页的结构

// page structure
/*********************************************************
PAGE STRUCTURE
-------------------------
| PAGE HEADER           |
-------------------------
| Slot List             |
-------------------------
| Free Space            |
-------------------------
| Data                  |
-------------------------
**********************************************************/
#define DMS_PAGE_EYECATCHER "PAGH"//数据页的魔数
#define DMS_PAGE_EYECATCHER_LEN 4
#define DMS_PAGE_FLAG_NORMAL    0
#define DMS_PAGE_FLAG_UNALLOC   1
#define DMS_SLOT_EMPTY 0xFFFFFFFF//当slot对应的数据记录被删除时,要将该slot设为-1

struct dmsPageHeader
{
char             _eyeCatcher[DMS_PAGE_EYECATCHER_LEN] ;
unsigned int     _size ;
unsigned int     _flag ;
unsigned int     _numSlots ;
unsigned int     _slotOffset ;
unsigned int     _freeSpace ;
unsigned int     _freeOffset ;
char             _data[0] ;
} ;


(5)数据库文件中各个单位的大小

#define DMS_PAGESIZE   4194304//linux中一个数据块的大小为4096,diy数据库一个page的大小设置为4M
#define DMS_MAX_PAGES  262144//数据库文件最大256K个数据页,所以数据库文件最大为1T
#define DMS_FILE_SEGMENT_SIZE 134217728//段长128M
#define DMS_FILE_HEADER_SIZE  65536//数据库文件头部的长度
#define DMS_EXTEND_SIZE 65536//扩展磁盘时一次扩展的大小,实际上就是一个段的长度

(7)DMS数据管理模块的实现类的声明如下

class dmsFile : public ossMmapFile
{
private :
dmsHeader            *_header ;//数据库文件的头
std::vector<char *>   _body ;//每个SEGMENT在虚拟内存中的起始位置
std::multimap<unsigned int, PAGEID> _freeSpaceMap ;//管理空闲空间,每次要插入记录时,根据记录大小来
ossSLatch             _mutex ;//读写锁
ossXLatch             _extendMutex ;//扩展数据库文件时的互斥锁,防止同时有两个线程扩展这个文件
char                 *_pFileName ;//文件名
ixmBucketManager     *_ixmBucketMgr ;//数据索引
public :
dmsFile ( ixmBucketManager *ixmBucketMgr ) ;
~dmsFile () ;
// 初始化  dms 文件
int initialize ( const char *pFileName ) ;
// 插入数据,将record插入到rid指定的槽对应的数据记录中,并且用outRecord返回record在插入后在内存映射中的位置
int insert ( bson::BSONObj &record, bson::BSONObj &outRecord, dmsRecordID &rid ) ;
//给定一个记录id,删除对应的记录
int remove ( dmsRecordID &rid ) ;
//根据记录id查找对应的记录
int find ( dmsRecordID &rid, bson::BSONObj &result ) ;
private :
int _extendSegment () ;//为数据库文件扩展一个段
int _initNew () ;//初始化一个空的数据库文件,只创造一个数据库文件头
int _extendFile ( int size ) ;//扩展文件,扩展指定的大小

int _loadData () ;//装载数据库文件
// search slot
int _searchSlot ( char *page,//给定一个数据页
dmsRecordID &recordID,
SLOTOFF &slot ) ;//搜索槽
void _recoverSpace ( char *page ) ;//重组
void _updateFreeSpace ( dmsPageHeader *header, int changeSize,
PAGEID pageID ) ;//更新空闲空间
PAGEID _findPage ( size_t requiredSize ) ;//在空闲空间列表中找满足<span style="font-family: Arial, Helvetica, sans-serif; font-size: 12px;">requiredSize大小的页</span>
}
注:根据上面的描述,我们可以发现,数据库的元数据有:数据库文件头中的信息(主要是数据库文件的大小)、数据库文件映射到内存中的每个段的起始位置、数据库空闲空间列表、数据库文件名、数据库的索引

(6)数据插入( insert)实现

int dmsFile::insert ( BSONObj &record, BSONObj &outRecord, dmsRecordID &rid )
{
int rc                     = DIY_OK ;
PAGEID pageID              = 0 ;
char *page                 = NULL ;
dmsPageHeader *pageHeader  = NULL ;
int recordSize             = 0 ;
SLOTOFF offsetTemp         = 0 ;
const char *pGKeyFieldName = NULL ;
dmsRecord recordHeader ;

recordSize                 = record.objsize() ;//记录的大小
if ( (unsigned int)recordSize > DMS_MAX_RECORD )//每一条记录最大4m减去页的头部
{
rc = DIY_INVALIDARG ;
PD_LOG ( PDERROR, "record cannot bigger than 4MB" ) ;
goto error ;
}
pGKeyFieldName = gKeyFieldName ;

//检测是否有_id字段
if ( record.getFieldDottedOrArray ( pGKeyFieldName ).eoo () )
{
rc = DIY_INVALIDARG ;
PD_LOG ( PDERROR, "record must be with _id" ) ;
goto error ;
}

retry :
// 对全局锁加锁
_mutex.get() ;
pageID = _findPage ( recordSize + sizeof(dmsRecord) ) ;//找足够的空间
// if there's not enough space in any existing pages, let's release db lock
if ( DMS_INVALID_PAGEID == pageID )
{
_mutex.release () ;//如果找不到合适大小的数据页就释放锁
// if there's not enough space in any existing pages, let's release db lock and
// try to allocate a new segment by calling _extendSegment
if ( _extendMutex.try_get() )//扩展锁,即增加数据段
{
// 同时只有一个线程可以扩展数据段,扩展时,先扩展数据库文件,然后将扩展的段映射到内存中
// 接着初始化每个数据页的元数据,然后初始化数据库的元数据,包括更改空闲空间列表,将映射
// 进内存的段的起始位置列表
rc = _extendSegment () ;
if ( rc )
{
PD_LOG ( PDERROR, "Failed to extend segment, rc = %d", rc ) ;
_extendMutex.release () ;
goto error ;
}
}
else
{
// if we cannot get the extendmutex, that means someone else is trying to extend
// so let's wait until getting the mutex, and release it and try again
_extendMutex.get() ;
}
_extendMutex.release () ;
goto retry ;//然后继续找拥有足够空间的页
}
// 同过pageID找到该页在映射在内存中的位置
page = pageToOffset ( pageID ) ;
// 如果找不到对应页在内存中的位置,释放扩展锁,并返回error
if ( !page )
{
rc = DIY_SYS ;
PD_LOG ( PDERROR, "Failed to find the page" ) ;
goto error_releasemutex ;
}
// 读取页的元数据
pageHeader = (dmsPageHeader *)page ;
// 检测页的标识字段有没有问题
if ( memcmp ( pageHeader->_eyeCatcher, DMS_PAGE_EYECATCHER,
DMS_PAGE_EYECATCHER_LEN ) != 0 )//检测是不是数据库的页
{
rc = DIY_SYS ;
PD_LOG ( PDERROR, "Invalid page header" ) ;
goto error_releasemutex ;
}
// 我们找到的页只是说空闲空间总和够插入一条数据,但是页中的空间并不一定是连续的,
// 所以,要看有没有连续的空间够插入一条数据,如果没有,这要进行业内重组,即把页
// 内多块空闲空间调整成一块连续的空闲空间
if ( pageHeader->_slotOffset + recordSize + sizeof(dmsRecord) + sizeof(SLOTID) >
pageHeader->_freeOffset )//看有没有足够的空间和足够的连续空间
{
_recoverSpace ( page ) ;//页内重组
}

offsetTemp = pageHeader->_freeOffset - recordSize - sizeof(dmsRecord) ;
recordHeader._size = recordSize + sizeof( dmsRecord ) ;
recordHeader._flag = DMS_RECORD_FLAG_NORMAL ;
// 填写给带插入记录分配的槽
*(SLOTOFF*)( page + sizeof( dmsPageHeader ) +
pageHeader->_numSlots * sizeof(SLOTOFF) ) = offsetTemp ;
// 填写记录的头部信息
memcpy ( page + offsetTemp, ( char* )&recordHeader, sizeof(dmsRecord) ) ;
// 填写记录体
memcpy ( page + offsetTemp + sizeof(dmsRecord),
record.objdata(),
recordSize ) ;
outRecord = BSONObj ( page + offsetTemp + sizeof(dmsRecord) ) ;
rid._pageID = pageID ;
rid._slotID = pageHeader->_numSlots ;
// 更改数据页的元数据信息
pageHeader->_numSlots ++ ;
pageHeader->_slotOffset += sizeof(SLOTID) ;
pageHeader->_freeOffset = offsetTemp ;
// 更改数据库的元数据信息(即空闲空间列表)
_updateFreeSpace ( pageHeader,
-(recordSize+sizeof(SLOTID)+sizeof(dmsRecord)),
pageID ) ;
// 释放全局锁
_mutex.release () ;
done :
return rc ;
error_releasemutex :
_mutex.release() ;
error :
goto done ;
}
注:这里我们可以看出,当一个线程在对数据库操作时,加的是数据库的全局锁,这个锁的粒度是相当大的,非常不建议这么做,这正是本数据库不能商用的原因之一。

        另外,我们可以看到,数据的插入操作包括:

• 判定输入数据的合法性
• 锁数据库
• 找到拥有足够空间的数据页
• 如果无法找到拥有足够空间的数据页,释放锁,分
配新的数据段, 得到锁, 然后重新查找
• 如果找到的空闲页不包括足够的连续大小内存页,
则进行数据页重组
• 将记录写入数据页
• 更新数据页元数据信息
• 更新空闲空间信息
• 解锁

三、总结

1、本章主要讲解了diydb的DMS模块,即数据的管理模块,runtime模块在执行请求时,正是基于这个模块对数据库中的数据进行操作的(当然还要根据索引模块去查找记录)

2、diydb中的数据是存在磁盘上面的,传统读磁盘上的数据会导致对数据的两次复制,非常耗时,所以diydb采用了将数据库文件中的数据映射进用户内存空间(虚拟),来提高磁盘读写的效率。而且为了防止用户内存空间没有足够的连续地址,所以每次将一个数据段映射到用户内存空间中

3、diydb的数据管理模块在操作数据时,会锁住整个数据库,所以效率不敢恭维

4、diydb的数据库文件中的数据存储格式是比较机智的,他为每个数据记录分配了一个指向该记录的槽,因为数据记录的长度是变化的,而槽的大小是一定的,所以对槽的查找更方便。正因为这样,每个记录id由页id和槽id组成。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: