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

一、数据持久化

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

时间: 2024-10-17 20:48:22

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

安卓数据持久化:文件存储、SharedPreferences存储以及数据库存储

Android系统中主要提供了三种方式用于简单的实现数据持久化功能: 文件存储(手机自带的内存).SharedPreferences存储以及数据库存储 当然还可以用sd卡存储 读入写出 下面是疯狂java讲义中的关于IO流的一些补充,回忆一下 1,文件存储 手机自带的内存,只能供当前应用程序访问,其他应用程序访问不了,程序卸载这些数据也会随着消失 原理: 基本是先获取一个文件的输出流,然后把信息write进去,最后关闭流 a,通过上下文类context的openFileOutput()方法获得一

iOS 中级数据持久化——简单的数据库(Sqlite3)

sqlite是嵌入式的和轻量级的sql数据库.sqlite是由c实现的.广泛用于包括浏览器(支持html5的大部分浏览器,ie除外).ios.android以及一些便携需求的小型web应用系统. 数据库无非就是增,删,改,查四种.除了查询以为,其他的三种方法比较类似 //使用数据库之前,打开数据库 - (void)openDB { if (db != nil) { return; } //数据库存储在沙河中的caches文件夹下 NSString * cachesPath = [NSSearch

ios开发中的4种数据持久化方式【二、数据库 SQLite3、Core Data 的运用】

               在上文,我们介绍了ios开发中的其中2种数据持久化方式:属性列表.归档解档.本节将继续介绍另外2种iOS持久化数据的方法:数据库 SQLite3.Core Data 的运用: 在本节,将通过对4个文本框内容的创建.修改,退出后台,再重新回到后台,来认识这两种持久化数据的方式.效果图如下[图1]: [图1 GUI界面效果图] [本次开发环境: Xcode:7.2     iOS Simulator:iphone6S plus   By:啊左]     一.数据库SQL

ios开发学习笔记--数据持久化之数据库(SQLite.swift)

数据持久化之SQLite数据库(SQLite.swift使用) 一.     简介 SQLite是一款轻型的嵌入式数据库,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就足够了.它的处理速度比Mysql.PostgreSQL这两款著名的数据库都还快.SQLite提供的是一些C函数接口,你可以用这些函数操作数据库.通过使用这些接口,传递一些标准 sql 语句(以 char * 类型)给 SQLite函数,SQLite就会为你操作数据库 一个数据库中的表就算是一个文件,一般是将这个文件放

diy数据库(七)--线程控制块、消息、线程池

一.概述 1.diy数据库使用的是一个多进程and多线程的服务器模型.每个进程作为一个节点实例,监听一个端口:而每个用户连接在数据库节点实例中都会有一个代理线程与之对应. 2.除了主线程外每个线程都有一个EDU(进程调度单元,也可称为线程控制块),另外每种系统线程类型有且只有一个线程实体(这里的系统线程只有一种,即监听线程,主线程不在线程管理池里面) 3.代理线程是专门处理用户请求的 ,由监听线程创建 4.线程池对进行调度,通过传入不同的类型, 内部调用不同的函数执行相应请求 5.EDU回池后可

iOS数据持久化-OC

沙盒详解 1.IOS沙盒机制 IOS应用程序只能在为该改程序创建的文件系统中读取文件,不可以去其它地方访问,此区域被成为沙盒,所以所有的非代码文件都要保存在此,例如图像,图标,声音,映像,属性列表,文本文件等. 1.1.每个应用程序都有自己的存储空间 1.2.应用程序不能翻过自己的围墙去访问别的存储空间的内容 1.3.应用程序请求的数据都要通过权限检测,假如不符合条件的话,不会被放行. 通过这张图只能从表层上理解sandbox是一种安全体系,应用程序的所有操作都要通过这个体系来执行,其中核心内容

UI_19 数据持久化(本地存储)

一.数据持久化概述 数据持久化就是数据的永久存储.其本质是将数据保存为文件,存到程序的沙盒中. 1.数据持久化的方式 1.1 writeToFile:简单对象写入文件 1.2 NSUserDefaults:应用程序偏好设置1.3 Sqlite:轻量级关系型数据库,不能直接存储对象(NSData除外),需要用到一些SQL语句,先将复杂对象归档(对象->NSData) 1.4 CoreData:对象型数据库,实质是将数据库的内部存储细节封装 1.5 Plist文件 2.应用程序沙盒 每一应用程序都有

iOS中几种数据持久化方案

概论 所谓的持久化,就是将数据保存到硬盘中,使得在应用程序或机器重启后可以继续访问之前保存的数据.在iOS开发中,有很多数据持久化的方案,接下来我将尝试着介绍一下5种方案: plist文件(属性列表) preference(偏好设置) NSKeyedArchiver(归档) SQLite 3 CoreData 沙盒 在介绍各种存储方法之前,有必要说明以下沙盒机制.iOS程序默认情况下只能访问程序自己的目录,这个目录被称为"沙盒". 1.结构 既然沙盒就是一个文件夹,那就看看里面有什么吧

深入理解iPhone数据持久化(手把手教你iphone开发 – 基础篇)

在所有的移动开发平台数据持久化都是很重要的部分:在j2me中是rms或保存在应用程序的目录中,在symbian中可以保存在相应的磁盘目录中和数据库中.symbian中因为权限认证的原因,在3rd上大多数只能访问应用程序的private目录或其它系统共享目录.在iphone中,apple博采众长,提供了多种数据持久化的方法,下面笔者会逐个进行详细的讲解. iphone提供的数据持久化的方法,从数据保存的方式上讲可以分为三大部分:属性列表.对象归档.嵌入式数据库(SQLite3).其他方法. 一.属