一个数据库最重要的部分是什么?
关系型数据库mysql有着四大特性,原子性,隔离性,一致性,持久性。
kv数据库有着原子性,持久性,弱一致性。
可见,不管数据库的存储引擎是什么,其根本大概就是保证原子性,持久性和一致性的同时,通过一些优化提高数据库的查询效率。
以postgreSQL为例,系统结构包括链接管理,编译执行系统,存储管理系统,事务系统。不可否认这些都很重要,但我认为最重要的应当是存储管理和事务。
目前,我认为,一个数据库的核心实现在于事务和持久化,以及后续的一系列操作的优化。
因此,重点是首先解决事务和持久化。
从github上寻找数据库,分析源码,看他们是如何实现事务和持久化,就是本文的重点了。
skiplist-cpp项目
一个基于跳表的存储kv引擎:https://github.com/youngyangyang04/Skiplist-CPP
这个是我第一个接触的存储引擎,非常简单的一个项目,核心的实现只有两个cpp文件,这里就不多介绍了,ReadMe写的很清楚。
提供增删查三个接口,
没有进行事务处理,原子性是通过对方法加锁的方式比较粗暴的实现的。
持久化是直接kv转string以文本的方式通过fstream.writh
保存字符串在本地。直接通过readline的方式进行本地存取,我觉得这不是一个理想的方式。
BeetleDB项目
一个简单的数据库实现:https://github.com/chenbjin/BeetleDB
作者的码风挺好,但是没有文档,只能自己扒源码慢慢看,算是一个比较全的流程吧,从sql解析,到数据库,表这些的创建,再到索引的建立,增上改查和数据持久化。
尽管我关注的是事务和持久化,但由于该项目的文档比较简陋,所以简单梳理一下项目结构:
大概介绍一下,我将项目主要分为三部分:1.业务,2.存储,3.索引
- 业务部分:主要由Interpreter,SQLStatement,CatalogManager和API部分进行管理。
- 索引部分:主要由IndexManager,BPlusTree进行管理。
- 存储部分:主要由RecoderManager,BufferManager,fileHandle,fileInfo,BlockHandle和BlockInfo进行管理。
BeetleDB是单线程,没有事务管理的数据库,因此我们对它的学习重点是在持久化和索引。
持久化
持久化值得说一说,BeetleDB在持久化上做的工作就比上面的skiplist-cpp项目要多一些,它将文件保存为下图这样的二进制文件,一个表中的数据保存在一个文件中,一个文件由多个block组成,每个block保存着逻辑相邻block的信息以及block本身保存的数据,这样做的好处就是可以直接通过文件指针跳到对应的存储单元中进行本地修改,减少遍历文件的花费,这一手我觉得是可以参考学习的。
给出核心代码(部分)BlockInfo::WriteInfo()
:
path += file_->get_db_name() + "/" + file_->get_file_name();
if (file_->get_type() == FORMAT_INDEX) path += ".index";
else path += ".records";
ofstream ofs(path, ios::binary);
ofs.seekp(block_num_*4*1024);
ofs.write(data_,4*1024);
ofs.close();
这里的代码就是关于写入一个file中的block的方法了,很清晰,先通过path找到文件,用二进制打开,然后用seekp移动文件指针到对应的block上,直接write一个block的数据就好了。read的方法同理,把write换成read即可。
这些写入的block都是在内存中维护的,因此当要对一个内存中的block持久化的时候,直接调用对应blockInfo对象的write方法即可。
至于如何维护内存中的blockInfo对象信息,可以结合上面的项目结构去源码里看,个人感觉还蛮清晰的。
简单提一下关于crud的操作,业务逻辑主要集中在RecordManager中,其实本质还是现在内存中找,找不到就去磁盘一个block一个block的找。
索引
建立在一个表column上的index是通过B+树进行维护的。
其实就是常见的B+树操作,注意一点就是,这里b+树的一个节点可以保存一个block的信息,里面的key顺序存储,在查找的时候有二分和顺序两种选择,阈值源码中定义的是20。
感觉叶子节点比较重要:BPlusTreeNode::GetKeys(), BPlusTreeNode::GetValues(), BPlusTreeNode::GetNextLeaf()
TKey BPlusTreeNode::GetKeys(int index)
{
TKey k(tree_->GetIndex()->get_key_type(), tree_->GetIndex()->get_key_len());
int base = 12;
int lenr = 4 + tree_->GetIndex()->get_key_len();
memcpy(k.get_key(), &buffer_[base + index * lenr+4], tree_->GetIndex()->get_key_len());
return k;
}
int BPlusTreeNode::GetValues(int index)
{
int base = 12;
int lenR = 4 + tree_->GetIndex()->get_key_len();
return *((int*)(&buffer_[base + index*lenR]));
}
int BPlusTreeNode::GetNextLeaf()
{
int base = 12;
int lenR = 4 + tree_->GetIndex()->get_key_len();
return *((int*)(&buffer_[base + tree_->get_degree()*lenR]));
}
这里的buffer就是block对应的那一块数据,索引文件的block相比起普通文件的block,每行多了4个字节的空间,从BPlusTreeNode的set方法中就可以看出来,一个block组成为nodetype,count,parent,多至少4字节的空间来保存int型变量。
BettleDB数据库是一个小型的单线程数据库,没有事务管理,但提供了一个相比起skiplist-cpp更好的持久化方法,和索引示例。这也是我认为BettleDB最重要的部分之一。
LevelDB
源码:https://github.com/messixukejia/leveldb
一个开源的优秀的kv NoSQL。码风很好,网上的资料也有,因此可以尝试入门学习。
可以学习它的事务,存储,和sql优化方案。
PostgreSQL
源码:https://github.com/postgres
一个开源的经典关系型数据库。码风很好,源码也容易获得,可以尝试继续学习。