码迷,mamicode.com
首页 > 数据库 > 详细

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

时间:2016-07-10 19:16:19      阅读:257      评论:0      收藏:0      [点我收藏+]

标签:

一、数据持久化

       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组成。


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

标签:

原文地址:http://blog.csdn.net/qq_15457239/article/details/51732661

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!