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

来源:互联网 发布:智慧旅游大数据平台 编辑:程序博客网 时间:2024/06/16 14:33

一、数据持久化

       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设为-1struct 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组成。


0 0