DBDB: Dog Bed Database学习笔记

原英文链接:http://aosabook.org/en/500L/dbdb-dog-bed-database.html
这里简要写下自己学习的笔记(基本按照翻译的顺序一步步写下来)

简介

DBDB(Dog Bed Database)是一个实现了简单的键值类数据库的Python库。将键值关系存储到磁盘上,以备后续取回。
DBDB的目的在于当机器崩溃或者出错也能保留数据,避免一次性全在RAM上保存数据,因此可以存储比RAM更多的数据。

Memory

I remember the first time I was really stuck on a bug. When I finished typing in my BASIC program and ran it, weird sparkly pixels (奇怪的闪闪发光的像素)showed up on the screen, and the program aborted(中止) early. When I went back to look at the code, the last few lines of the program were gone.
One of my mom’s friends knew how to program, so we set up a call. Within a few minutes of speaking with her, I found the problem: the program was too big, and had encroached onto(侵犯到) video memory(显存). Clearing the screen truncated the program, and the sparkles were artifacts of Applesoft BASIC’s behaviour of storing program state in RAM just beyond the end of the program.
From that moment onwards, I cared about memory allocation(分配). I learned about pointers and how to allocate memory with malloc. I learned how my data structures were laid out(布局) in memory. And I learned to be very, very careful about how I changed them.
Some years later, while reading about a process-oriented(面向进程) language called Erlang, I learned that it didn’t actually have to copy data to send messages between processes, because everything was immutable(一成不变). I then discovered immutable data structures in Clojure, and it really began to sink in.
When I read about CouchDB in 2013, I just smiled and nodded, recognising the structures and mechanisms for managing complex data as it changes.
I learned that you can design systems built around immutable data.
Then I agreed to write a book chapter.
I thought that describing the core data storage concepts of CouchDB (as I understood them) would be fun.
While trying to write a binary tree algorithm that mutated the tree in place, I got frustrated with how complicated things were getting. The number of edge cases and trying to reason about how changes in one part of the tree affected others was making my head hurt. I had no idea how I was going to explain all of this.
Remembering lessons learned, I took a peek at a recursive(递归) algorithm for updating immutable binary trees and it turned out to be relatively straightforward.
I learned, once again, that it’s easier to reason about things that don’t change.
So starts the story.

Why Is it Interesting?

Most projects require a database of some kind. You really shouldn’t write your own; there are many edge cases that will bite you, even if you’re just writing JSON to disk:
What happens if your filesystem runs out of space?
What happens if your laptop battery dies while saving?
What if your data size exceeds(超过) available memory? (Unlikely for most applications on modern desktop computers… but not unlikely for a mobile device or server-side web application.)
However, if you want to understand how a database handles all of these problems, writing one for yourself can be a good idea.
The techniques and concepts we discuss here should be applicable to any problem that needs to have rational(合理的), predictable behaviour when faced with failure.
Speaking of failure…

检定失败

数据库一般通过他们坚持ACID性能来表现他们的特征:atomicity(原子性), consistency(一致性), isolation(隔离性), and durability(持久性)
DBDB的更新是原子的并且是可持久的。DBDB不提供一致性保证,因为数据存储没有限制,隔离性也没有实现。
应用层代码可以加上他自己的一致性保证,但是合适的隔离需要有一个事务管理器。
另外,我们还有其他的系统维护问题需要考虑。在本设计中,陈旧的数据没有回收,所以重复的更新最终会完全消耗掉磁盘空间。(You will shortly discover why this is the case.)PostgreSQL calls this reclamation “vacuuming” (which makes old row space available for re-use), and CouchDB calls it “compaction” (by rewriting the “live” parts of the data store into a new file, and atomically moving it over the old one).
DBDB could be enhanced to add a compaction feature(压缩功能), but it is left as an exercise for the reader1.

DBDB结构

DBDB将存储到磁盘中某个位置(数据如何放到文件中;实体层)从键值存储内容(键-值间的关联;公共API)中的数据逻辑结构(本例为二叉树;逻辑层)中分离出来。
许多数据结构将逻辑部分和实体部分分开,这样可以对各部分的实现进行替代,从而能得到不同的性能特点, e.g. DB2’s SMS (files in a filesystem) versus DMS (raw block device,原始块设备) tablespaces, or MySQL’s alternative engine implementations.

Discovering the Design

书中的大部分章节都是从描述程序如何搭建的到最终实现。我们大部分都是找到别人写的代码,然后学会怎么修改或者扩展它来实现不同的功能。
本章中,我们假设DBDB是一个已经实现的工程,一步步往下执行来学习它如何工作的。

组织单元

tool.py:命令行工具,从终端窗口来操作数据库

interface.py:使用二叉树实现方式定义了DBDB类->实现Python的字典接口。因此才能在Python程序中使用DBDB

logical.py:逻辑层,键值存储的抽象接口

LogicalBase:逻辑层更新的接口(get、set、commit)
ValueRef:指向存储在数据库中的二进制数据的Python类(间接引用->避免一次性全将数据存到内存中)

binary_tree.py:逻辑接口之下的实体二叉树算法

BinaryTree:实现二叉树(get、insert、delete、key/value对),一成不变的 -- > 更新 -->返回一新的树。
BinaryNode:二叉树的一个点
BinaryNodeRef:实例化的ValueRef,可以 serialise and deserialise(连载和丢弃)一二叉树节点(BinaryNode)

physical.py:实体层,Storage类提供持久性(大多是只可加的)

读取值

从最简单的例子开始:从数据库读一个值。
get the value associated with key foo inexample.db:

$ python -m dbdb.tool example.db get foo

运行dbdb.tool.py中的main()函数:

# dbdb/tool.py
def main(argv):
    if not (4 <= len(argv) <= 5):
        usage()
        return BAD_ARGS
    dbname, verb, key, value = (argv[1:] + [None])[:4]
    if verb not in {'get', 'set', 'delete'}:
        usage()
        return BAD_VERB
    db = dbdb.connect(dbname)          # CONNECT
    try:
        if verb == 'get':
            sys.stdout.write(db[key])  # GET VALUE
        elif verb == 'set':
            db[key] = value
            db.commit()
        else:
            del db[key]
            db.commit()  #提交
    except KeyError:
        print("Key not found", file=sys.stderr)
        return BAD_KEY
    return OK

(1)

connect()函数打开数据库文件(如果不存在会创建它,但是从不会覆盖已存在的数据库文件),返回一DBDB:

# dbdb/__init__.py
def connect(dbname):
    try:
        f = open(dbname, 'r+b')
    except IOError:
        fd = os.open(dbname, os.O_RDWR | os.O_CREAT)
        f = os.fdopen(fd, 'r+b')
    return DBDB(f)
# dbdb/interface.py
class DBDB(object):

    def __init__(self, f):
        self._storage = Storage(f)
        self._tree = BinaryTree(self._storage)

可以看到DBDB有一个Storage的实例引用,同时也通过self._tree共享了Storage的实例。为何?self._tree不能自己完全掌管到Storage么?
哪一类拥有一个资源在设计时候是一个重要的点,它给了我们哪些改变可能是不安全的。保留这个疑问,继续往下看。

(2)

一旦有了DBDB实例,通过字典查询(db[key])得到键key对应的值,字典查询导致Python解释器调用DBDB.getitem():

# dbdb/interface.py
class DBDB(object):
# ...
    def __getitem__(self, key):
        self._assert_not_closed()
        return self._tree.get(key)

    def _assert_not_closed(self):
        if self._storage.closed:
            raise ValueError('Database closed.')

_ getitem_ ()通过调用_assert_not_closed保证数据库仍然处于打开的状态 —>这里我们就要求DBDB有Storage实例的直接引用:这样他才能执行先决条件判断。

(3)

DBDB之后通过调用_tree.get()函数查找_tree中跟键key关联的值,_tree.get()由LogicalBase类提供:

# dbdb/logical.py
class LogicalBase(object):
# ...
    def get(self, key):
        if not self._storage.locked:
            self._refresh_tree_ref()
        return self._get(self._follow(self._tree_ref), key)

get()首先检查我们该存储是否被锁着。

  • 【3.1】

如果该存储storage没有被锁着:

# dbdb/logical.py
class LogicalBase(object):
# ...
def _refresh_tree_ref(self):
        self._tree_ref = self.node_ref_class(
            address=self._storage.get_root_address())

_refresh_tree_ref重置树看目前在磁盘上的数据的视角’view’,这样我们可以实现一个完全更新过的读取。

  • 【3.2】

如果storage被锁着,说明有其他进程可能正在改变我们正想去读的这个数据;我们读到的可能就不是最新的数据。这个叫做“dirty read”。这样的模式允许读者读取数据时可以不用担心阻塞问题,代价就是数据不是最新的。

(4)

现在,我们看看如何真正读到该数据的:

# dbdb/binary_tree.py
class BinaryTree(LogicalBase):
# ...
    def _get(self, node, key):
        while node is not None:
            if key < node.key:
                node = self._follow(node.left_ref)
            elif node.key < key:
                node = self._follow(node.right_ref)
            else:
                return self._follow(node.value_ref)
        raise KeyError

这是标准的二叉树搜索。Node和NodeRef是值类:他们是不变的,其中的内容也从不变。Node由键值关系,左右子叶组成。键值关系也从不变。
二叉树只有在根节点变化时才会改变,因此我们查询时不需要担心树的内容发生了变化。

插入和更新

设置键foo的值为bar in example.db:

$ python -m dbdb.tool example.db set foo bar

运行dbdb.tool模块中的main()函数,如上所示。

# dbdb/tool.py
def main(argv):
    ...
    db = dbdb.connect(dbname)          # CONNECT
    try:
        ...
        elif verb == 'set':
            db[key] = value            # SET VALUE
            db.commit()                # COMMIT
        ...
    except KeyError:
        ...

(1)

db[key] = value 调用DBDB.setitem()

# dbdb/interface.py
class DBDB(object):
# ...
    def __setitem__(self, key, value):
        self._assert_not_closed()
        return self._tree.set(key, value)

_ _setitem__确保数据库仍然处于打开状态。

(2)

通过调用_tree.set()存储key、value关系到_tree中:

# dbdb/logical.py
class LogicalBase(object):
# ...
    def set(self, key, value):
        if self._storage.lock():
            self._refresh_tree_ref()
        self._tree_ref = self._insert(
            self._follow(self._tree_ref), key, self.value_ref_class(value))

【2.1】
set()首先检查storage锁:

# dbdb/storage.py
class Storage(object):
    ...
    def lock(self):
        if not self.locked:
            portalocker.lock(self._f, portalocker.LOCK_EX)
            self.locked = True
            return True
        else:
            return False
我们的锁由第三方文件锁库(portalocker)提供
如果数据库已经被锁住了,lock()返回False,否则返回True

【2.2】
回到_tree.set(),我们可以理解我们首先要检查lock()的返回值了:这样我们对于最近的根节点的引用调用 _refresh_tree_ref 就不会错过另一个进程已经做得更新。之后替换根节点,新的根节点是一个新的树,包含插入的或更新的键值对。
插入或更新树没有改变任何节点,因为_insert()返回一个新的树。新的树和之前的树共享未改变的部分,这样可以节省内存和执行时间。

# dbdb/binary_tree.py
class BinaryTree(LogicalBase):
# ...
    def _insert(self, node, key, value_ref):
        if node is None:
            new_node = BinaryNode(
                self.node_ref_class(), key, value_ref, self.node_ref_class(), 1)
        elif key < node.key:
            new_node = BinaryNode.from_node(
                node,
                left_ref=self._insert(
                    self._follow(node.left_ref), key, value_ref))
        elif node.key < key:
            new_node = BinaryNode.from_node(
                node,
                right_ref=self._insert(
                    self._follow(node.right_ref), key, value_ref))
        else:
            new_node = BinaryNode.from_node(node, value_ref=value_ref)
        return self.node_ref_class(referent=new_node)

注意到,我们总是返回一个新的节点(包裹在NodeRef中)。我们建立一个新的节点,该节点共享未改变的子树部分,而不是更新该节点到一个新的子树。这样,我们的二叉树是一个永久不变的数据结构。

(3)

你可能已经注意到了奇怪的地方:我们没有在磁盘上做任何的改变,我们之前做的是通过来回移动树节点来操纵我们看磁盘上的数据的视角。
为了真正的在磁盘上写入这些改变值,我们需要明确地调用commit(),,tool.py中set操作的第二部分:
提交包括将内存中的dirty stat写出,保存树的新的根节点的磁盘地址。
由API开始:

# dbdb/interface.py
class DBDB(object):
# ...
    def commit(self):
        self._assert_not_closed()
        self._tree.commit()

_tree.commit()的实现来自于LogicalBase:

# dbdb/logical.py
class LogicalBase(object)
# ...
    def commit(self):
        self._tree_ref.store(self._storage)
        self._storage.commit_root_address(self._tree_ref.address)

【3.1】
所有的NodeRef通过首先让他们的子节点通过prepare_to_store()连载到磁盘来将其自身连载到磁盘上:

# dbdb/logical.py
class ValueRef(object):
# ...
    def store(self, storage):
        if self._referent is not None and not self._address:
            self.prepare_to_store(storage)
            self._address = storage.write(self.referent_to_string(self._referent))

【3.2】
LogicalBase中的self._tree_ref实际上是一个 BinaryNodeRef (一个ValueRef的子类) 所以 prepare_to_store()的具体实现为:

# dbdb/binary_tree.py
class BinaryNodeRef(ValueRef):
    def prepare_to_store(self, storage):
        if self._referent:
            self._referent.store_refs(storage)

【3.3】

# dbdb/binary_tree.py
class BinaryNode(object):
# ...
    def store_refs(self, storage):
        self.value_ref.store(storage)
        self.left_ref.store(storage)
        self.right_ref.store(storage)

这样的递归一直进行下去,对于所有的未被写操作改变的NodeRef,例如没有_address的。

【3.4】
回到ValueRef的store方法中(【3.1】所示)。最后一步是连载该节点,保存数据存储地址。

# dbdb/logical.py
class ValueRef(object):
# ...
    def store(self, storage):
        if self._referent is not None and not self._address:
            self.prepare_to_store(storage)
            self._address = storage.write(self.referent_to_string(self._referent))

这里,我们已经保证NodeRef的_referent对于他自己的refs都有可用的地址,所以我们通过创建一个字节串来代表这个节点,实现连载它:

# dbdb/binary_tree.py
class BinaryNodeRef(ValueRef):
# ...
    @staticmethod
    def referent_to_string(referent):
        return pickle.dumps({
            'left': referent.left_ref.address,
            'key': referent.key,
            'value': referent.value_ref.address,
            'right': referent.right_ref.address,
            'length': referent.length,
        })

(4)

store()方法中更新地址是技术上突变的ValueRef。因为他对用户可见的值没有影响,因此我们可以认为他是一成不变的。
一旦store()对于根节点_tree_ref实现了,我们也就把所有数据写到了磁盘上了。我们现在可以提交根节点地址:

# dbdb/physical.py
class Storage(object):
# ...
    def commit_root_address(self, root_address):
        self.lock()
        self._f.flush()
        self._seek_superblock()
        self._write_integer(root_address)
        self._f.flush()
        self.unlock()

因为根节点地址要么是旧的要么是新的,不会是两者之间模糊的,这样其他进程不需要获得锁才能读数据。对于外部程序来说,看到的要么是旧的要么是新的树,永不会两种树混杂再一起。所以说,提交时原子性的。
因为我们在写根节点地址之前在磁盘上写了新的数据并且调用了fsync的syscall,因此未被提交的数据是无法访问到的。反过来说,一旦根节点地址更新了,那么它所指向的数据都在磁盘上。所以说提交也是持久性的。

NodeRefs如何保存到内存中的

为了避免一下把整个树结构放到内存中,当读磁盘上的一个逻辑节点时,他的左和右子叶的磁盘地址也被加载到了内存中。为了得到子叶和他们的值需要另一个额外的函数:NodeRef.get()来从新引用这个数据。
NodeRef结构:

+---------+
| NodeRef |
| ------- |
| addr=3  |
| get()   |
+---------+

调用get()会返回具体的节点,同时还有节点的引用:

+---------+     +---------+     +---------+
| NodeRef |     | Node    |     | NodeRef |
| ------- |     | ------- | +-> | ------- |
| addr=3  |     | key=A   | |   | addr=1  |
| get() ------> | value=B | |   +---------+
+---------+     | left  ----+
                | right ----+   +---------+
                +---------+ |   | NodeRef |
                            +-> | ------- |
                                | addr=2  |
                                +---------+

当对树的改变还没提交时,这些从根节点一直指向改变的树叶的值存在于内存中。这些改变还没存到磁盘中,所以这些改变的节点包含具体的键、值,但是没有磁盘地址。写操作可以看到这些未提交的改变值,并且可以在发生提交前做更多的变化,因为NodeRef.get()如果有就会返回未提交的值。
所有的更新会原子性得出现给读者,因为直到新的根节点地址写到了磁盘上这些改变才能看到。并且更新操作通过磁盘上的文件锁来阻塞。在开始更新时获得锁,提交完释放锁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值