CouchDB Views

CouchDB 视图简介及增量更新视图的方法

载自:https://www.ibm.com/developerworks/cn/opensource/os-cn-couchdb-view-change/ 。有关CouchDB国内的资源真的少之又少,我又不喜欢看英文,好不容易找到了这篇文章,写的真的很棒,所以转载。

郭 君, 高 云鹤, 和 林 宜谦 2016 年 4 月 18 日发布

简介

CouchDB 是一种 NoSQL 数据库,数据以 JSON 文档的形式存储在数据库中,每个文档都可以拥有不同的结构,文档之间没有关联性,这给数据的存储提供了极大的灵活性。然而,在真实的应用中,我们仍然需要一种机制去筛选、组织这些无关联、无结构的数据并将它们以不同的方式呈现出来。为了解决这个问题,CouchDB 引入了视图(view)的概念,它实现了类似 SQL 中 select 的功能,使得用户可以灵活快速地查询数据。

同时 CouchDB 中数据的 index 也要通过构建视图(view)来实现,但随着数据量的增大,更新视图的效率也将逐之降低。

本文将首先介绍视图(view)的基本概念和用法,之后介绍一种可行的方法来增量地更新视图。

View 的概念和用途

在关系型数据库中,视图是从一个或多个表中导出的表,是一种虚表,即视图所对应的数据不进行实际的存储,数据库中只存储视图的定义,在对视图的数据进行操作时,系统根据视图的定义去操作与视图相关联的基本表。

在 CouchDB 中,视图的功能与关系型数据库中的类似。也是用于动态地聚合和呈现同一个数据库中的数据,且一个数据库中可以定义多个不同的视图。不同的是,CouchDB 中数据的 index 也是通过创建视图来完成的。当视图创建好后,系统也将在后台自动产生并保存视图所定义的数据 index 文件。可以说,CouchDB 中对数据的查询和索引在查询视图的过程中同时完成,对数据的查询也是一个索引的过程,这就是为什么当视图建立好后,每次查询都十分高效的原因。CouchDB 视图的用途可概括为如下几个方面:

  • 过滤出符合特定要求的文档并以一定的顺序呈现它们
  • 对任意字段建立 index
  • 使用已建立的 index 展现数据之间的关系或进行统计

Design documents

CouchDB 中,视图在一种特殊的叫做“design documents”的文档中定义,该文档和普通的数据文档一样保存在数据库中且可以像普通的文档一样在两个不同的数据库直接复制。视图要完成的文档过滤和索引描述在 design documents 中的 map 和 reduce 函数中定义。顾名思义,map 函数实现对数据的过滤或查询,reduce 函数实现对 map 函数返回结果的聚合。CouchDB 中的视图均通过 JavaScript 进行保存且对 design documents 的写法有严格的要求,其大致框架如下:

{
 "_id": "_design/yourDesignName",
 "_rev": "6-5f3a39495b5cd3ed3c40b5196ffb1972",
 "views": {
 "yourView1": {
 "map": "function(doc){ 
//some condition –your code
emit(key,value);
}",
 "reduce": "function(keys, values){
return yourCode;                                     
}"
 },
 " yourView2": {
 "map": "function(doc){
//some condition –your code
emit(key,value);
                 }"
 }
    “yourView3”{...}
...
 }
}

几点说明:

  • “_id”字段的值必须为"_design/yourDesignName"这样的格式
  • 一个 design documents 可以定义任意数量的 view
  • View 由 map 和 reduce 函数组成,也可以只包含 map 函数
  • Map 函数由单一的参数 doc 和内建函数 emit 组成,emit 函数有两个参数,key 和 value。emit(key,value) 用来构建视图的结果,视图的返回结果以 key 来排序
  • Reduce 函数的参数由 keys 和 values 组成,values 即为 map 函数的返回值

接下来我们举例详细说明。为了方便,我们假定已经建立了 test 数据库且包含多条如图 1 所示的数据:

img001

图 1. 数据样例

这里我们用两个 view 来展示常用的使用场景,如图 2 所示:

img002

图 2 . 一个包含两个视图的 design documents

在该 design documents 中定义了两个视图“testView1”和“testView2”,testView1 视图用来建立 index 并对 index 的结果进行统计,testView2 视图用来抽取符合条件的文档,我们分别来解释:

  • “testView1”中包含一个 map 函数和一个 reduce 函数,在 map 函数的 emit 中,以 doc.name 作为 key,1 作为 value。意思是在文档的 name 字段上建立 index,且每个文档都返回 1。 这也是 CouchDB 中给字段建立 index 的常用方法,即要在哪个字段上添加 index,就在 map 函数的 emit 中将该字段写在第一个参数 key 的位置。第一个参数可以为单一字段,也可以为多个字段的列表,例如 [doc.name,doc.hometown] 这种形式。这里 map 函数的返回值是 1,reduce 中的 sum 函数将所有的 1 累加,实现了对 map 返回值的统计。
  • “testView2”中只有 map 函数,此时将 hometown 作为 index,并判断 hometown 为“Shenzhen”的文档,emit 中第二个参数写为 doc 的意思是列出符合条件的文档所有字段,如果只想列出文档的部分字段,则可写为 doc.yourColumn 或 [doc.column1,doc.column2,...] 的形式。TestView2 实现了对所有 hometown 字段为 Shenzhen 的查询。

以上的两个视图定义就是 CouchDB 中视图的两个常见使用场景。

View 的实现原理

上面的章节中,我们定义好了 design documents,但此时仅仅有了视图的定义,数据库中视图的结果并没有产生。CouchDB 中视图的结果以 B-Tree 来保存,B-Tree 提供非常快速的查询模式,关于 B-Tree 的具体实现原理这里不再涉及,读者请自行搜索。

视图结果的产生过程就是 B-Tree 建立的过程,当用户第一次查询视图时,CouchDB 会根据视图的定义,扫描数据库中每一个文档并建立对应的 B-Tree。一旦 B-Tree 建立,之后对视图的查询将不再逐一扫描每一个文档,而仅仅是读取 B-Tree 的结果。在读取 B-Tree 结果时,如果发现在上次查询视图后又有文档的写操作(增删改查),那么 CouchDB 会先把上次的写操作跟当前 B-Tree 结果合并,然后再返回。由于 B-Tree 的结构,CouchDB 可以迅速地在 B-Tree 结果中找到需要更新的文档位置从而更新 B-Tree 对应的节点。正是因为 B-Tree 的这种结构,使得在 CouchDB 中,一旦视图结果被建立,之后的每次查询都会非常高效。

读时更新带来的问题

然而这种机制也带来两个问题,一是当数据量非常大时,第一次查询视图将会非常耗时,二是如果两次查询视图期间有太多的写操作,那么也将导致写操作后的第一次查询速度的大大降低,因为 CouchDB 的视图总是在写操作后的第一次查询视图时进行视图更新,即读时更新。

该问题在一些对实时性要求比较高的场景尤为突出,例如将 CouchDB 作为某些社交网站的存储媒介,由于社交网站用户数量可能非常庞大,数据库可能每时每刻都在发生数据的写操作,例如每隔一段时间都要写入几十万条数据。然而,大量的写操作本身就需要一定的时间来完成,由于 CouchDB 视图的读时更新机制,如果等所有写操作完成才进行视图的第一次查询,那么查询视图时对几十万条数据的再次遍历又将花去大量时间,整个过程将会非常耗时。如图 3 所示:

img003

图 3. 写入数据后更新视图

一个可行的解决思路是,如果能在写操作的同时就进行视图的更新,让写操作和视图更新同时进行。当写操作完成之后视图也随即更新完成,视图的更新工作在写操作的这段时间内增量的进行,这样就可以大大提高整个过程的效率。如图 4 所示:

img004

图 4. 写操作的同时增量的更新视图

CouchDB 中提供了一个叫做_change 的功能,接下来我们介绍如何使用该功能来达到增量更新视图的目的。

使用_change 增量的更新视图

CouchDB 支持一个叫_change 的功能,它提供了一种实时监听数据库中数据变更的操作,并会以时间顺序返回数据变更信息列表。类似订阅操作,它持续地监听数据库,并将所有变更操作推给订阅了此消息的客户端,常用于构建类似消息通知和推送的服务。

因此可以在数据写入 CouchDB 时使用_change 来监测写入的数据条数,当达到一定的写入条数时(例如 2000 条)就触发一次视图的更新操作。这样每当有一定数量的新数据被写入,就会触发视图的更新操作,从而实现了图 4 中所示的写入数据的同时增量地更新视图的效果。在 CouchDB-Python 库中提供了_change 的接口,使用 Python 脚本可以很容易地实现这一功能。

此外,由于_change 将一直保持一个与 CouchDB 服务器的连接直到人为终止程序的运行,因此在数据的写入操作终止时还需要利用一个 shell 脚本来关闭_change 的进程以防止长连接对系统资源的长期占用。整个流程如图 5 所示,monitor 即为使用_change 功能的 Python 程序:

img005

图 5. 增量更新视图流程图

在 shell 脚本中,首先调用 monitor.py 脚本来监听数据写入操作,之后调用写入数据的程序,当数据写入完成后关闭 monitor.py,其大致代码如下:

python2.7 /yourPath/monitor.py $dbName >> log.txt &
for file in "$inputDir"/*
do
 python2.7 /yourPath/insertFileToDB.py $file $dbName >> log.txt
 echo "Finished inserting "$file" to $dbName"
done
ps -ef | grep monitor |grep -v grep |awk '{print $2}' |xargs kill -9

由于数据插入过程因数据量不同可能会花费几分钟至几十分钟的时间,因此最好使用

nohup ./thisShell.sh &来启动该 shell 脚本,使其即使在终端关闭的情况下仍能在后台执行。同样,程序第一行使用“&”让 monitor.py 在后台运行。

monitor.py 的代码如下:

import CouchDB;
import sys
import os
 
if __name__ == '__main__':
 try:
 server = CouchDB.Server('http://localhost:5984/')
 except Exception, e:
 sys.exit(1)
  
 DbName="test"
  
 try: 
 DB = server[DbName]
 except Exception, e:
 sys.exit(1)
  
 changeRS = DB.changes(feed='continuous',heartbeat='50000',include_docs=True)
 counter=0
 for each in changeRS:
 counter+=1
 if (counter > 2000):
 len(DB.view('TestDesignDoc/testView1'))
 print "update view"
 counter=0

其中 ch = DB.changes(feed='continuous',heartbeat='50000',include_docs=True) 可以持续监听数据库中数据的变更并将变更结果以列表形式返回到 ch。for 循环遍历该变更结果,当计数器 counter 大于 2000 时,使用语句 len(DB.view('TestDesignDoc/testView1')) 获取视图的总长度,即是对视图的一次更新。

总结

本文给出的增量更新 CouchDB 视图的方法十分简单,使用场景也有一定的局限,仍然存在改进的空间。如果读者有更好的关于优化 CouchDB 性能的方法欢迎与作者交流。

相关主题

加载评论