Let's build a simple Database Engine (1)

 

 

Let's build a simple Database Engine (1)

 

邓辉


   

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

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

 

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

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

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

 

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

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

 

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

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

 

下面,我们用 Ptyhon list 来模拟一个磁盘 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:

       print key," not found"

    else:

       return log[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 log WAL )”来保证事务一致性。首先会把所有的变化写入到一个磁盘 WAL 中,

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

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

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

 

2 snapshot

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

 

3 、并发访问

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

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

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

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

 

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

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

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

 

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

    

 

展开阅读全文

没有更多推荐了,返回首页