返回介绍

5.2 NoSQL

发布于 2023-05-19 13:36:37 字数 20761 浏览 0 评论 0 收藏 0

说起 NoSQL,这里并不是指某种数据库软件叫这个名字1。所谓 NoSQL,是一个与象征关系型数据库的 SQL 语言相对立而出现的名词,它是包括键 - 值存储在内的所有非关系型数据库的统称。不过,关系型数据库在很多情况下还是非常有效的,因此有人批判 NoSQL 这个词中所体现出的“不再需要 SQL”这个印象过于强烈,主张应该将其解释为“Not Only SQL”(不仅是 SQL)(图 1)。

1 我查了一下,真有一种数据库软件叫 NoSQL,不过这次我们的话题并不是指这个软件。(原书注)

图 1 NoSQL 数据库

属于 NoSQL 类的数据库,主要有 ROMA(Rakuten On-Memory Architecture)这样的键 - 值存储型数据库,以及接下来要介绍的 MongoDB 这样的面向文档数据库等。

RDB 的极限

在大规模环境中,尤其是作为大流量网站的后台,一般认为关系型数据库在性能上存在极限,因为关系型数据库必须遵守 ACID 特性。

ACID 是 Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)和 Durability(持久性)这四个单词首字母的缩写。

所谓 Atomicity,是指对于数据的操作只允许“全部完成”或“完全未做改变”这两种状态中的一种,而不允许任何中间状态。因为操作无法进一步进行分割,所以用了“原子”这个词来表现。

所谓 Consistency,是指数据库的状态必须永远满足给定的条件这一性质。当某个事务无法满足给定条件时,其执行就会被取消。

所谓 Isolation,是指保持原子性的一系列操作的中间状态,不能由其他事务进行干涉这一性质,由此可以保持隔离性而避免对其他事务产生影响。

所谓 Durability,是指当保持原子性的一系列操作完成时,其结果会被保存并且不会丢失这一性质。

当数据量和访问频率增加时,ACID 特性就成了导致性能下降的原因,因为随着数据量和访问频率的增加,维持 ACID 特性所带来的开销就会越来越明显。

例如,为了保持数据的一致性,就需要对访问进行并发控制,这样则必然会导致能接受的并发访问数量下降。如果将数据库分布到多台服务器上,则为了保持一致性所带来的通信开销也会导致性能下降。

当然,如果以适当的方式将数据库分割开来,从而在控制访问频率和数据量方面进行优化的话,在一定程度上可以应对这个问题。在大规模环境下使用关系型数据库,一般有水平分割和垂直分割两种分割方式。

所谓水平分割,就是将一张表中的各行数据直接分割到多个表中。例如,对于像 mixi 2这样的社交化媒体(SNS)网站,如果将用户编号为奇数的用户信息和编号为偶数的用户信息分别放在两张表中,应该会比较有效。

2 mixi:http://mixi.jp/,是日本最大的社交网站。

相对地,所谓垂直分割就是将一张表中的某些字段(列)分离到其他的表中。用 SNS 网站举例的话,相当于按照“日记”、“社区”等功能来对数据库进行分割。

通过这样的分割,可以对单独一个关系型数据库的访问量和数据量进行控制。但是这样做,维护的难度也随之增加。

NoSQL 数据库的解决方案

NoSQL 之所以受到关注,就是因为它可以成为解决关系型数据库极限问题的一种方案。和关系型数据库相比,NoSQL 数据库具有以下优势(图 2):

图 2 NoSQL 的优点

· 限定访问数据的方式

在大多数 NoSQL 数据库中,对数据访问的方式都被限定为通过键(查询条件)来查询相对应的值(查询对象数据)这一种。由于存在这样的限定,就可以实现高速查询。而且,大多数 NoSQL 数据库都可以以键为单位来进行自动水平分割。

此外,也有像 memcached 这样不永久保存数据,只是作为缓存来使用的数据库。这也算是一种对数据访问方式的限定吧。

· 放宽一致性原则

要保持大规模数据库,尤其是分布式数据库的一致性,所需要的开销十分显著。因此大多数 NoSQL 数据库都遵循“BASE”这一原则。

所谓 BASE,是 Basically Available、Soft-state 和 Eventually consistent 的缩写,ACID 无论在任何情况下都要保持严格的一致性,而实际上数据不一致并不会经常发生,因此 BASE 比较重视可用性(Basically Available),但不追求状态的严密性(Soft-state),且不管过程中的情况如何,只要最终能够达成一致即可(Eventually consistent)。

如果遵循 BASE 原则,那么用于保持一致性的开销就可以得到控制,而标榜 ACID 的关系型数据库则很难做出这样的决断。

形形色色的 NoSQL 数据库

NoSQL 数据库只是一个统称,其中包含各种各样的数据库系统。大体上,可以分为以下三种:

· 键 - 值存储数据库

· 面向文档数据库

· 面向对象数据库

键 - 值存储是一种让键和值进行关联的简单数据库,查询方式基本上限定为通过键来进行,可以理解为在关系型数据库中只能提供对“拥有特定值的记录”进行查询的功能,而且还是有限制的。在 UNIX 中从很早就提供的 DBM 这种简单数据库,从分类上来看也可以算作键 - 值存储,但是在 NoSQL 这个语境中,所谓键 - 值存储一般都指的是分布式键 - 值存储系统。符合这样条件的键 - 值存储数据库包括“memcached”、“ROMA”、“Redis”、“TokyoTyrant”等。

所谓面向文档数据库,是指对于键 - 值存储中“值”的部分,存储的不是单纯的字符串或数字,而是拥有结构的文档。和单纯的键 - 值存储不同,由于它可以保存文档结构,因此可以基于文档内容进行查询。

举个例子,一张会员清单包括姓名、地址和电话号码,现在要从中查找一个名字叫“松本”的会员。也许乍看之下这和关系型数据库的应用方式是一样的,但是不同之处在于,在面向文档数据库中,对于存放会员信息的文档来说,每个会员的文档结构可以是不同的。因此要查找名字叫“松本”的会员,实际上相当于对“具备名字这个属性,且该属性的值为松本的文档”进行查询。这种情况下的文档,通常采用的是 XML(eXtended Markup Language)和 JSON(JavaScript Object Notation)格式。面向文档数据库包括 CouchDB、MongoDB 以及各种 XML 数据库等。

所谓面向对象数据库,是将面向对象语言中的对象直接进行永久保存,也就是当计算机断电关机之后对象也不会消失的意思。键 - 值存储和面向文档数据库给人的感觉还像是个数据库,但大多数面向对象数据库看起来只是一个将对象进行永久保存的系统而已。当然,面向对象数据库也提供对对象的查询功能。面向对象数据库的例子有 Db4o、ZopeDB、ObjectStore 等。

在我跳槽到现在的公司之前,经常使用 ObjectStore。那时候的工作内容是用 C++ 和 ObjectStore 编写一个 CAD 软件,真是怀念啊。说起来,那个时候 ObjectStore 还不支持分布式环境,对于跨越多数据库创建对象的功能,以及对不再使用的对象进行回收的分布式垃圾回收功能等,都是靠自己的力量实现的,不知道现在是不是有了正式的支持呢。

从“非关系型数据库”的角度来看,在这里我们暂且将面向对象数据库也算作是 NoSQL 的一种,至少从我(有些过时)的经验来说,面向对象数据库的主要目的,是提升一些数据结构比较复杂的小规模数据库的访问速度,而和其他 NoSQL 数据库相比,在可扩展性方面并不是很擅长。

面向文档数据库

下面我们来介绍一下面向文档数据库。所谓面向文档数据库,可以理解为是将 JSON、XML 这样的文档直接进行数据库化的形式,其特点包括:不需要 schema(数据库结构定义),支持由多台计算机进行并行处理的“水平扩展”等。

1. CouchDB

CouchDB 可以说是面向文档数据库的先驱。CouchDB 的特点是 RESTful 接口以及采用 Erlang 进行实现。

CouchDB 提供了遵循 REST(Representational State Transfer,表征状态转移)模型的接口,因此,即便没有特殊的客户端和库,使用 HTTP 也可以对数据进行插入、查询、更新和删除操作。和关系型数据库不同,其中每条数据不必拥有相同的结构,可以各自拥有一些自由的元素。在 CouchDB 中,是通过 JSON 来对记录进行描述的。

此外,在 CouchDB 中,一部分逻辑可以用 JavaScript 编写并插入到数据库中,从整体上看,数据库和应用程序之间的区别并不是那么明确。大多数人都习惯于“数据库负责数据,应用程序负责逻辑”,但此时也许需要让自己从这种模式中跳出来。

出人意料的是,像数据表的连结(Join)之类,在传统数据库中通过 SQL 可以轻松完成的查询,在 CouchDB 中是做不到的。因此用惯了传统关系型数据库的人可能会觉得四处碰壁。但是,如果能够完全运用 CouchDB 的功能,应用程序的设计可以变得十分简洁。

这种数据库是用 Erlang 来实现的,这一点也很值得关注。Erlang 是一种为并行计算特别优化过的函数型语言,分布式计算和并行计算方面的程序设计一直是它的强项,因此在 CouchDB 这样需要通过多台机器的分布和协调应对大量访问的场景中,应该说能够充分发挥 Erlang 的性能。

2. MongoDB

和 CouchDB 相比,MongoDB 大概更接近传统的数据库。MongoDB 的宣传口号是 Combining the best features of document databases, key-value stores, and RDBMSes,即要结合(像 CouchDB 这样的)文档数据库、键 - 值存储数据库和关系型数据库的优点,这真是个颇具挑战的目标。

MongoDB 除了不具备事务功能之外,确实提供了和关系型数据库非常接近的易用性。此外,它还为 C++、C#、JavaScript、Java、各种 JVM 语言、Perl、PHP、Python、Ruby 等语言提供了访问驱动程序,这一点也非常重要。有了这样的支持,在语言的选择上也就没有什么顾虑了。

MongoDB 的安装

如果你所使用的操作系统发行版本中提供了 MongoDB 的软件包,那么安装就非常容易了。在 Debian 中该软件包的名字叫做 mongodb。

即便没有提供软件包,安装它也并非难事。只要访问 MongoDB 官方网站的下载页面:http://www.mongodb.org/downloads,找到对应的二进制包并下载就可以了。提供官方预编译版本的系统平台如表 1 所示。

表1 MongoDB提供预编译版本的系统平台

Mac OS X 32位

Mac OS X 64位

Linux 32 位

Linux 64 位

Windows 32位

Windows 64位

Solaris x86

Solaris 64 位

我选用的是 Linux 32 位版本。将下载好的 tar.gz 文件解压缩后,其目录结构如下:

GNU-AGPL-3.0(许可协议)
README(说明文件)
THIRD-PARTY-NOTICES(第三方依赖关系信息)
bin/(二进制文件)
include/(头文件)
lib/(库文件)
MongoDB 的许可协议是 GNU-AGPL-3.0。AGPL 这种协议可能大家没怎么听说过,它是 AFFERO GENERAL PUBLIC LICENSE 的缩写,简单讲,基本条款和 GPL 是差不多的,区别只有一点,就是在该软件是通过网络进行使用的情况下,也需要提供源代码。在用于商业用途的情况下,如果不想公开源代码,貌似也可以购买商用许可。

bin 目录中包含了 MongoDB 的数据库服务器、客户端、工具等可执行文件。只要将这些文件复制到 /usr/bin 等 Path 能搜索到的目录中就可以完成安装了。如果需要自行编译客户端和驱动程序的话,还需要安装 include 目录中的头文件和 lib 目录中的库文件。

如果没有和你所使用的操作系统或 CPU 相对应的预编译版本,则需要下载源代码自行编译。不过,MongoDB 所依赖的库有很多,准备起来也有点麻烦。如果要在 Ubuntu 下用源代码进行编译,可以参考这里的资料(英文):http://www.mongodb.org/display/DOCS/Building+for+Linux

接下来我们需要用 Ruby 来访问 MongoDB,因此还需要安装 Ruby 的驱动程序。用 RubyGems 就可以轻松完成安装。RubyGems 是为 Ruby 的各种库和应用程序设计的软件包管理系统,使用起来非常方便。如果你还没有安装 RubyGems 的话,趁这个机会赶紧安装吧。在 Debian 或 Ubuntu 中,输入下列命令进行安装:

$ sudo apt-get install ruby rubygems
用 RubyGems 来安装 MongoDB 的 Ruby 驱动程序,可以输入下列命令:

$ sudo gem install mongo

启动数据库服务器

启动数据库服务器的命令是 mongod,作为参数需要指定数据库存放路径以及 mongod 监听连接的端口号,默认的端口号为 27017。指定数据库路径的选项为“--dbpath”,指定端口号的选项为“--port”。例如,如果创建一个“/var/db/mongo”目录并希望将数据库存放在此处,可以用下面的命令来启动数据库服务器(假设 mongod 所在的路径能够被 Path 找到,如果不能的话则需要指定绝对路径):

$ sudo mongod --dbpath /var/db/mongo
服务正常启动后会显示“waiting for connections on port 27017”这样一条消息(屏幕截图 1)。

屏幕截图 1 MongoDB 启动时的样子

对 MongoDB 进行操作需要使用 mongo 命 令。如果为数据库服务器指定了非默认的端口号,则 mongo 命令也需要指定 --port 参数。打开一个新的终端控制台,用下列命令来启动 mongo:

$ mongo
MongoDB shell version: 1.3.1
url: test
connecting to: test
type "exit" to exit
type "help" for help
>
这样就连接成功了。这个命令可以通过交互的方式对数据库进行操作,对于学习 MongoDB 很有帮助。此外,对于数据库的小规模调整和修改也十分方便。

不过 mongo 命令没有提供行编辑功能,如果配合使用支持行编辑功能的 rlwrap 命令则会更加方便。

$ rlwrap mongo
用上述格式启动,就可以为 mongo 命令增加行编辑功能。这样不仅能对输入行进行编辑,还可以查询输入历史,非常方便。

在 Debian 和 Ubuntu 中,可以用下列命令来安装 rlwrap:

$ sudo apt-get install rlwrap

MongoDB 的数据库结构

MongoDB 的结构分为数据库(database)、集合(collection)、文档(document)三层。在 mongo 命令中输入“show dbs”可以显示当前连接的数据库服务器所管理的数据库清单。

> show dbs
admin
local
我们可以看到,这台服务器所管理的数据库有 admin 和 local 这两个。对数据库的操作是针对当前数据库进行的。在连接时显示的消息中,“connecting to:”所表示的就是当前数据库。查看当前数据库可以使用“db”命令:

> db
test
在这里,数据库包含若干个集合,而集合则相当于关系型数据库中“表”的概念。关系型数据库中的表,都拥有各自的结构定义(schema),结构定义决定了表中各行(记录)包含怎样的数据,以及这些数据排列的顺序。因此每条记录都遵循 schema 的定义而具备完全相同的结构。

但对于无结构的 MongoDB 数据库来说,虽然集合中包含了相当于记录的文档,但每一个文档并不必具备相同的结构,而是能够存放可以用 JSON 进行描述的任意数据。一般来说,在同一个集合中倾向于保存结构相同的文档,但 MongoDB 对此并非强制。

这就意味着,随着应用程序开发的进行,对于数据库中数据的结构变化,可以灵活地做出应对。在 Ruby on Rails 的开发中,一旦数据库结构发生变化,就必须花很大精力来编写数据迁移脚本,而这样的苦差事在 MongoDB 中是完全可以避免的。

数据的插入和查询

在关系型数据库中,要创建新的表,需要对表结构进行明确的定义并执行显式的创建操作,而在更加灵活的 MongoDB 中则不需要这么麻烦。在 mongo 命令中用 use 命令可以切换当前数据库,如果 use 命令指定了一个不存在的数据库,则会自动创建一个新数据库。

> use linux_mag
switched to db linux_mag
而且,如果向不存在的集合中保存文档的话,就会自动创建一个新的集合。

> db.articles.save({
... title: "技术的剖析",
... author: "matz"
... })
其中“…”是 mongo 命令中表示折行的提示符。通过这样的命令,我们就向 linux_mag 数据库的 articles 集合中插入了一个新文档。

> show collections
articles
system.indexes
下面我们来查询一下这个文档。查询文档需要使用集合的 find 方法。

> db.articles.find()
{ "_id" : ObjectId("4b960889e4ffd91673c93250"), "title" : "技术的剖析", "author" : "matz" }
保存的数据会被自动分配一个名为“_id”的唯一 ID。find 方法还可以指定查询条件,如:

> db.articles.find({author: "matz"})
 { "_id" : ObjectId("4b960889e4ffd91673c93250"), "title" : "技术的剖析", "author" : "matz" }
如果指定一个 JavaScript 对象作为 find 方法的参数,则会返回与其属性相匹配的文档。在这里我们的数据库中只有一个文档,如果有多个匹配的文档的话,自然会返回多个结果。

如果希望只返回一个符合条件的文档,则可以用 findOne 方法来代替 find 方法。

用 JavaScript 进行查询

mongo 命令最重要的一点,是可以自由地运行 JavaScript。mongo 所接受的命令,除了 help、exit 等一部分命令之外,其余的都是 JavaScript 语句。甚至可以说,mongo 命令本身就是一个交互式的 JavaScript 解释器。刚才的例子中出现的:

db.articles.find()
等写法,正是 JavaScript 的方法调用形式。由于支持 JavaScript,因此我们可以自由地进行一些简单的计算,将结果赋值给变量,甚至用 for 语句进行循环。

> 1 + 1
2
> print("hello")
hello
下面我们就用 JavaScript 来为数据库填充一定规模的数据。

> for (var i = 0; i < 1000000
; i++) { ... db.bench.save( { x:4, j:i } ); ... }
在花了相当长一段时间之后,我们就创建了一个包含 100 万个文档的 bench 集合。接下来,我们来试试看查询。

> db.bench.findOne({j:999999})
{ "_id" : ObjectId("4b965ef5ffa07ec509bd338e"), "x" : 4, "j" : 999999 }
在我的电脑上查询到这个结果用了差不多 1 秒的时间。因为要将 100 万个文档全部查询一遍,所以这个速度不是很快,于是我们来创建一个索引。

> db.bench.ensureIndex({j:1}, {unique: true})
这样我们就对 j 这个成员创建了一个索引,再查询一次试试看。

> db.bench.findOne({j:999999})
{ "_id" : ObjectId("4b965ef5ffa07ec509bd338e"), "x" : 4, "j" : 999999 }
在创建索引之前,按下回车键到返回结果总觉得会卡一会儿,现在则是瞬间就可以得到结果,索引的效果是非常明显的。

在 mongo 命令中,可以使用 JavaScript 这样一种完全编程语言来对数据库进行操作,感觉真是不错。也许是因为我没有在工作中使用过 SQL 的原因吧,总觉得需要用 SQL 这样一种不完全语言来编写算法和进行操作的关系型数据库让我觉得不太习惯,相比之下还是 MongoDB 感觉更加亲近一些。从我个人的喜好来说,自然希望在 mongo 命令中也可以用 Ruby 来对数据库进行操作。话说,2012 年 4 月,AvocadoDB 3宣布要集成 mruby,这很值得期待。

3 AvocadoDB 已于 2012 年 5 月改名为 ArangoDB: http://www.arangodb.org/

高级查询

在 MongoDB 中,使用 find 或者 findOne 方法并指定对象作为条件时,会返回成员名称和值相匹配的文档。严格来说,find 方法返回的是与符合条件的结果集相对应的游标,而 findOne 则是仅返回第一个找到的符合条件的文档。

不过,说到查询,可并不都是这么简单的用法。下面我们通过将 SQL 查询转换为 MongoDB 查询的方式,来学习一下 MongoDB 的查询编写方法。刚才出现过的查询符合条件记录的例子,用 SQL 来编写的话应该是下面这样:

SELECT * FROM bench
 WHERE x = 4
这条查询写成 MongoDB 查询则是这样:

> db.bench.find({x: 4})
如果希望只选出特定的成员(字段),用 SQL 要写成:

SELECT j FROM bench WHERE x = 4
MongoDB 的话则是:

> db.bench.find({x: 4}, {j: true})
刚才我们的查询条件都是“等于”,如果要比较大小当然也是可以的。例如,“x 大于等于 4”这样的条件,用 SQL 查询可以写成:

SELECT j FROM bench WHERE x >= 4
而 MongoDB 的话则是:

> db.bench.find({x: 4}, {j: {$gte: 4}})
当比较条件不是等于时,要像上面这样使用以“$”开头的比较操作符来表达。MongoDB 中可以使用的比较操作符如表 2 所示。

表2 比较操作符

名 称

语 法

含 义

$gt

{$gt: val}

大于val

$lt

{$gt: val}

小于val

$gte

{$gte: val}

大于等于val

$lte

{$lte: val}

小于等于val

$ne

{$ne: val}

不等于val

$in

{$in: val}

包含val

$nin

{$nin: val}

不包含val

$mod

{$mod: [n, m]}

除以 n 的余数为m

$all

{$all: ary}

包含ary 的所有元素

$size

{$size: n}

数组长度为n

$exists

{$exists: true}

存在

$exists

{$exists: false}

不存在

$not

{$not: cond}

否定条件

正则表达式/

^foo

等正则匹配

$where

{$where: str}

将 str 作为 JavaScript 进行求值,用 this 来引用文档

我们刚才已经讲过,在 find 方法中,返回的并不是文档本身,而是游标(cursor)。当执行的查询得到多个匹配结果时,某些情况下返回的结果数量可能会超乎想象。这时,我们可以使用 count、limit、skip、sort 等方法。

count 方法可以返回游标所关联的结果集的大小。

> db.bench.find().count()
 1000000
limit 方法可以将结果集的大小限制为从游标开始位置起指定数量的文档(图 3)。

> db.bench.find().limit(10)
 { "_id" : ..., "x" : 4, "j" : 0 }
 { "_id" : ..., "x" : 4, "j" : 1 }
 { "_id" : ..., "x" : 4, "j" : 2 }
 { "_id" : ..., "x" : 4, "j" : 3 }
 { "_id" : ..., "x" : 4, "j" : 4 }
 { "_id" : ..., "x" : 4, "j" : 5 }
 { "_id" : ..., "x" : 4, "j" : 6 }
 { "_id" : ..., "x" : 4, "j" : 7 }
 { "_id" : ..., "x" : 4, "j" : 8 }
 { "_id" : ..., "x" : 4, "j" : 9 }
图 3 limit 方法的执行结果

skip 方法可以使游标跳过指定数量的记录(图 4)。配合使用 limit 和 skip,就可以像 Google 搜索页面一样,轻松实现以 n 个结果为单位将结果进行分页的操作。

> db.bench.find().skip(10).limit(10)
 { "_id" : ..., "x" : 4, "j" : 10 }
 { "_id" : ..., "x" : 4, "j" : 11 }
 { "_id" : ..., "x" : 4, "j" : 12 }
 { "_id" : ..., "x" : 4, "j" : 13 }
 { "_id" : ..., "x" : 4, "j" : 14 }
 { "_id" : ..., "x" : 4, "j" : 15 }
 { "_id" : ..., "x" : 4, "j" : 16 }
 { "_id" : ..., "x" : 4, "j" : 17 }
 { "_id" : ..., "x" : 4, "j" : 18 }
 { "_id" : ..., "x" : 4, "j" : 19 }
图 4 skip 方法的执行结果

sort 方法可以按指定成员对查询结果进行排序(图 5)。

> var c = db.bench.find()
 > c.skip(10).limit(10).sort({j: -1})
 { "_id" : ..., "x" : 4, "j" : 999989 }
 { "_id" : ..., "x" : 4, "j" : 999988 }
 { "_id" : ..., "x" : 4, "j" : 999987 }
 { "_id" : ..., "x" : 4, "j" : 999986 }
 { "_id" : ..., "x" : 4, "j" : 999985 }
 { "_id" : ..., "x" : 4, "j" : 999984 }
 { "_id" : ..., "x" : 4, "j" : 999983 }
 { "_id" : ..., "x" : 4, "j" : 999982 }
 { "_id" : ..., "x" : 4, "j" : 999981 }
 { "_id" : ..., "x" : 4, "j" : 999980 }
图 5 sort 方法的执行结果

这样我们就完成了按成员 j 降序排列的操作。和前面的 skip(10).limit(10) 的结果相比,j 的值是不同的。由于 sort 方法是对整个查询结果进行排序,因此对于查询结果来说,这些方法的执行顺序和实际的调用顺序无关,总是按照① sort ② skip ③ limit 的顺序来执行。

数据的更新和删除

只有文档的插入和查询并不能构成数据库的完整功能,我们还需要进行更新和删除。文档的插入我们使用了 save 方法,保存好的文档会被赋予一个 _id 成员,因此,当要保存的文档的 _id 已存在时,就会覆盖相应 _id 的文档。

也就是说,用 find 或 findOne 方法取出文档后,对取出的文档(JavaScript 对象)进行修改并再次调用 save(只有 _id 成员是不能修改的)的话,就会覆盖原来的文档。

在 MongoDB 中不存在事务的概念,因此总是以最后写入的数据为准。MySQL 在最开始不支持事务的时候还是非常有用的,由此可见,Web 应用中的数据库系统,即便不支持事务,貌似也不是很大的问题。MongoDB 中虽然不支持事务,但可以支持原子操作(atomic operation)和乐观并发控制(optimistic concurrency control)。要实现原子操作和乐观并发控制,可以使用 update 方法。

update 所支持的原子操作如表 3 所示,原子操作的名称都是以“$”开头的。例如,要将 j 为 0 的文档的 x 值增加 1,可以写成下面这样:

> db.bench.update({j:0},{$inc:{x:1}})

表3 update的原子操作

名 称

语 法

功 能

$inc

{$inc: {mem: n}}

对 mem 的值加 n

$set

{$set: {mem: val}}

将 mem 的值设置为 val

$unset

{$unset: {mem: 1}}

删除 mem

$push

{$push: {mem: val}}

在数组 mem 中添加 val

$pushAll

{$pushAll: {mem: ary}}

在数组 mem 中添加 ary 的元素

$addToSet

{$addToSet: {mem: val}}

当数组 mem 中不包含 val 时则添加val

$pop

{$pop: {mem: 1}}

删除数组 mem 中最后一个元素

$pop

{$pop: {mem: -1}}

删除数组 mem 中第一个元素

$pull

{$pull: {mem: val}}

从数组 mem 中删除所有的val

$pullAll

{$pullAll: {mem: ary}}

从数组 mem 中删除所有 ary 中的元素

乐观并发控制

然而,当需要进行并发操作时,仅凭原子操作还不够。在关系型数据库中,一般是通过事务的方式来处理的,但 MongoDB 中没有这样的机制。MongoDB 中进行并发操作的步骤如下。

1. 通过查询获取文档。

2. 保存原始值。

3. 更新文档。

4. 将原始值(包含 _id)作为第一参数,将更新后的文档作为第二参数,调用 update 方法。如果文档已经被其他并发操作修改时,则 update 失败。

5. 如果 update 失败则返回第 1 步重新执行。

这样的方式,也就是利用了 update 方法可以进行原子更新这一点,通过同时指定事务开始时和更新后的值,来手动实现相当于关系型数据库中事务处理的功能。这种方法的前提,是基于“同一个文档基本上不会被同时修改”这一预测,也就是一种乐观的(近似的)事务机制。

需要显式创建数据的副本这一点有些麻烦,但忽略这一点的话,实际上还是很实用的。作为一个简单的例子,我们将刚才讲过的 $inc 那个例题,用乐观并发处理进行实现,如图 6 所示。

> for (;;) {
... var d = db.bench.findOne({j:0})
... var n = d.x
... d.x++
... db.bench.update({_id:d._id, x:n}, d)
... if (db.$cmd.findOne({getlasterror:1}).updatedExisting) break
... }
图 6 乐观并发处理

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文