用Python写一个NoSQL数据库

发表于:2017-5-23 10:55

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:佚名    来源:segmentfault

  Set Up
  下面是我们服务器所需的一些样板代码:
"""NoSQL database written in Python"""
# Standard library imports
import socket
HOST = 'localhost'
PORT = 50505
SOCKET = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
STATS = {
'PUT': {'success': 0, 'error': 0},
'GET': {'success': 0, 'error': 0},
'GETLIST': {'success': 0, 'error': 0},
'PUTLIST': {'success': 0, 'error': 0},
'INCREMENT': {'success': 0, 'error': 0},
'APPEND': {'success': 0, 'error': 0},
'DELETE': {'success': 0, 'error': 0},
'STATS': {'success': 0, 'error': 0},
}
  很容易看到, 上面的只是一个包的导入和一些数据的初始化。
  Set up(Cont'd)
  接下来我会跳过一些代码, 以便能够继续展示上面准备部分剩余的代码。 注意它涉及到了一些尚不存在的一些函数, 不过没关系, 我们会在后面涉及。 在完整版(将会呈现在最后)中, 所有内容都会被有序编排。 这里是剩余的安装代码:
COMMAND_HANDERS = {
'PUT': handle_put,
'GET': handle_get,
'GETLIST': handle_getlist,
'PUTLIST': handle_putlist,
'INCREMENT': handle_increment,
'APPEND': handle_append,
'DELETE': handle_delete,
'STATS': handle_stats,
}
DATA = {}
def main():
"""Main entry point for script"""
SOCKET.bind(HOST, PORT)
SOCKET.listen(1)
while 1:
connection, address = SOCKET.accept()
print('New connection from [{}]'.format(address))
data = connection.recv(4096).decode()
command, key, value = parse_message(data)
if command == 'STATS':
response = handle_stats()
elif command in ('GET', 'GETLIST', 'INCREMENT', 'DELETE'):
response = COMMAND_HANDERS[command](key)
elif command in (
'PUT',
'PUTLIST',
'APPEND', ):
response = COMMAND_HANDERS[command](key, value)
else:
response = (False, 'Unknown command type {}'.format(command))
update_stats(command, response[0])
connection.sandall('{};{}'.format(response[0], response[1]))
connection.close()
if __name__ == '__main__':
main()
  我们创建了 COMMAND_HANDLERS, 它常被称为是一个 查找表 (look-up table) . COMMAND_HANDLERS 的工作是将命令与用于处理该命令的函数进行关联起来。 比如说, 如果我们收到一个 GET 命令, COMMAND_HANDLERS[command](key) 就等同于说 handle_get(key) . 记住,在 Python 中, 函数可以被认为是一个值,并且可以像其他任何值一样被存储在一个 dict 中。
  在上面的代码中, 虽然有些命令请求的参数相同,但是我仍决定分开处理每个命令。 尽管可以简单粗暴地强制所有的 handle_ 函数接受一个 key 和一个 value , 但是我希望这些处理函数条理能够更加有条理, 更加容易测试,同时减少出现错误的可能性。
  注意 socket 相关的代码已是十分极简。 虽然整个服务器基于 TCP/IP 通信, 但是并没有太多底层的网络交互代码。
  最后还须需要注意的一小点: DATA 字典, 因为这个点并不十分重要, 因而你很可能会遗漏它。 DATA 就是实际用来存储的 key-value pair, 正是它们实际构成了我们的数据库。
  Command Parser
  下面来看一些 命令解析器 (command parser) , 它负责解释接收到的消息:
def parse_message(data):
"""Return a tuple containing the command, the key, and (optionally) the
value cast to the appropriate type."""
command, key, value, value_type = data.strip().split(';')
if value_type:
if value_type == 'LIST':
value = value.split(',')
elif value_type == 'INT':
value = int(value)
else:
value = str(value)
else:
value = None
return command, key, value
  这里我们可以看到发生了类型转换 (type conversion). 如果希望值是一个 list, 我们可以通过对 string 调用 str.split(',') 来得到我们想要的值。 对于 int, 我们可以简单地使用参数为 string 的 int() 即可。 对于字符串与 str() 也是同样的道理。
  Command Handlers
  下面是命令处理器 (command handler) 的代码. 它们都十分直观,易于理解。 注意到虽然有很多的错误检查, 但是也并不是面面俱到, 十分庞杂。 在你阅读的过程中,如果发现有任何错误请移步 这里 进行讨论.
def update_stats(command, success):
"""Update the STATS dict with info about if executing *command* was a
*success*"""
if success:
STATS[command]['success'] += 1
else:
STATS[command]['error'] += 1
def handle_put(key, value):
"""Return a tuple containing True and the message to send back to the
client."""
DATA[key] = value
return (True, 'key [{}] set to [{}]'.format(key, value))
def handle_get(key):
"""Return a tuple containing True if the key exists and the message to send
back to the client"""
if key not in DATA:
return (False, 'Error: Key [{}] not found'.format(key))
else:
return (True, DATA[key])
def handle_putlist(key, value):
"""Return a tuple containing True if the command succeeded and the message
to send back to the client."""
return handle_put(key, value)
def handle_putlist(key, value):
"""Return a tuple containing True if the command succeeded and the message
to send back to the client"""
return handle_put(key, value)
def handle_getlist(key):
"""Return a tuple containing True if the key contained a list and the
message to send back to the client."""
return_value = exists, value = handle_get(key)
if not exists:
return return_value
elif not isinstance(value, list):
return (False, 'ERROR: Key [{}] contains non-list value ([{}])'.format(
key, value))
else:
return return_value
def handle_increment(key):
"""Return a tuple containing True if the key's value could be incremented
and the message to send back to the client."""
return_value = exists, value = handle_get(key)
if not exists:
return return_value
elif not isinstance(list_value, list):
return (False, 'ERROR: Key [{}] contains non-list value ([{}])'.format(
key, value))
else:
DATA[key].append(value)
return (True, 'Key [{}] had value [{}] appended'.format(key, value))
def handle_delete(key):
"""Return a tuple containing True if the key could be deleted and the
message to send back to the client."""
if key not in DATA:
return (
False,
'ERROR: Key [{}] not found and could not be deleted.'.format(key))
else:
del DATA[key]
def handle_stats():
"""Return a tuple containing True and the contents of the STATS dict."""
return (True, str(STATS))
  有两点需要注意: 多重赋值 (multiple assignment) 和代码重用. 有些函数仅仅是为了更加有逻辑性而对已有函数的简单包装而已, 比如 handle_get 和 handle_getlist . 由于我们有时仅仅是需要一个已有函数的返回值,而其他时候却需要检查该函数到底返回了什么内容, 这时候就会使用 多重赋值 。
  来看一下 handle_append . 如果我们尝试调用 handle_get 但是 key 并不存在时, 那么我们简单地返回 handle_get 所返回的内容。 此外, 我们还希望能够将 handle_get 返回的 tuple 作为一个单独的返回值进行引用。 那么当 key 不存在的时候, 我们就可以简单地使用 return return_value .
  如果它 确实存在 , 那么我们需要检查该返回值。并且, 我们也希望能够将 handle_get 的返回值作为单独的变量进行引用。 为了能够处理上述两种情况,同时考虑需要分开处理结果的情形,我们使用了多重赋值。 如此一来, 就不必书写多行代码, 同时能够保持代码清晰。 return_value = exists, list_value = handle_get(key) 能够显式地表明我们将要以至少两种不同的方式引用 handle_get 的返回值。
  How Is This a Database?
  上面的程序显然并非一个 RDBMS, 但却绝对称得上是一个 NoSQL 数据库。它如此易于创建的原因是我们并没有任何与 数据 (data) 的实际交互。 我们只是做了极简的类型检查,存储用户所发送的任何内容。 如果需要存储更加结构化的数据, 我们可能需要针对数据库创建一个 schema 用于存储和检索数据。
  既然 NoSQL 数据库更容易写, 更容易维护,更容易实现, 那么我们为什么不是只使用 mongoDB 就好了? 当然是有原因的, 还是那句话,有得必有失, 我们需要在 NoSQL 数据库所提供的数据灵活性 (data flexibility) 基础上权衡数据库的可搜索性 (searchability).
  Querying Data
  假如我们上面的 NoSQL 数据库来存储早前的 Car 数据。 那么我们可能会使用 VIN 作为 key, 使用一个列表作为每列的值, 也就是说, 2134AFGER245267 = ['Lexus', 'RX350', 2013, Black] . 当然了, 我们已经丢掉了列表中每个索引的 涵义 (meaning) . 我们只需要知道在某个地方索引 1 存储了汽车的 Model , 索引 2 存储了 Year.
  糟糕的事情来了, 当我们想要执行先前的查询语句时会发生什么? 找到 1994 年所有车的颜色将会变得噩梦一般。 我们必须遍历 DATA 中的 每一个值 来确认这个值是否存储了 car 数据亦或根本是其他不相关的数据, 比如说检查索引 2, 看索引 2 的值是否等于 1994,接着再继续取索引 3 的值. 这比 table scan 还要糟糕,因为它不仅要扫描每一行数据,还需要应用一些复杂的规则来回答查询。
  NoSQL 数据库的作者当然也意识到了这些问题,(鉴于查询是一个非常有用的 feature) 他们也想出了一些方法来使得查询变得不那么 “遥不可及”。一个方法是结构化所使用的数据,比如 JSON, 允许引用其他行来表示关系。 同时, 大部分 NoSQL 数据库都有名字空间 (namespace) 的概念, 单一类型的数据可以被存储在数据库中该类型所独有的 "section" 中,这使得查询引擎能够利用所要查询数据的 "shape" 信息。
  当然了,尽管为了增强可查询性已经存在 (并且实现了)了一些更加复杂的方法, 但是在存储更少量的 schema 与增强可查询性之间做出妥协始终是一个不可逃避的问题。 本例中我们的数据库仅支持通过 key 进行查询。 如果我们需要支持更加丰富的查询, 那么事情就会变得复杂的多了。
  Summary
  至此, 希望 "NoSQL" 这个概念已然十分清晰。 我们学习了一点 SQL, 并且了解了 RDBMS 是如何工作的。 我们看到了如何从一个 RDBMS 中检索数据 (使用 SQL 查询 (query)). 通过搭建了一个玩具级别的 NoSQL 数据库, 了解了在可查询性与简洁性之间面临的一些问题, 还讨论了一些数据库作者应对这些问题时所采用的一些方法。
  即便是简单的 key-value 存储, 关于数据库的知识也是浩瀚无穷。虽然我们仅仅是探讨了其中的星星点点, 但是仍然希望你已经了解了 NoSQL 到底指的是什么, 它是如何工作的, 什么时候用比较好。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号