编译自:http://www.aosabook.org/en/500L/dbdb-dog-bed-database.html 作者:Taavi Burns 翻译:鸿 如有翻译问题或建议,请公众号留言
前文点击链接:DBDB: 一个简单的key/value数据库(一) 前文点击链接:DBDB: 一个简单的key/value数据库(二)
插入和更新数据 将key值foo对应的value值bar插入到example.db中:
$ python -m dbdb.tool example.db set foo bar
同样的进入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:
...
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__方法通过_assert_not_closed方法保证DB数据库是可以使用的,并且调用_tree方法更新key/value值。_tree.set()由LogicalBase提供:
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))
set()方法首先检查数据是否上锁:
# 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
值得注意的是: 1.这儿的锁是由portalocker模块提供 2.如果数据库锁上了返回False,否则就是True 再回到_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)
DBDB总是返回一个包装在一个NodeRef中的新节点,所以不用通过更新节点来指向新的子树,而是创建一个共享未改变的子树的新节点。 目前所做的只是通过移动树节点来控制磁盘数据。为了这些修改写入磁盘,我们需要对commit()进行显式调用。
# 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)
store()方法来源于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))
LogicalBase里的self._tree_ref实际上是BinaryNodeRef(ValueRef的子类)类
# dbdb/binary_tree.py
class BinaryNodeRef(ValueRef):
def prepare_to_store(self, storage):
if self._referent:
self._referent.store_refs(storage)
_referent实际上是BinaryNode,调用的store_refs
# 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)
这里是递归的调用store方法,将数据存储。回到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))
NodeRef的_referent确保已经递归结束,referent_to_string进行序列化:
# 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,
})
到这里所有的节点都被写入磁盘了,回到commit()方法,commit_root_address解决所有剩下的事。
# 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()
这里保证了文件句柄被成功刷新(操作系统可以将所有数据保存到稳定的存储器中)并给出根节点的地址。由于根节点地址同时拥有旧值或新值,其他进程可以从数据库中读取而不需要获得锁。外部进程可能会看到不同状态的二叉树树,但并不会混淆这两种树。所以commit具有原子性的。由于我们在写入根节点地址之前就将新数据写入磁盘并调用了fsync syscall2,因此未commit的数据是无法访问的。相反,一旦根节点地址被更新,它引用的所有数据也在磁盘上。所以,commit也具有持久性。NodeRefs如何存储数据:这是为了避免整个二叉树结构同一时间都保存在内存当中,当从磁盘读入逻辑节点时,其左右子节点的磁盘地址(及其值)也会被加载到内存中。一个NodeRef就是一个地址:
+---------+
| NodeRef |
| ------- |
| addr=3 |
| get() |
+---------+
调用get()方法会不断遍历地址:
+---------+ +---------+ +---------+
| NodeRef | | Node | | NodeRef |
| ------- | | ------- | +-> | ------- |
| addr=3 | | key=A | | | addr=1 |
| get() ------> | value=B | | +---------+
+---------+ | left ----+
| right ----+ +---------+
+---------+ | | NodeRef |
+-> | ------- |
| addr=2 |
+---------+
当对二叉树的更新没有commit时,它们会保存在内存中。 这些更新不会保存到磁盘中,因此更新的节点会包含具体的key和value,但是没有磁盘地址。在执行写入的过程可以看到没有commit的更新,并且在确认commit之前可以继续更新,因为NodeRef.get()会返回未commit的值(如果有的话);在通过API访问时,commit数据与未commit数据之间是没有区别的。 所有更新都是原子性的。并发更新则会被磁盘上的锁阻塞。锁是在第一次更新时获取并在commit后释放。