CouchDB 实现原理

特性

  1. RESTFul API
  2. 基于文档存储,无表固定结构,数据之间没有关系范式要求
  3. 多版本并发控制模型
  4. 用户自定义查询结构(map/reduce)
  5. 增量索引更新机制
  6. 多master复制模式
  7. 用Erlang编写

CouchDB能够适应非常广 泛的应用场景,在某些偶尔连接网络的应用中,我们可以用CouchDB暂存数据,随后进行同步。也可以在Cloud环境中,作为大型的分布式的数据存储。 CouchDB提供了基于 HTTP的API的访问方式,这样,保证了所有的常见的语言都可以使用CouchDB。

底层存储结构

CouchDB是一个"面向文 档"的数据库,文档的格式是一个JSON字符串(也可包含二进制附件)。 底层结构是由一个"存储"(storeage) ,以及多个"视图索引"(view indexs)。 "储存"用来储存文件, "视图索引"用于查询处理。

CouchDB落实到最底层的 数据结构就是两类B+Tree 。

第一类 B+Tree,by_id_index (使用document的id为key)。常用来通过document的id来查找document的位置,实际上,他是指向的一个reversion列 表集合。

第二类B+Tree, by_seqnum_index (使用序列号来作为key,说得具体点就是记录最新的reversion来作为key) 。当document进行更新的时候,就会产生一个新的序列号。 (值得注意的是,所有更新操作都是一个串行的方式,因此序列号反映了序列的非同步更新)。 同步复制的追踪,数据的显示以及更新与它都密不可分。

一切皆追加

所有的更新操作(包括 document的创建,修改和删除)都是以在couch文件尾部追加的方式(即Append方式)进行。而不是修改现有的文件。在此之后, B+树节点也修改为指向新的文件的位置。修改操作实际上就是在现有的B+树的末尾附加一个新的文件。这反过来又引发修改父节点的B+树节点,造成一个新的 副本父节点...直到所有的方式回到根B+树节点。最后修改文件头以指向新的跟节点。

这意味着所有更新将触发1次写 入文件(除删除)和logN写入每个B +树节点的页面。 因此,复杂度为O ( logN )。

一切皆追加的操作提供了一个有 趣的MVCC (多版本并发控制)模型,因为该文件保存了所有以前的历史文件版本信息。 只要客户端持有先前根节点的B +树索引,它就可以得到的快照视图。即便是更新不断发生,客户将不会看到任何的最新变化。这种一致性快照在在线备份以及在线"瘦身"方面是非常有用的。锁机制和MVCC(多版本并发控制)的比较见文章最后补充!

值得注意的是,读操作是并行 的,写操作是串行的。 换句话说,在任何时候只有一个文件可以进行更新操作(但是,如果是写入附件的话,可以在一个文件中进行并行操作。)

GET document

当客户端向CouchDB的发出的HTTP的REST的GET请求,DBServer将做如下操作...

  1. 查找文件头,找到by_id B +树索引的根节点
  2. 在B +树中找出该文件的位置
  3. 阅读了该文件,并返回到客户端

PUT document(修改操作)

当客户端向CouchDB的发 出的HTTP的REST的PUT请求,DBServer将做如下操作...

  1. 查找文件头,找到by_id B +树索引的根节点
  2. 在B +树中找出该文件的位置
  3. 读取该文件。对比reversion,如果他们不匹配就抛出一个错误。
  4. 如果它们匹配,生成一个新的reversion。
  5. 寻找末尾的一个区域(region),看看该文件的容量是否可以容纳,如果不能就另外开辟一个region。
  6. 写文件(新reversion)到新的区域(region)
  7. 修改by_id B +树以指向新的文件位置
  8. 修改by_seqnum B +树添加新实体(新seqnum ) ,并删除旧的条目(旧seqnum ) 。

请注意, by_seqnum B +树索引总是指向最新版本,以前的修改会自动被覆盖。

PUT/POST document(创建)

当客户端向CouchDB的发 出的HTTP的REST的PUT请求,DBServer将做如下操作...

  1. 生成一个新的seqnum及一个新的文件编号和修订
  2. 寻找末尾的一个区域(region),看看该文件的容量是否可以容纳,如果不能就另外开辟一个region。
  3. 写文件(新reversion)到新的区域(region)
  4. 修改by_id B +树以指向新的文件位置
  5. 修改by_seqnum B +树添加新项目(新seqnum )

删除文件(修改)

当客户端向CouchDB的发 出的HTTP的REST的DELETE请求,DBServer将做如下操作...

  1. 查找文件头找到根节点的by_id B +树索引
  2. 在B +树找出叶节点以及该文件的位置
  3. 读取该文件。 对比修订,如果他们不匹配就抛出一个错误。
  4. 如果它们匹配,找出找到这个reversion。
  5. 生成一个新的reqnum
  6. 修改by_id B +树的reversion 历史,将这个路径的reversion标记为被删除
  7. 修改by_seqnum B +树添加新实体(新seqnum ) ,并删除旧的条目(旧seqnum )。

Online Compation(删除旧的Document)

作为一个一切皆追加的存储方 式,存储文件会随着时间的推移与日俱增。 因此,我们需要对其进行"瘦身",删除那些旧的Document数据。这个过程称为 Compaction。

打开一个新的存储文件
从by_seqnum B +树索引中找到最新reversion的document
复制document到新的存储文件中(在新的存储文件中自动更新相应的B +树索引)。

在Compation的过程, 数据库仍然可用,只是请注意,在Compation的时候,是通过遍历DBName.couch文件, 将最新的数据拷贝到一个DBName.compat文件中, 因此这个过程可能会耗费很大的存储空间,如果您在系统繁忙(主要是write)的情况下进 行Compation,可能会导致你的硬盘空间耗尽。推荐系统在一个写操作不是很繁忙的情况下进行Compation。

View Indexs

CouchDB支持类似数据库 的View的概念。所不同的是CouchDB是采用Map/Reduce的方式来表现的。(请注意,reduce语义与谷歌的Map/Reduce模型有 着很大的不同)。Map()是一个用户定义的函数,它用来将每个文件处理成中间结果。Reduce()是另外的一种用户定义的函数,它                     将Map()函 数所产生的中间结果进行收集汇总而生成最终结果。

Map()的中间对象和 Reduce()后的结果存储在View Indexs中。随着存储得到更新, 以前的结果也会随着更新。

每一个View的定义就是一个 Map函数和一个可选的Reduce函数。View存储在design Document中。

(map函数,必须)
function(doc) {
emit(null, doc);
}
(reduce函数,可选)
function (key, values, rereduce) {return sum(values);
}

请注意这里design Document和View Index是不同的。design Document保存的是view的定义,View Index保存的是针对某个Database进行View操作,产生的结果。

起初,View文件是空的 (View尚未创建) , 当查询执行的时候会触发下面一系列的处理。

1.CouchDB将遍历存储 文件的by_seqnum B +树索引。
2.在此基础上, CouchDB获得所有现有文件的最新reversion
3.CouchDB通过seqnum获取document,然后将每份document反馈到View Server 上用于进行Map操作。
4.View Server 调用map(doc)函数,遍历调用emit(key,value),一个中间实体就是被这么创建出来的。
5.最后,将map(doc)函数所产生和结果集返回给CouchDB 。
6.CouchDb将这些实体加入到B +树索引,Key = emit_key + doc_id 。 遍历每一个B +树的叶节点。
7.CouchDB将View Servier所获得的Map()结果集进行"Reduce" 操作。
8.View Server 调用Reduce(key,value)函数。
9.将reduce计算的结果返回给CouchDB
10.CouchDb将更新叶子上的B +树节点,将其指向reduce的值。
11.在此之后, CouchDb移动到父节点的叶子上的B +树节点。 遍历每个B +树父节点, CouchDB将相应reduce子节点的数据发送到View Server,再次进行reduce的操作(rereduce)。
12.View Server再次调用 reduce(key, value)函数。
13.最后, 再将rereduce计算出的结果返回给CouchDB 。
14.CouchDB将更新父B +树节点,将其指向rereduce的值。

CouchDB继续上升一个等 级,并重复计算rereduce结果。 最后,直到更新完根节点的rereduce的结果为止。

当处理完成后,View Index 看上去就是下面这种样子

增量视图更新

CouchDB更新view indexes采用懒加载和增量的方式。 也即是说,当文件被更新了,CouchDB不会立即刷新view index,直到下一次查询到来的时候才进行更新。

CouchDB刷新index 采用如下的方式:

  1. CouchDB将遍历存储文件的by_seqnum B +树索引。
  2. CouchDB从最后一次视图查询的结果中抽取出的所有改变的文档,然后从新进行map操作,并取得一个Map结果集合。
  3. CouchDb更新Map结果到B +树索引,一些叶子B +树节点将被更新。
  4. 对于那些已经更新了的叶子B +树节点, CouchDB重新发送Map生成的所有中间结果到View Server 进行Reduce的操作。 然后reduce的结果保存到B +树节点。
  5. 所有涉及到这个叶子B +树节点都需要更新, CouchDB需要再次执行Reduce计算,并且更新相应的父节点。 直到更新完根节点为止。

由于一致的快照的特性,在进行 数据更新操作的过程中,view 查询需要花费较长时间。 查询需要等待索引更新完成后才能看到一致的结果。 还有一个选择(开发中) ,立即返回一个陈旧的副本,这对客户来说是可以容忍的。


补充:

悲观锁(即锁机制)和MVCC对比

上面介绍了悲观锁和MVCC的基本原理,但是对于它们分别适用于什么场合,不同的场合下两种机制优劣具体表现在什么地方还不是很清楚。这里我就对一些典型的应用场景进行简单的分析。需要注意的是下面的分析不针对分布式,悲观锁和MVCC两种机制在分布式系统、单数据库系统、甚至到内存变量各个层次都存在。

### 场景1:对读的响应速度要求高

有一类系统更新特别频繁,并且对读的响应速度要求很高,如股票交易系统。在悲观锁机制下,写会阻塞读,那么当有写操作时,读操作的响应速度就会受到影响;而MVCC不存在读写锁,读操作是不受任何阻塞的,所以读的响应速度会更快更稳定。

### 场景2:读远多于写

对于许多系统来讲,读操作的比例往往远大于写操作,特别是某些海量并发读的系统。在悲观锁机制下,当有写操作占用锁,就会有大量的读操作被阻塞,影响并发性能;而MVCC可以保持比较高且稳定的读并发能力。

### 场景3:写操作冲突频繁

如果系统中写操作的比例很高,且冲突频繁,这时就需要仔细评估。假设两个有冲突的业务L1和L2,它们在单独执行是分别耗时t1,t2。在悲观锁机制下,它们的总时间大约等于串行执行的时间:

T = t1 + t2

而在MVCC下,假设L1在L2之前更新,L2需要retry一次,它们的总时间大约等于L2执行两次的时间(这里假设L2的两次执行耗时相等,更好的情况是,如果第1次能缓存下部分有效结果,第二次执行L2耗时是可能减小的):

T’ = 2 * t2

这时关键是要评估retry的代价,如果retry的代价很低,比如,对某个计数器递增,又或者第二次执行可以比第一次快很多,这时采用MVCC机制就比较适合。反之,如果retry的代价很大,比如,报表统计运算需要算几小时甚至一天那就应该采用锁机制避免retry。

从上面的分析,我们可以简单的得出这样的结论:对读的响应速度和并发性要求比较高的场景适合MVCC;而retry代价越大的场景越适合悲观锁机制。

总结

本文介绍了一种基于多版本并发控制(MVCC)思想的Conditional Update解决分布式系统并发控制问题的方法。和基于悲观锁的方法相比,该方法避免了大粒度和长时间的锁定,能更好地适应对读的响应速度和并发性要求高的场景。

CouchDB 实现原理相关推荐

  1. 2020年“有史以来”全网最全1309道BAT大厂java面试题,mongodb原理知识

    Java中异常分为哪两种? 异常的处理机制有几种? 如何自定义一个异常 try catch fifinally,try里有return,finally还执行么? Excption与Error包结构 T ...

  2. HBase数据库原理解析

    文章目录 1.HBase 数据库介绍 1.1产生背景 1.2简介 1.3表结构逻辑视图 1.3.1行键(RowKey) 1.3.2列簇(Column Family) 1.3.3时间戳(TimeStam ...

  3. 区块链技术系列(3)- Fabric基础架构原理

    前言 对于区块链方面多技术,我还是建议大家多看英文文档,多利用Google来搜索技术文章. 怎么搭建自己专属V-P-N来访问Google,请看我之前发的文章: 新人如何快速搭建自己的个人网站以及自己专 ...

  4. 浅析SSRF原理及利用方式

    原文链接:https://www.anquanke.com/post/id/145519 漏洞简介 SSRF(Server-side Request Forge, 服务端请求伪造) 通常用于控制web ...

  5. 计算机端口原理与作用

    在IP地址3种主要类型里,各保留了3个区域作为私有地址,其地址范围如下: A类地址:10.0.0.0-10.255.255.255 B类地址:172.16.0.0-172.31.255.255 C类地 ...

  6. 【大数据存储技术】第7章 MongoDB 的原理和使用

    文章目录 第7章 MongoDB 的原理和使用 7.1 概述 7.2 MongoDB 技术原理 7.2.1 文档和集合 7.2.2 分片机制和集群架构 7.2.3 CouchDB 简介 7.3 安装配 ...

  7. 大数据技术原理与应用(1-6)-TYUT

    知识很多很碎,以下内容只是对重点内容的罗列,许多地方还需要大家翻书,看图理解.另外关于大题部分,由于精力有限就不详写了,大家可以根据实验内容,找重点部分记忆.比如SSH无密码登录指令:ssh-keyg ...

  8. 04735数据库系统原理(知识点快速记忆)

    一 概述 数据管理系统DBMS 数据库系统DBS 简述DBA的的主要职责 数据模型的分类 三级模式.两级映像 数据独立性 简述外模式.模式映像及如何保证数据的逻辑独立性 简述物理数据独立性(模式/内模 ...

  9. Redis 安装使用以及原理攻略

    Redis介绍 什么是Redis? Redis是用C语言开发的一个开源的高性能键值对(key-value)内存数据库. 它提供五种数据类型来存储值:字符串类型.散列类型.列表类型.集合类型.有序集合类 ...

最新文章

  1. .NET(C#)连接各类数据库
  2. 【渝粤教育】 国家开放大学2020年春季 1050金融理论前沿课题 参考试题
  3. ubuntu不会自动休眠_关机、睡眠、休眠有啥区别?微软说非特殊情况不要关机
  4. iar环境下c语言编程,c语言_源代码-iar环境配置.pdf
  5. python继承的特点_python面向对象三大特性之继承
  6. 我公司有个统计学的985应届(硕士)从事数据分析岗位
  7. Nodejs实时通讯 在线聊天室(Socket.io)_收藏
  8. Spring Boot学习记之Maven
  9. Unity游戏开发:对话系统的实现
  10. 上证50基金有哪些_哪一只上证50指数基金最值得关注?
  11. 计算机网络密码用户名是什么,宽带连接的用户名和密码是什么
  12. 部落优势服务器,魔兽怀旧服联盟优势服有哪些?怀旧服联盟优势服务器一览
  13. LightOJ - 1395
  14. 系统业务逻辑书籍_企业应该如何建立自己的分销系统和分销团队
  15. Linux 安装红帽 RHEL 8 详细图文教程
  16. getValue()方法 java_【Java 】实用方法
  17. C语言库函数strstr、strch
  18. CX51 用户手册----MDU_F120伪指令
  19. 微信转盘抽奖前端源码(三):移动端浏览器兼容性(12个奖品,指针开始时指向奖品)
  20. Python3 多线程

热门文章

  1. Go核心开发学习笔记(廿九) —— 反射
  2. 软件工程——需求分析(生存周期),需求规格说明书,数据流图
  3. Temporal-Relational CrossTransformers for Few-Shot Action Recognition
  4. 黑匣子解密要多久_解密“黑匣子”:黑匣子发展历史及如何定位打捞
  5. ckeditor3.0.1上传图片功能(.net版本)
  6. 循环神经网络综述 -语音识别与自然语言处理的利器
  7. php 图片批量加水印
  8. 工业大数据平台解决方案的应用价值
  9. 软件构造Lab2实验总结
  10. 基于STM32的蓝牙密码锁