[Git翻译]Git作为NoSql数据库

699 阅读18分钟

原文地址:www.kenneth-truyers.net/2016/10/13/…

原文作者:

发布时间:2016年10月13日

image.png

Git的手册上写着,它是一个愚蠢的内容跟踪器。它可能是世界上使用最多的版本控制系统。这很奇怪,因为它并没有把自己描述为一个源码控制系统。而事实上,你可以使用git来跟踪任何类型的内容。比如你可以创建一个Git NoSQL数据库。

之所以在man-pages中说得很笨,是因为它没有假设你在里面存储什么内容。git的底层模型是相当基本的。在这篇文章中,我想探讨一下将git作为NoSQL数据库(一个键值存储)的可能性。你可以使用文件系统作为数据存储,然后使用git add和git commit来保存文件。

# saving a document 
echo '{"id": 1, "name": "kenneth"}' > 1.json 
git add 1.json 
git commit -m "added a file" 
# reading a document 
git show master:1.json => {"id": 1, "name": "kenneth"}

这样做是可行的,但你现在把文件系统当作数据库来使用:路径是键,值是你存储在其中的任何东西。有几个缺点。

  • 我们需要把所有的数据都写到磁盘上才能保存到git中去
  • 我们要多次保存数据
  • 文件存储没有重复数据删除,我们就失去了git为我们提供的自动数据重复数据删除的好处。
  • 如果我们想同时在多个分支上工作,我们需要多个检查出来的目录。

我们想要的是一个裸仓库,一个文件都不存在于文件系统中,而只存在于git数据库中的仓库。让我们来看看git的数据模型和实现这一功能的管道命令。

Git作为一个NoSQL数据库

Git是一个内容可寻址的文件系统。这意味着它是一个简单的键值存储。每当你向它插入内容时,它都会给你一个键,以便以后检索这些内容。 让我们来创建一些内容。

#Initialize a repository 
mkdir MyRepo 
cd MyRepo git init 
# Save some content 
echo {"id": 1, "name": "kenneth"} | git hash-object -w --stdin da95f8264a0ffe3df10e94eed6371ea83aee9a4d

Hash-object是一个git管道命令,它接收内容,将其存储在数据库中,并返回密钥。

-w开关告诉它要存储内容,否则它只会计算哈希值。-stdin开关告诉git从输入中读取内容,而不是从文件中读取。

它返回的 key 是基于内容的 sha-1。如果你在你的机器上运行上述命令,你会发现它返回的是完全相同的 sha-1。现在我们已经在数据库中找到了一些内容,我们可以把它读回来。

git cat-file -p da95f8264a0ffe3df10e94eed6371ea83aee9a4d 
{"id": 1, "name": "kenneth"}

Git Blobs

我们现在有了一个键值存储,有一个对象,即blob。

image.png

只有一个问题:我们不能更新这个,因为如果我们更新内容,密钥就会改变。这意味着我们的文件的每一个版本,都必须记住一个不同的密钥。我们需要的是,指定我们自己的密钥,用来追踪版本。

Git 树

树解决了两个问题。

  • 需要记住我们对象的哈希值和它的版本。
  • 存储文件组的可能性。

思考树的最佳方式是像文件系统中的文件夹。 要创建一个树,你必须遵循两个步骤。

# Create and populate a staging area 
git update-index --add --cacheinfo 100644 da95f8264a0ffe3df10e94eed6371ea83aee9a4d 1.json 
# write the tree 
git write-tree d6916d3e27baa9ef2742c2ba09696f22e41011a1

这也给你回了一个sha。现在我们可以读回那棵树了。

git cat-file -p d6916d3e27baa9ef2742c2ba09696f22e41011a1 100644 blob 
da95f8264a0ffe3df10e94eed6371ea83aee9a4d 1.json

此时,我们的对象数据库看起来如下。

image.png

要修改文件,我们按照同样的步骤进行。

# Add a blob 
echo {"id": 1, "name": "kenneth truyers"} | git hash-object -w --stdin 42d0d209ecf70a96666f5a4c8ed97f3fd2b75dda 

# Create and populate a staging area 
git update-index --add --cacheinfo 100644 42d0d209ecf70a96666f5a4c8ed97f3fd2b75dda 1.json 

# Write the tree 
git write-tree 2c59068b29c38db26eda42def74b7142de392212

这样我们就有了下面的情况。

image.png

我们现在有两棵树,代表我们文件的不同状态。这并没有什么帮助,因为我们仍然需要记住树的 sha-1 值来获取内容。

Git提交

再往上,我们就到了提交。一个提交拥有5条关键信息。

  1. 提交的作者
  2. 创建日期
  3. 为什么要创建它(留言
  4. 它所指向的单个树对象
  5. 一个或多个之前的提交(目前我们只考虑只有一个父体的提交,有多个父体的提交为合并提交)。

让我们提交上面的树。

# Commit the first tree (without a parent) 
echo "commit 1st version" | git commit-tree d6916d3 05c1cec5685bbb84e806886dba0de5e2f120ab2a 

# Commit the second tree with the first commit as a parent 
echo "Commit 2nd version" | git commit-tree 2c59068 -p 05c1cec5 
9918e46dfc4241f0782265285970a7c16bf499e4

这样我们就有了下面的状态。

image.png

现在我们已经建立了一个完整的文件历史。你可以用任何git客户端打开仓库,你会看到1.json是如何被正确跟踪的。为了证明这一点,这是运行git日志的输出。

git log --stat 9918e46 9918e46dfc4241f0782265285970a7c16bf499e4 "Commit 2nd version"
1.json | 1 + 1 file changed, 1 insertions(+) 
05c1cec5685bbb84e806886dba0de5e2f120ab2a "Commit 1st version" 1.json | 1 + 1 file changed, 1 insertion(+)

并获取上次提交时的文件内容。

git show 9918e46:1.json 
{"id": 1, "name": "kenneth truyers"}

但我们仍然没有达到目的,因为我们必须记住上次提交的哈希值。到目前为止,我们创建的所有对象都是git的*对象数据库的一部分。*该数据库的一个特点是,它只存储不可变的对象。一旦你写了一个blob、一棵树或一个提交,你永远不能在不改变键的情况下修改它。你也不能删除它们(至少不能直接删除,git gc命令确实可以删除那些悬空的对象)。

Git 引用

然而再往上一层,就是Git的引用。引用不是对象数据库的一部分,而是引用数据库的一部分,而且是可以变的。引用有不同的类型,如分支、标签和远程。它们在本质上是相似的,但有一些小的区别。目前,我们只考虑分支。一个分支是一个提交的指针。要创建一个分支,我们可以将提交的哈希值写到文件系统中。

echo 05c1cec5685bbb84e806886dba0de5e2f120ab2a > .git/refs/heads/master

现在我们有了一个分支主干,指向我们的第一个提交。要移动该分支,我们发出以下命令。

git upd-ref refs/heads/master 9918e46

这样我们就有了下面的图。

image.png

最后,我们现在可以读取我们文件的当前状态。

git show master:1.json 
{"id": 1, "name": "kenneth truyers"}

即使我们添加了新版本的文件和后续的树和提交,只要我们把分支指针移到最新的提交上,上述命令就会继续工作。

对于一个简单的键值存储来说,以上所有的事情似乎都相当复杂。然而我们可以将这些事情抽象化,这样客户端应用程序只需要指定分支和一个键。我将在另一篇文章中再来讨论这个问题。现在,我想讨论一下使用git作为NoSQL数据库的潜在优势和缺点。

数据效率

在存储数据方面,Git是非常高效的。如前所述,由于哈希的计算方式,相同内容的blobs只存储一次。你可以通过将一大堆内容相同的文件添加到一个空的 git 仓库中,然后检查 .git 文件夹的大小与磁盘上的大小来验证这一点。你会发现,.git文件夹小了不少。

但这还没完,git对树也是如此。如果你修改了一个子树上的文件,git 只会创建一个新的子树,而只是引用其他未受影响的树。下面的例子显示了一个指向有两个子文件夹的层次结构的提交。

image.png

现在如果我想替换4658ea84这个blob git只会替换那些被修改过的项目 而保留那些没有被修改过的项目作为参考。将blob替换成不同的文件并提交更改后,图表看起来如下(新对象用红色标记)。

image.png

正如你所看到的,git只替换了必要的项目,并引用了已经存在的项目。

虽然git在引用现有数据的方式上非常高效,但如果每一个小的修改都会导致一个完整的副本,我们仍然会在一段时间后得到一个巨大的仓库。为了缓解这种情况,有一个自动收集垃圾的过程。当 git gc 运行时,它会查看你的 blobs。在可能的情况下,它会删除 blobs,并存储一份基础数据的副本,以及 blob 每个版本的 delta。这样一来,git 仍然可以检索到 blob 的每个独特版本,但不需要多次存储数据。

版本化

你可以免费得到一个完整的版本系统。有了版本化,也就有了永远不删除数据的优势。我在SQL数据库中看到过这样的例子。

id | name | deleted 1 | kenneth | 1

对于这样的简单记录来说是可以的,但这通常不是全部。数据可能会对其他数据有依赖性(是否是外键是一个实现细节),当你想恢复它的时候,你有可能无法孤立地进行恢复。有了git,只需要把你的分支指向不同的提交,就可以在数据库层面而不是记录层面恢复到正确的状态。

我见过的另一种做法是这样的。

id | street | lastUpdate 1 | town rd | 20161012

这种做法更没有什么用处:你知道它被更新了,但没有任何信息说明实际更新了什么,以前的值是多少。每当你更新数据时,你实际上是在删除数据并插入新的数据。旧的数据就会永远丢失。有了git,你可以在任何一个文件上运行git log,看看有什么变化,谁改的,什么时候改的,为什么改的。

git工具

Git拥有丰富的工具集,你可以用它们来探索和操作你的数据。它们中的大多数都集中在代码上,但这并不意味着你不能用它们来处理其他数据。以下是我所能想到的一些工具的非详尽概述。

在基本的git命令中,你可以。

  • 用git diff找到两个提交/分支/标签/... 之间的确切变化
  • 使用 git bisect 来发现什么时候因为数据的改变而停止工作了
  • 使用git钩子获得自动变更通知,并建立全文索引,更新缓存,发布数据, ...
  • 还原、分支、合并、......。

还有一些外部工具。

  • 你可以使用Git客户端来可视化你的数据,并对其进行探索。
  • 你可以使用拉动请求,比如GitHub上的拉动请求,在合并数据之前检查数据的变化。
  • Gitinspector:对git仓库的统计分析

任何能和git一起工作的工具,都能和你的数据库一起工作。

NoSQL

因为它是一个键值存储,所以你可以获得NoSQL存储的通常优势,比如无模式数据库。你可以存储任何你想要的内容,甚至不需要是JSON。

连接性

Git可以在分区的网络中工作。你可以把所有的东西都放在U盘上,当你没有连接到网络的时候保存数据,然后当你恢复在线时推送和合并。这也是我们在开发代码时经常使用的优势,但对于某些用例来说,它可能是救命稻草。

事务

在上面的例子中,我们把每一个改动都提交到一个文件中。你不一定要这样做,你也可以将各种变化作为一个单一的提交。这样可以方便以后原子式地回滚这些变化。

长时间的事务也是可能的:你可以创建一个分支,提交几个变化,然后合并它(或丢弃它)。

备份和复制

对于传统的数据库,通常要建立一个完整备份和增量备份的计划,有点麻烦。由于git已经存储了整个历史记录,所以永远都不需要做完整的备份。此外,备份只是执行git推送。而这些推送可以在任何地方进行,GitHub、BitBucket或者自带的git服务器。

复制也同样简单。通过使用git钩子,你可以设置一个触发器,在每次提交后运行git push。例如

git remote add replica git@replica.server.com:app.git 
cat .git/hooks/post-commit 

#!/bin/sh 
git push replica

这真是太棒了!我们应该从现在开始使用Git作为数据库。从现在开始,我们都应该把Git当做数据库来用

等一下 但也有一些缺点。

查询

你可以通过键来查询......仅此而已。唯一的好消息是,你可以在文件夹中结构你的数据,这样你可以很容易地通过前缀获得内容,但仅此而已。任何其他查询都是不允许的,除非你想做一个完整的递归搜索。这里唯一的选择是建立专门用于查询的索引。如果你不担心索引过时,你可以按计划进行,或者你可以使用git钩子在提交时立即更新索引。

并发性

只要我们在写blobs,并发性就没有问题。当我们开始写提交和更新分支时,问题就出现了。下图说明了当两个进程并发地试图创建一个提交时的问题。

image.png

在上面的案例中,你可以看到,当第二个进程修改树的副本时,它实际上是在一个过时的树上工作。当它提交树时,它将失去第一个进程所做的修改。

同样的故事也适用于移动分支头。在你提交和更新分支头之间,可能会有另一个提交进入。你有可能将分支头更新到错误的提交中。

唯一的办法是锁定从读取当前树的副本到更新分支头之间的所有写入。

速度

我们都知道git的速度很快。但那是在创建分支的情况下。当谈到每秒提交次数时,其实并没有那么快,因为你一直在向磁盘写东西。我们不会注意到这一点,因为通常我们在写代码的时候,每秒的提交次数并不多(至少我没有)。在我的本地机器上运行了一些测试后,我进入了大约110次/秒的提交极限。

Brandon Keepers在几年前的一个视频中展示了一些结果,他得到了大约90次/秒的提交,这似乎符合硬件进步可能带来的结果。

110提交/秒对于很多应用来说已经足够了,但并不是所有的应用都能达到。这也是我本地开发机器上的理论最大值,资源很多。影响速度的因素有很多种。

树的大小

一般来说,你应该优先使用很多子目录,而不是把所有的文档放在同一个目录中。这样可以使写入速度尽可能地接近最大值。原因是每次创建新的提交时,你都要复制树,对其进行修改,然后保存修改后的树。虽然你可能会认为这也会影响大小,但实际上并非如此,因为运行 git gc 会确保将其保存为 delta,而不是两个不同的树。让我们来看一个例子。

在第一种情况下,我们有10. 000个blobs存放在根目录下。当我们添加一个文件时,我们复制包含10.000个项目的树,添加一个并保存它。由于树的大小,这可能是一个潜在的冗长操作。

image.png

在第二种情况下,我们有4层树,每层有10个子树,最后一层有10个blobs(101010*10=10.000个文件)。

image.png

在这种情况下,如果我们要添加一个blob,我们不需要复制整个层次结构,我们只需要复制通向blob的分支。下图显示了必须复制和修改的树。

image.png

因此,通过使用子文件夹,我们现在可以复制5棵有10个条目的树,而不是必须复制1棵有10.000个条目的树,这速度快了不少。你的数据增长越多,你就越想使用子文件夹。

将值合并到事务中

如果你需要每秒进行超过100次的提交,那么你很有可能不需要能够逐次回滚。在这种情况下,你可以不提交每一个变化,而是在一次提交中提交几个变化。你可以并发的写blobs,所以你有可能并发的写1000个文件到磁盘上,然后做1次提交把它们保存到仓库。这有缺点,但如果你想要原始速度,这是最好的方式。

解决这个问题的方法是给git增加一个不同的后端,不立即将内容刷新到磁盘,而是先写到内存数据库,然后异步刷新到磁盘。不过实现这个并不是那么容易。当我使用libgit2sharp测试这个解决方案连接到仓库时,我尝试使用Voron后端(它是开源的,还有一个使用ElasticSearch的变体)。这样一来,速度提高了不少,但你失去了用任何标准git工具检查数据的好处。

合并

另一个潜在的痛点是当你要合并不同分支的数据时。只要不存在合并冲突,其实这是一个相当愉快的体验,因为它可以实现很多不错的场景。

  • 修改数据,在数据上线前需要批准。
  • 在需要还原的实时数据上运行测试。
  • 在合并数据之前,先隔离工作

从本质上讲,你可以在开发中获得所有分支的乐趣,但在不同的层面上。问题是当存在合并冲突时。合并数据是相当困难的,因为你不一定能弄清楚如何处理这些冲突。

一个潜在的策略是,当你写数据时,只需将合并冲突按原样存储,然后当你读数据时,向用户展示差异,这样他们就可以选择哪一个是正确的。尽管如此,要正确地管理这一点可能是一项困难的任务。

结束语

Git在某些情况下可以很好地作为NoSQL数据库使用。它有它的位置和时间,但我认为它在以下情况下特别有用。

  • 你有分层的数据(因为它固有的分层性质)。
  • 你需要能够在不相连的环境中工作。
  • 你需要为你的数据建立一个审批机制(也就是你需要进行分支和合并

在其他情况下,它并不适合。

  • 你需要极快的写入性能
  • 你需要复杂的查询(虽然你可以通过提交钩子索引来解决)
  • 你有一个巨大的数据集(写入速度会进一步减慢)。

所以,你去了,这就是你如何使用git作为NoSQL数据库。让我知道你的想法!


通过www.DeepL.com/Translator(免费版)翻译

通过www.DeepL.com/Translator(免费版)翻译