Let's build a simple Database Engine (1)

来源:互联网 发布:麦迪cba数据 编辑:程序博客网 时间:2024/05/16 07:37

 

 

Let's build a simple Database Engine (1)

 

邓辉


   

    最近的工作中,有一部分和存储系统设计有关,因此研究了不少这方面的文章,对于其中比较经典的也尽量抽空把它们翻译

出来,供大家学习和交流之用。

 

   有篇名为“一种日志结构文件系统的设计与实现(The Design and Implementation of a Log-Structured filesystem)”(http://blog.csdn.net/hoping/archive/2010/03/01/5336371.aspx),非常经典,也是目前很多流行的存储系统设计所采用的方法,

比如:couchDB。尤其是其在backupsanpshot、事务以及并发访问方面,能够大大简化设计,为我们带来非常优雅的系统实现。并且

非常适合于那些写密集型存储系统。

 

   尽管log-structrued有这么多的优点,但是其思想却非常简单,我准备通过一系列blog,采用Python语言来展示一个简单

的基于Key-value的日志结构非关系数据库的实现(虽然最初是用于文件系统设计的,但是也非常适合数据库存储引擎的设计),主要为展示其思想。

 

    Log-Structured的主要思想就是它是append-only的,pure-functional的。所有的更改操作都不会更改已有的数据,而是append到log

的尾部,数据项的索引也存在于log的尾部,并和数据一样在发生变化时也是重新append的log的尾部的。

 

下面,我们用Ptyhonlist来模拟一个磁盘log文件,用dict来实现索引,存放在log的尾部,来说明一下上述思想。


我们先来进行一些简单的实验:

 

一开始:log = []

我们在其中增加一个数据('key1','data1'),此时

 

log=[('key1','data1'),{'key1':0}],其中{'key1':0}log

 

的数据索引,当增加一个新数据项('key2','data2')时,

log=[('key1', 'data1'), {'key1': 0},('key2', 'data2'), {'key2': 2, 'key1': 0}]

也就是说,没有对原始数据进行任何更改,只是把新数据增加到log的尾部,并更新索引。

 

如果更改现有数据,比如把key1的值改为new_data1,此时

log=[('key1', 'data1'), {'key1': 0},('key2', 'data2'), {'key2': 2, 'key1': 0}, ('key1', 'new_data1'), {'key2': 2,'key1': 4}]

新值被追加的log尾部,老的值及其索引都没有发生任何变化。

 

如果把key2删除了,那么

log=[('key1', 'data1'), {'key1': 0},('key2', 'data2'), {'key2': 2, 'key1': 0}, ('key1', 'new_data1'), {'key2': 2,'key1': 4},{'key1': 4}]

 

 

把上述过程封装在logdb.py模块中,代码如下:

 

log = []

def store(key,value):

   if len(log) == 0:

       root = {}

   else:

       root= log[-1].copy()

   log.append((key,value))

   root[key] = len(log)-1

   log.append(root)

 

def retrieve(key):

   root = log[-1]

   if key not in root:

       printkey," not found"

   else:

       returnlog[root[key]]

 

def delete(key):

   root = log[-1].copy()

   if key in root:

       del(root[key])

       log.append(root)

   

 

上面的代码已经表达出来log-structured存储系统的基本思想,当然,还有些关键问题需要解决,比如随着数据的更改和删除,如何

高效地解决数据碎片问题,当数据量很大时,如何解决索引存储效率问题(用B-Tree)等等,这些内容会在后续文章中介绍。

 

接下来我们先谈谈这种方法的优点(当然最大的优点就是写入性能得到大大提升):

1、事务一致性问题

    一般来讲,许多数据库系统都用一个“write ahead logWAL)”来保证事务一致性。首先会把所有的变化写入到一个磁盘WAL中,

然后才更新实际的数据库文件。如果此间出现了系统崩溃,只要从WAL读出最近的记录,重新执行一遍就行了(一般会有一个checkpoint,只要

执行checkpoint后的操作即可)。在日志结构系统中,如果我们把root的索引记录在另外一个磁盘位置,那么就不不需要额外的WAL了,在恢复时,

只要读取出前一次正确的root索引,然后向前搜索(roll forward),根据数据重建root索引即可。  

 

2snapshot

  snapshot更是小菜一碟,log中的每个原来的索引都表示那个时刻数据库的snapshot

 

3、并发访问

   为了能够在并发访问时,保证数据库的事务语义,绝大多数数据库都是用非常复杂的锁机制来控制数据的更新顺序。这实现起来非常复杂和低效。

log-structrued的实现中,可以采用Multiversion ConcurrencyControl, 也就是MVCC的方法。对于读取来说,任何并发体只要得到一个root index

就是绝对安全的(当然,这个root index不能直接从log尾部得到,因为此时可能会有其他并发体在同时进行append或者更改中,得放到一个安全的地方),无需任何锁。比如:以log=[('key1','data1'),{'key1':0}]为例,一个并发体A得到索引{'key1':0},此后的任何对于log的操作都不会

影响到A看到的logsnapshot视图,因此读可以是无锁的。

 

   对于写来说,也很简单,可以采用乐观并发控制(http://en.wikipedia.org/wiki/Optimistic_concurren)。如果并发体A要更改某个数据项,它先进行一个

读取操作,获取一个snapshot,然后获取一个写入锁,接着检查前面读入的数据是不是被更改过(这个检查可以很快,只要对比要更改的key值的snapshot的索引和

log中的最新索引即可),如果没有就进行写入操作,如果被更改了就说明有冲突,那就回滚,重试。

 

这些优点,会在后续的文章中,通过代码进行详细说明

    

 

原创粉丝点击