diff --git a/code/framework/service/mysqld.lua b/code/framework/service/mysqld.lua deleted file mode 100644 index 5988de4..0000000 --- a/code/framework/service/mysqld.lua +++ /dev/null @@ -1,50 +0,0 @@ -local skynet = require "skynet" -local mysql = require "skynet.db.mysql" -local util = require "store_util" -require "skynet.manager" - -local traceback = debug.traceback - -local dbkey, index = ... -local db - -local CMD = {} - -local function success(ret) - if not ret or ret.err or ret.badresult then - return false - end - return true -end - -function CMD.init(conf) - db = mysql.connect(conf) - db:query("set names utf8mb4") - return true -end - -function CMD.exec_one(sql) - local ok, ret = xpcall(db.query, traceback, db, sql) - if not ok or not success(ret) then - assert(false, ("sql=[%s] ret=[%s]"):format(sql, util.encode(ret))) - return - end - return ret -end - -function CMD.exec(sqls) - for i = 1, #sqls do - local sql = sqls[i] - CMD.exec_one(sql) - end -end - -skynet.start(function() - skynet.dispatch("lua", function(_, _, cmd, ...) - local f = CMD[cmd] - assert(f, cmd) - skynet.retpack(f(...)) - end) -end) - -skynet.register(util.mysql_sname(dbkey, index)) diff --git a/docs/lua-style-guide/README.md b/docs/lua-style-guide.md similarity index 100% rename from docs/lua-style-guide/README.md rename to docs/lua-style-guide.md diff --git a/docs/mongodb.md b/docs/mongodb.md new file mode 100755 index 0000000..68bb8e3 --- /dev/null +++ b/docs/mongodb.md @@ -0,0 +1,710 @@ +\thispagestyle{empty} +\changepage{}{}{}{-1.5cm}{}{2cm}{}{}{} +![The Little MongoDB Book, By Karl Seguin](title.png)\ + +\clearpage +\changepage{}{}{}{1.5cm}{}{-2cm}{}{}{} + +## 关于本书 ## + +### 许可证 ### +这本书,The Little MongoDB Book,基于Attribution-NonCommercial 3.0 Unported license发布。**您不需要为本书付钱。** + +您有权复制、分发、修改或展示本书。但请认可本书的作者是Karl Seguin,也请勿将其用于任何商业用途。 + +您可以在以下链接查看该许可证的全文: + + + +### 关于作者 ### +Karl Seguin是一位在多个技术领域有着丰富经验的研发人员,精通.NET以及Ruby。作为技术文档撰写人,他有时还会参与OSS项目的工作或做演讲。在MongoDB方面,他曾是C# MongoDB library NoRM的核心开发人员,开发了互动教程[mongly](http://mongly.com)以及[Mongo Web Admin](https://github.com/karlseguin/Mongo-Web-Admin)。他还用MongoDB开发了[mogade.com](http://mogade.com/)为业余游戏开发者提供免费服务。 + +Karl还著有[The Little Redis Book](http://openmymind.net/2012/1/23/The-Little-Redis-Book/)。 + +他的博客,推特账号是[@karlseguin](http://twitter.com/karlseguin)。 + +### 致谢 ### +Perry Neal,谢谢你给我无法衡量的帮助,谢谢你锐利的眼光、独到的思路以及无比的热情。非常感谢。 + +### 最新版本 ### +本书的最新版本可以在下面的链接找到: + +. + +### 关于本书的中文版本 ### +本书的中文版由[justinyhuang](http://justinyhuang.com)完成,基于与原著相同的许可证。最新版本在[GitHub](https://github.com/justinyhuang/the-little-mongodb-book-cn)。译文的纰漏欢迎告知或直接提交github。 + +\clearpage + +## 简介 ## + > 本章很短,但不是我的错,MongoDB就是那么简单易学。 + +人们总是说科技的发展风驰电掣。确实,一直以来都不断有新的技术涌现出来。但是我却一直坚持认为程序员所用的基本技术的发展相对而言就缓慢很多。您可以很多年不学习什么但还是可以混过去。让人瞩目的是业已成熟的技术被替代的速度。仿佛一夜间,那些长期以来业已成熟的技术忽然就失去了开发者的关注,昔日地位岌岌可危。 + +NoSQL逐步攻陷了传统关系数据库的领地,就是这种急剧转变最好的例子。仿佛就在昨天所有的网页还是由一些RDBMS驱动的,而一早起来就已经有大约5种NoSQL的方案证明了他们都是有价值的解决方案。 + +虽然这些转变看起来是一夜间就发生的,事实却是这些新生的技术历经多年才被接受并应用于实践。一开始先是由一小部分开发者或者企业推动,然后逐步吸取教训,改善方案并见证新技术地位的确立。其他的跟随者之后也慢慢地开始了尝试。对于NoSQL来说也是一样。很多新方案的出现都不是为了去代替更加传统的存储方案,而是为了填补后者所能满足需求之外的一些空白。 + +说了那么多,在这里我们首先要做的是弄清楚什么是NoSQL。这是一个很宽泛的概念,不同的人有不同的解读。我个人用NoSQL来泛指参与数据存储的系统。换句话说,NoSQL(还是我个人的意见),是一种观念,这种观念认为维护数据的持久性不必是单个系统的责任。相比之下,关系数据库的缔造者一开始就力图把他们的软件当作通用解决方案。NoSQL则更倾向于负责系统中的一小部分功能:限定了部分功能,便可以使用最适合的工具。因此,您以后的NoSQL架构中依旧有可能利用到关系数据库,比如说MySQL,于此同时还可能在特定的部件使用Redis,还会用Hadoop进行大量的数据处理。简而言之,NoSQL就是保持开放和警醒,利用已有的可用的工具和方法去管理您的数据。 + +您也许会想,MongoDB怎么能搞定那么多?作为一个面向文档的数据库,Mongo是一个更加通用的NoSQL方案。可以认为它是关系数据库的一个替代方案。和关系数据库一样,Mongo也可以和其它更细化的NoSQL方案协作而更加强大。MongoDB既有优点也有缺点,我们在后续的章节中都会提及。 + +您也许也注意到了,在书中我们是混用Mongo和MongoDB这两个词的。 + +## 准备 ## +本书大部分篇幅会用来关注MongoDB的核心功能。所以我们基本上使用的是MongoDB的外壳(shell)。shell在学习MongoDB还有管理数据库的时候很有用,不过您的实际代码还是会用相应的语言来驱动mongoDB的。 + +这也引出了关于MongoDB您首先需要了解的东西:它的驱动。MongoDB有许多针对不同语言的[官方驱动](http://www.mongodb.org/display/DOCS/Drivers)。可以认为这些驱动和您所熟知的各种数据库驱动是一样的。基于这些驱动,MongoDB的开发社区又搭建了更多语言/框架相关的库。比如说[NoRM](https://github.com/atheken/NoRM)就是一个实现了LINQ的C#库,还有[MongoMapper](https://github.com/jnunemaker/mongomapper),一个很好地支持ActiveRecord的Ruby库。您可以自行决定直接针对MongoDB的核心驱动编程,或者采用一些高层的库。在这里指出这点,是因为不少MongoDB的新手都会为既有官方驱动又有社区提供的库而困惑不已——前者着重与MongoDB的核心通讯/连接,而后者则提供了更多语言/框架相关的具体实现。 + +我建议您在阅读本书的同时,也在MongoDB中尝试我给出的例子。如果在这个过程中您自己发现了什么问题,也可以在MongoDB环境中探索需求答案。安装并运行MongoDB其实很简单,只需要几分钟的时间。那么现在就开始吧。 + +1. 从[官方下载页面](http://www.mongodb.org/downloads)的第一行(这是推荐的稳定版本)下载与您操作系统相应的安装包。根据不同的开发需要,选择32位或是64位的包。 + +2. 解压下载的包(到任意路径)并进入`bin`子目录,暂且不要执行任何命令。让我先介绍一下,`mongod`将启动服务器进程而`mongo`会打开客户端的shell——大部分时间我们将和这两个可执行文件打交道。 + +3. 在`bin`子目录中创建一个新的文本文件,取名为`mongodb.config`。 + +4. 在mongodb.config中加一行:`dbpath=PATH_TO_WHERE_YOU_WANT_TO_STORE_YOUR_DATABASE_FILES`。例如,在Windows中您需要添加的可能是`dbpath=c:\mongodb\data`而在Linux下可能就是`dbpath=/etc/mongodb/data`。 + +5. 确认您指定的`dbpath`是存在的。 + +6. 执行mongod,带上参数`--config /path/to/your/mongodb.config`。 + +以Windows用户为例,如果您把下载的的文件解压到`c:\mongodb\`,创建了`c:\mongodb\data\`,然后在`c:\mongodb\bin\mongodb.config`中添加了`dbpath=c:\mongodb\data\`。那么您就可以在命令行中输入以下指令来启动`mongod`: + +`c:\mongodb\bin\mongod --config c:\mongodb\bin\mongodb.config` + +您可以把这个`bin`加入到您的默认路径中省得每次都要输入完整的路径。对于MacOSX和Linux的用户,方法也是几乎一样的。唯一的区别在于路径不同。 + +我希望您现在已经安装并可以运行MongoDB了。如果您遇到什么错误,注意看输出的错误信息——服务器(server)很善于解释究竟是哪里出了问题。 + +此时您可以运行`mongo`了(没有*d*),它会启动一个shell并连接到运行中的服务器。输入'db.version()`以确认所有的东西都正常工作:您应该可以看到您所安装的软件版本。 + +\clearpage + +## 第一章 - 基础 ## +从了解基本的构成开始,我们开始踏上MongoDB探索之路。显然,这是认识MongoDB的关键,同时也有助于搞清楚MongoDB适用范围的高层次问题。 + +作为开始,我们需要了解6个简单的概念: + +1. MongoDB有着与您熟知的‘数据库’(database,对于Oracle就是‘schema’)一样的概念。在一个MongoDB的实例中您有若干个数据库或者一个也没有,不过这里的每一个数据库都是高层次的容器,用来储存其他的所有数据。 + +2. 一个数据库可以有若干‘集合’(collection),或者一个也没有。集合和传统概念中的‘表’有着足够多的共同点,所以您大可认为这两者是一样的东西。 + +3. 集合由若干‘文档’(document)组成,也可以为空。类似的,可以认为这里的文档就是‘行’。 + +4. 文档又由一个或者更多个‘域’(field)组成,您猜的没错,域就像是‘列’。 + +5. ‘索引’(index)在MongoDB中的意义就如同索引在RDBMS中一样。 + +6. ‘游标’(cursor)和以上5个概念不同,它很重要但是却常常被忽略,有鉴于此我认为应该进行专门讨论。关于游标有一点很重要,就是每当向MongoDB索要数据时,它总是返回一个游标。基于游标我们可以作诸如计数或是直接跳过之类的操作,而不需要真正去读数据。 + +小结一下,MongoDB由‘数据库’组成,数据库由‘集合’组成,集合由‘文档’组成。‘域’组成了文档,集合可以被‘索引’,从而提高了查找和排序的性能。最后,我们从MongoDB读取数据的时候是通过‘游标’进行的,除非需要,游标不会真正去作读的操作。 + +您也许已经觉得奇怪,为什么要用新的术语(表换成集合,行换成文档,列换成域),这不是越弄越复杂了么?这是因为虽然这些概念和那些关系数据库中的相应概念很相似,但是还是存在差异的。关键的差异在于关系数据库是在‘表’这一层次定义‘列’的,而一个面向文档的数据库则是在‘文档’这一层次定义‘域’的。也就是说,集合中的每个文档都可以有独立的域。因此,虽说集合相对于表来说是一个简化了的容器,而文档则包含了比行要多得多的信息。 + +虽然这些异同很重要,但是如果您现在还没搞清楚也不必担心。以后试着插入几次(数据)就知道我们这里说的是什么了。最后,集合对其中储存的内容并没有严格的要求(它是无模式的(schema-less)),域是与其所在的文档绑定的。当中的优缺点我们会在后续的章节中继续探讨。 + +开始动手实践吧。如果您还没有运行Mongo,现在就可以启动`mongod`服务器以及Mongo的shell。Mongo的shell运行在JavaScript之上,您可以执行一些全局的指令,如`help`或者`exit`。操作对象`db`来执行针对当前数据库的操作,例如`db.help()`或是`db.stats()`。操作对象`db.COLLECTION_NAME`来执行针对某一给集合的操作,比如说`db.unicorns.help()`或是`db.unicorns.count()`。我们以后将会有许多针对集合的操作。 + +试试输入`db.help()`,您会看到一串命令列表,这些命令都可以用来操作`db`对象。 + +顺带说一句,因为我们用的是JavaScript的shell,如果您执行一个命令而忘了加上`()`,您看到的将是这个命令的实现而并没有执行它。知道这个,您在这么做并看到以`function(...){`开头的信息的时候就不会觉得惊讶了。比如说如果您输入`db.help`(后面没有括弧),你就将看到`help`命令的具体实现。 + +首先我们用全局命令`use`来切换数据库。输入`use learn`。这个数据库是否存在并没有关系,我们创建第一个集合后这个`learn`数据库就会生成的。现在您应该已经在一个数据库里面了,可以执行一些诸如`db.getCollectionNames()`的数据库命令了。如果您现在就这么做,将会看到一个空的数组(`[]`)。因为集合是无模式的,我们不需要专门去创建它。我们要做的只是把一个文档插入一个新的集合,这个集合就生成了。您可以像下面一样调用`insert`命令去插入一个文档: + + db.unicorns.insert({name: 'Aurora', gender: 'f', weight: 450}) + +以上命令对`unicorns`对象执行`insert`操作,并传入一个参数。在MongoDB内部,数据是以二进制的串行JSON格式存储的。这对外部的用户而言,意味着JSON的大量应用,就如同上面的参数一样。如果我们现在执行`db.getCollectionNames()`,将看到两个集合:`unicorns`以及`system.indexes`。`system.indexes`在每个数据库中都会创建,它包含了数据库中的索引信息。 + +现在您可以对`unicorns`对象执行`find`命令,得到一个文档列表: + + db.unicorns.find() + +请注意,除了您在文档中输入的各个域,还有一个一个叫做`_id`的域。每一个文档都必须有一个独立的`_id`域。您可以自己创建,也可以让MongoDB为您生成这个ObjectId。大部分情况下您还是会让MongoDB为您生成ID的。默认情况下,`_id`域是被索引了的——这就是为什么会有`system.indexes`创建出来的原因。看看`system.indexes`有什么: + + db.system.indexes.find() + +在结果中您会看到该索引的名字,它所绑定的数据库和集合的名字,还有包含这个索引的域。 + +回到我们前面关于无模式集合的讨论。现在往`unicorns`插入一个完全不同的文档,比如说这样: + + db.unicorns.insert({name: 'Leto', gender: 'm', home: 'Arrakeen', worm: false}) + +再次用`find`可以列出所有的文档。学习到后面,我们将继续讨论MongoDB无模式的这一有趣的行为,不过我希望您已经开始慢慢了解为什么传统的那些术语不适合用在这里了。 + +### 掌握选择器(selector) ### +除了刚才介绍过的6个概念,MongoDB还有一个很实用的概念:查询选择器(query selector),在进入更高阶的内容之前,您也需要很好的了解它是什么。 +MongoDB的查询选择器就像SQL代码中的`where`语句。因此您可以用它在集合中查找,统计,更新或是删除文档。选择器就是一个JSON对象,最简单的形式就是`{}`,用来匹配所有的文档(`null`也可以)。如果我们需要找到所有雌性的独角兽(unicorn),我们可以用选择器`{gender:'f'}`来匹配。 + +在深入选择器之前,我们先输入一些数据以备后用。首先用`db.unicorns.remove()`删除之前我们在`unicorns`集合中输入的所有数据。(由于在这条命令中我们没有指定选择器,于是所有的文档都将被清除)。然后用下面的插入命令准备一些数据(建议拷贝粘帖这些命令): + + db.unicorns.insert({name: 'Horny', dob: new Date(1992,2,13,7,47), loves: ['carrot','papaya'], weight: 600, gender: 'm', vampires: 63}); + db.unicorns.insert({name: 'Aurora', dob: new Date(1991, 0, 24, 13, 0), loves: ['carrot', 'grape'], weight: 450, gender: 'f', vampires: 43}); + db.unicorns.insert({name: 'Unicrom', dob: new Date(1973, 1, 9, 22, 10), loves: ['energon', 'redbull'], weight: 984, gender: 'm', vampires: 182}); + db.unicorns.insert({name: 'Roooooodles', dob: new Date(1979, 7, 18, 18, 44), loves: ['apple'], weight: 575, gender: 'm', vampires: 99}); + db.unicorns.insert({name: 'Solnara', dob: new Date(1985, 6, 4, 2, 1), loves:['apple', 'carrot', 'chocolate'], weight:550, gender:'f', vampires:80}); + db.unicorns.insert({name:'Ayna', dob: new Date(1998, 2, 7, 8, 30), loves: ['strawberry', 'lemon'], weight: 733, gender: 'f', vampires: 40}); + db.unicorns.insert({name:'Kenny', dob: new Date(1997, 6, 1, 10, 42), loves: ['grape', 'lemon'], weight: 690, gender: 'm', vampires: 39}); + db.unicorns.insert({name: 'Raleigh', dob: new Date(2005, 4, 3, 0, 57), loves: ['apple', 'sugar'], weight: 421, gender: 'm', vampires: 2}); + db.unicorns.insert({name: 'Leia', dob: new Date(2001, 9, 8, 14, 53), loves: ['apple', 'watermelon'], weight: 601, gender: 'f', vampires: 33}); + db.unicorns.insert({name: 'Pilot', dob: new Date(1997, 2, 1, 5, 3), loves: ['apple', 'watermelon'], weight: 650, gender: 'm', vampires: 54}); + db.unicorns.insert({name: 'Nimue', dob: new Date(1999, 11, 20, 16, 15), loves: ['grape', 'carrot'], weight: 540, gender: 'f'}); + db.unicorns.insert({name: 'Dunx', dob: new Date(1976, 6, 18, 18, 18), loves: ['grape', 'watermelon'], weight: 704, gender: 'm', vampires: 165}); + +现在我们有足够的数据,我们可以来掌握选择器了。`{field: value}`用来查找所有`field`等于`value`的文档。通过`{field1: value1, field2: value2}`的形式可以实现`与`操作。`$lt`、`$lte`、`$gt`、`$gte`以及`$ne`分别表示小于、小于或等于、大于、大于或等于以及不等于。举个例子,查找所有体重超过700磅的雄性独角兽的命令是: + + db.unicorns.find({gender: 'm', weight: {$gt: 700}}) + //或者 (效果并不完全一样,仅用来为了演示不同的方法) + db.unicorns.find({gender: {$ne: 'f'}, weight: {$gte: 701}}) + +`$exists`操作符用于匹配一个域是否存在,比如下面的命令: + + db.unicorns.find({vampires: {$exists: false}}) +会返回单个文档(译者:只有这个文档没有vampires域)。如果需要*或*而不是*与*,可以用`$or`操作符并作用于需要进行或操作的数组: + + db.unicorns.find({gender: 'f', $or: [{loves: 'apple'}, {loves: 'orange'}, {weight: {$lt: 500}}]}) + +以上命令返回所有或者喜欢苹果,或者喜欢橙子,或者体重小于500磅的雌性独角兽。 + +您可能已经注意到了,在最后的一个例子中有一个非常棒的特性:`loves`域是一个数组。在MongoDB中数组是一级对象(first class object)。这是非常非常有用的功能。一旦用过,没有了它你可能都不知道怎么活下去。更有意思的是基于数组的选择是非常简单的:`{loves: 'watermelon'}`就会找到`loves`中有`watermelon`这个值的所有文档。 +除了我们目前所介绍过的,还有更多的操作符 +可以使用。最灵活的是`$where`,允许输入JavaScript并在服务器端运行。这些都在MongoDB网站的[Advanced Queries](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries)部分有详细介绍。不过这里介绍的都是基本的命令,了解了这些您就可以开始使用Mongo了。而这些命令也往往是您大多数时间会用到的所有命令。 + +我们已经介绍过怎样在`find`命令中使用选择器了。此外选择器还可以用在`remove`命令中,我们已经大致提过;还有`count`命令中,我们并没有介绍不过您自己可以去试试看;还有`update`命令,我们在后面还会提到。 + +MongoDB为`_id`域生成的`ObjectId`也是可以被选择的,就像这样: + + db.unicorns.find({_id: ObjectId("TheObjectId")}) + +### 本章小结 ### +我们还没有介绍`update`命令以及`find`的更华丽的功能。不过我们已经让MongoDB运行起来,并且执行了`insert`和`remove`命令(这些命令看过本章的介绍也差不多了)。我们还介绍了`find`命令并见识了什么是MongoDB的选择器。一个好的开头为后面的深入奠定了坚实的基础。不管你信不信,您事实上已经了解了关于MongoDB所需要知道的知识——它本来就是易学易用的。我强烈建议您在继续读下去之前多多在MongoDB练习尝试一下。试着插入不同的文档,可以插入到新的集合中,并且熟悉不同的选择器表达式。多用`find`,`count`和`remove`。经过您自己的实践,那些初看很别扭的东西最后都会变得好用起来的。 + +\clearpage + +## 第二章 - 更新 ## +在第一章中我们介绍了CRUD(Create、Read、Update、Delete)中的三个操作。本章专门用来介绍前面跳过的第四个操作:`update`。`update`有一些出人意料的行为,这就是为什么我们专门在这章当中讨论它。 + +### update: replace 与 $set ### +`update`最简单的执行方式有两个参数:一个是选择器(选择更新的范围),一个是需要更新的域。如果Roooooodles长胖了,我们就需要: + + db.unicorns.update({name: 'Roooooodles'}, {weight: 590}) + +(如果您用的是自己创建的`unicorns`集合,原来的数据都丢失了,那么就用`remove`删除掉所有文档,重新插入第一章中的数据) + +在实际的代码中,您也许会基于`_id`来更新记录,不过既然我不知道MongoDB给您分配的`_id`是什么,我们就用`name`好了。如果我们看看更新过的记录: + + db.unicorns.find({name: 'Roooooodles'}) + +您就会发现`update`第一个出人意料的地方:上面的命令找不到任何文档。这是因为命令中输入的第二个参数是用来**替换(replace)**原来的文档的。换句话说,`update`先是根据`name`找到一个文档,然后用新的文档(也就是第二个参数)去覆盖找到的整个文档。这和SQL中的`update`的行为是不一样的。在某些情况下,这一行为非常理想,可以用于实现完全动态的更新。然而当您需要的仅是改变某个文档的某个值或者几个域,最好还是用MongoDB的`$set`修改符(modifier): + + db.unicorns.update({weight: 590}, {$set: {name: 'Roooooodles', dob: new Date(1979, 7, 18, 18, 44), loves: ['apple'], gender: 'm', vampires: 99}}) + +这样做就会重设那些丢失的域。新的`weight`值不会被覆盖,因为我们没有在命令中指定它。如果现在执行: + + db.unicorns.find({name: 'Roooooodles'}) + +得到的就是预想的结果。所以在一开始时正确的更新体重的方法应该是: + + db.unicorns.update({name: 'Roooooodles'}, {$set: {weight: 590}}) + +### 更新修改符 ### +除了`$set`,还有其他的修改符可以用来非常漂亮地完成一些任务。所有的更新修改符都作用于域上——这样您的文档就不会被整个改写。比如说,`$inc`可以用来将一个域的值增加一个正的或负的数值。举个例子,如果由于失误,Pilot多获得了一些吸血的技能(vampire skill。译者:这里的独角兽可以理解为游戏中的某个角色,而这个角色也许可以通过升级打怪的方式提升某些技能,比如说吸血技能。),我们可以用下面的命令来纠正这个错误: + + db.unicorns.update({name: 'Pilot'}, {$inc: {vampires: -2}}) + +如果Aurora忽然长出了一颗可爱的牙(译者:可以吃糖了),可以用`$push`修改符为她的`loves`域添加一个新的值: + + db.unicorns.update({name: 'Aurora'}, {$push: {loves: 'sugar'}}) + +MongoDB网站上的[Updating](http://www.mongodb.org/display/DOCS/Updating)部分可以找到其他更新修改符的更多信息。 + +### 插新(Upsert) ### + > 译者:[Upsert](http://en.wikipedia.org/wiki/Upsert)的意思是update if present; insert if not。是update和insert合体的产物。没有找到一个合适的词作为翻译,于是我斗胆发明了“插新”这个词,取或插入或更新之意。如有更好的办法,还请指点。 + +`update`的一个比较讨喜的出人意料之处就是它完全支持插新(`upsert`)。当目标文档存在的时候,插新操作会更新该文档,否则就插入该新文档。插新在某些情况下是很方便的,当您碰到这种情况的时候就会知道了。为了打开插新的功能,我们在使用`update`时把第三个参数设为`true`。 + +一个很常见的例子就是网站的点击计数器。如果需要得到实时的点击累计数值,我们需要知道这个页面的点击记录是否存在,然后决定是要更新点击数还是插入。如果忽略第三个参数(或者是设置为false),下面的命令什么也不做: + + db.hits.update({page: 'unicorns'}, {$inc: {hits: 1}}); + db.hits.find(); + +如果打开了插新,结果就不一样了: + + db.hits.update({page: 'unicorns'}, {$inc: {hits: 1}}, true); + db.hits.find(); + +这一次,因为没有文档有域`page`的值为`unicorns`,就插入一个新的文档。再执行一次上面的命令,创建好的文档就会被更新,而`hits`的值就会增加为2。 + + db.hits.update({page: 'unicorns'}, {$inc: {hits: 1}}, true); + db.hits.find(); + +### 多重更新 ### +`update`最后的一个惊喜是,它会默认地只更新一个文档。到目前为止就我们所见到的例子来看,这样做是合理的。不过如果执行下面的命令: + + db.unicorns.update({}, {$set: {vaccinated: true }}); + db.unicorns.find({vaccinated: true}); + +您想要做的应该是找出所有已经注射过疫苗(vaccinated)的独角兽,但为了达到这样的目的,需要把第四个参数设为true: + + db.unicorns.update({}, {$set: {vaccinated: true }}, false, true); + db.unicorns.find({vaccinated: true}); + +### 本章小结 ### +本章完成了对基础的集合操作,CRUD的介绍。我们细致的了解了`update`以及它的三个有意思的行为:第一,和SQL的update不同,MongoDB的`update`会替换实际的文档。因此`$set`修改符就显得很有用了。第二,`update`支持直观的`插新`,这在和`$inc`修改符结合起来的时候特别有用。最后,`update`的默认行为是只更新第一个找到的文档。 + +一定要记住的是,我们是在MongoDB的shell中介绍它的。实际应用时您所采用的驱动或是库有可能会修改这些默认的行为,或是提供一个不同的编程接口(API)。例如:Ruby的驱动把最后的两个参数合并为一个哈希表:`{:upsert => false, :multi => false}`。类似地,PHP的驱动把最后的两个参数合并到了一个数组中:`array('upsert' => false, 'multiple' => false)`。 + +\clearpage + +## 第三章 - 掌握查找 ## +第一章对`find`命令作了一些简单的介绍,除了选择器以外,`find`还有很多其他的特性。之前有提到过`find`返回的结果是一个游标。现在就将对这点做深入的讨论。 + +### 域的选择 ### +在开始游标的话题之前,您需要知道`find`还有第二个可选参数。该参数是一个列表,用户在这个表中指明要求`find`读取的域。例如,可以用下面的命令获取所有独角兽的名字: + + db.unicorns.find(null, {name: 1}); + +`_id`域在默认情况下总是会被`find`返回的。`{name:1, _id: 0}`可以显式地从返回结果中排除它。 + +除了上面排除`_id`域的情况外,不可以将选择与排除的表达式混在一起使用(译者:比如说`{_id:1, name:0}`或者`{name:1, gender:0}`都是不合法的)。想一想其实也合乎逻辑:不是显式地选择,就是说明哪些域需要排除。 + +### 排序 ### +我已经好几次提到,`find`返回的是一个游标,对游标的操作直到必要的时候才会执行。然而在shell中的感觉却是`find`马上就执行了。这仅仅是`find`在shell中的行为。我们可以通过把`find`和一个命令连接起来的方法观察`find`真正的行为。我们先用`sort`来做这个实验。`sort`的运作方式有点像上一节提到的域的选择:标明哪些域需要排序,用1表示升序,用-1表示降序。例如: + + //最重的独角兽排在第一 + db.unicorns.find().sort({weight: -1}) + + //优先按名字排序再按吸血技能排序 + db.unicorns.find().sort({name: 1, vampires: -1}) + +如同关系数据库,MongoDB也可以利用索引进行排序。我们会在后面再详细讨论索引。只是您要知道,当没有建立索引时,MongoDB是限制排序对象大小的(译者:目前是16MB)。也就是说,如果您尝试对一个大规模的结果的集进行排序,而又没有为这个结果建立索引,那么就会看到错误的提示。对一些人来说,这是MongoDB的局限性。但是说实话,我真希望更多的数据库能够像MongoDB那样拒绝执行那些未经优化的查询。(我倒不是要把每个MongoDB的缺点都硬掰成优点,只是我见过太多缺乏优化的数据库了,所以我真的很希望它们能有一个`严格模式`以限制未经优化的查询。) + +### 分页(paging) ### +对结果的分页可以通过`limit`以及`skip`这两个游标操作来实现。比如可以用以下的命令来得到第二,第三重的独角兽: + + db.unicorns.find().sort({weight: -1}).limit(2).skip(1) + +对非索引域进行排序是很麻烦的,联合`limit`一起使用`sort`是避免此类麻烦的好方法。 + +### 计数 ### +在shell中我们可以对一个集合直接地执行`count`命令,例如: + + db.unicorns.count({vampires: {$gt: 50}}) + +而现实中`count`却是一个游标的操作,shell只是提供了一条捷径而已。在使用没有提供这些捷径的驱动时,就要用到下面的命令(在shell中也有用): + + db.unicorns.find({vampires: {$gt: 50}}).count() + +### 本章小结 ### +`find`和`cursors`的使用是比较直接明了的。还有一些额外的命令,有一些会在后面的章节继续介绍,其他的几乎都只是用在很少见的情况了。到现在为止您应该可以比较自如的在mongo的shell工作,也了解了MongoDB的基础知识了。 + +\clearpage + +## 第四章 - 数据建模 ## +这一章我们切换频道,谈谈关于MongoDB的一个比较抽象的话题吧。解释新的名字或者是新的句法都不是什么难事,而用新的范式探讨建模的问题就没那么简单了。事实上当涉及用新技术建模的问题时,我们中的大多数人还仍然在探索这些技术究竟是否合适。这是一个现在就开始的话题,但最终您还是需要自己实践并从真正的代码中去学习。 + +与大多数NoSQL方案相比,在建模方面,面向文档的数据库算是和关系数据库相差最小的。这些差别是很小,但是并不是说不重要。 + +### 没有连接 ### +您要接受的第一个也是最基本的一个差别,就是MongoDB没有连接(join)。我不知道MongoDB不支持某些类型连接句法的具体原因,但是我知道一般而言人们认为连接是不可扩展的。也就是说,一旦开始横向分割数据,最终不可避免的就是在客户端(应用程序服务器)使用连接。且不论MongoDB为什么不支持连接,事实是数据*是有关系的*,可是MongoDB不支持连接。(译者:这里的关系指的是不同的数据之间是有关联的,对于没有关系的数据,就完全不需要连接。) + +为了在没有连接的MongoDB中生存下去,在没有其他帮助的情况下,我们必须在自己的应用程序中实现连接。基本上我们需要用第二次查询去`找到`相关的数据。找到并组织这些数据相当于在关系数据库中声明一个外来的键。现在先别管什么`独角兽`了,我们来看看我们的`员工`。首先我们创建一个员工的数据(这次我告诉您具体的`_id`值,这样我们的例子就是一样的了): + + db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d730"), name: 'Leto'}) + +然后我们再加入几个员工并把`Leto`设成他们的老板: + + db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d731"), name: 'Duncan', manager: ObjectId("4d85c7039ab0fd70a117d730")}); + db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d732"), name: 'Moneo', manager: ObjectId("4d85c7039ab0fd70a117d730")}); + +(有必要再强调一下,`_id`可以是任何的唯一的值。在实际工作中你很可能会用到`ObjectId`, 所以我们在这里也使用它) + +显然,要找到Leto的所有员工,只要执行: + + db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")}) + +没什么了不起的。在最糟糕的情况下,为弥补连接的缺失需要做的只是再多查询一次而已,该查询很可能是经过索引了的。 + +#### 数组和嵌入文档(Embedded Documents) #### +MongoDB没有连接并不意味着它没有其他的优势。还记得我们曾说过MongoDB支持数组并把它当成文档中的一级对象吗?当处理多对一或是多对多关系的时候,这一特性就显得非常好用了。用一个简单的例子来说明,如果一个员工有两个经理,我们可以把这个关系储存在一个数组当中: + + db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d733"), name: 'Siona', manager: [ObjectId("4d85c7039ab0fd70a117d730"), ObjectId("4d85c7039ab0fd70a117d732")] }) + +需要注意的是,在这种情况下,有些文档中的`manager`可能是一个标量,而其他的却是数组。在两种情况下,前面的`find`还是一样可以工作: + + db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")}) + +很快您就会发现数组中的值比起多对多的连接表(join-table)来说要更容易处理。 + +除了数组,MongoDB还支持嵌入文档。尝试插入含有内嵌文档的文档,像这样: + + db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d734"), name: 'Ghanima', family: {mother: 'Chani', father: 'Paul', brother: ObjectId("4d85c7039ab0fd70a117d730")}}) + +也许您会这样想,确实也可以这样做:嵌入文档可以用‘.’符号来查询: + + db.employees.find({'family.mother': 'Chani'}) + +就这样,我们简要地介绍了嵌入文档适用的场合以及您应该怎样使用它。 + +#### DBRef #### +MongoDB支持一个叫做DBRef的功能,许多MongoDB的驱动都提供对这一功能的支持。当驱动遇到一个`DBRef`时它会把当中引用的文档读取出来。`DBRef`包含了所引用的文档的ID和所在的集合。它通常专门用于这样的场合:相同集合中的文档需要引用另外一个集合中的不同文档。例如,文档1的`DBRef`可能指向`managers`中的文档,而文档2中的`DBRef`可能指向`employees`中的文档。 + +#### 反规范化(Denormalization) #### +代替连接的另一种方法就是反规范化数据。在过去,反规范化是为性能敏感代码所设,或者是需要数据快照(例如审计日志)的时候才应用的。然而,随着NoSQL的日渐普及,有许多这样的数据库并不提供连接操作,于是作为规范建模的一部分,反规范化就越来越常见了。这样说并不是说您就需要为每个文档中的每一条信息创建副本。与此相反,与其在设计的时候被复制数据的担忧牵着走,还不如按照不同的信息应该归属于相应的文档这一思路来对数据建模。 + +比如说,假设您在编写一个论坛的应用程序。把一个`user`和一篇`post`关联起来的传统方法是在`posts`中加入一个`userid`的列。这样的模型中,如果要显示`posts`就不得不读取(连接)`users`。一种简单可行的替代方案就是直接把`name`和`userid`存储在`post`中。您甚至可以用嵌入文档来实现,比如说`user: {id: ObjectId('Something'), name: 'Leto'}`。当然,如果允许用户更改他们的用户名,那么每当有用户名修改的时候,您就需要去更新所有的文档了(这需要一个额外的查询)。 + +对一些人来说改用这种方法并非易事。甚至在一些情况下根本行不通。不过别不敢去尝试这种方法:有时候它不仅可行,而且就是正确的方法。 + +#### 您应该选择哪一种? #### +当处理一对多或是多对多问题的时候,采用id数组往往都是正确的策略。可以这么说,`DBRef`并不是那么常用,虽然您完全可以试着采用这项技术。这使得新手们在面临选择嵌入文档还是手工引用(manual reference)时犹豫不决。 + +首先,要知道目前一个单独的文档的大小限制是4MB,虽然已经比较大了。了解了这个限制可以为如何使用文档提供一些思路。目前看来多数的开发者还是大量地依赖手工引用来维护数据的关系。嵌入文档经常被使用,but mostly for small pieces of data which we want to always pull with the parent document。一个真实的例子,我把`accounts`文档嵌入存储在用户的文档中,就像这样: + + db.users.insert({name: 'leto', email: 'leto@dune.gov', account: {allowed_gholas: 5, spice_ration: 10}}) + +这不是说您就应该低估嵌入文档的作用,也不是说应该把它当成是鲜少用到的工具并直接忽略。将数据模型直接映射到目标对象上可以使问题变得更加简单,也往往因此而不再需要连接操作。当您知道MongoDB允许对嵌入文档的域进行查询并做索引后,这个说法就尤其显得正确了。 + +### 集合:少一些还是多一些? ### +既然集合不强制使用模式,那么就完全有可能用一个单一的集合以及一个不匹配的文档构建一个系统。以我所见过的情况,大部分的MongoDB系统都像您在关系数据库中所见到的那样布局。换句话说,如果在关系数据库中会用表,那么很有可能在MongoDB中就要用集合(多对多连接表在这里是一个不可忽视的例外) + +当把嵌入文档引进来的时候,讨论就会变得更加有意思了。最常见的例子就是博客系统。是应该分别维护`posts`和`comments`两个集合,还是在每个`post`中嵌入一个`comments`数组?暂且不考虑那个4MB的限制(哈姆雷特所有的评论也不超过200KB,谁的博客会比他更受欢迎?),大多数的开发者还是倾向于把数据划分开。因为这样既简洁又明确。 + +没有什么硬性的规定(呃,除了4MB的限制)。做了不同的尝试之后您就可以凭感觉知道怎样做是对的了。 + +### 本章小结 ### +本章的目的在于为您用MongoDB给数据建模提供一些有用的指引。用面向文档的系统来建模与用关系数据库不一样,但也不会相差很大。用MongoDB会有一个限制还有多出一些灵活性,不过对于新系统来说,一切都会很好的运行起来的。唯一有可能出错的情况就是不去尝试。 + +\clearpage + +## 第五章 - 何时使用MongoDB ## +至此您应该对MongoDB有了足够的了解并且知道在现有系统中何处以及怎样应用它了。然而,新的存储技术不止一个,让人很容易就被这么多的可选方案搞得不知所措。 + +对我来说,虽然与MongoDB无关,但最重要的一点是再也不会依赖于某一单一的方案来处理数据了。当然,对很多项目,很可能是大多数项目而言,采用单一的解决方案有很明显的好处,甚至是明智的选择。然而这里要强调的是您并非必须使用不同的技术,而是您可以这样做。只有您知道引入新技术带来的好处会不会大于采用新技术所需的代价。 + +说了这些,我希望基于目前对MongoDB的介绍,您会把它当成一个通用的方案。前面多次提到,面向文档的数据库和关系数据库有很多的共同点。因此,与其对之避而不谈,不如干脆把MongoDB视为关系数据库的平行方案(alternative。译者:这里的意思是MongoDB与关系数据库的功能类似但又有差异,如果说关系数据库是台式机,可以把MongoDB理解为笔记本电脑,两者有相似处,但是又各有优势,且没有哪一个能替代另一个)。只要是Lucene可以加强关系数据库全文索引能力的场合,或者是Redis可以当作持久性键-值存储的地方,MongoDB都可以用来集中存储管理这些数据。 + +注意,我没有说MongoDB是关系数据库的替代方案, 而是平行方案。作为一个工具,MongoDB能做的很多其他的工具也可以做到。有些方面MongoDB做得好些,有些方面差些。那么我们现在就来做进一步的分析。 + +### 无模式 ### +面向文档的数据库常见的一个卖点就是它是无模式的。这使得它比传统数据库的表更加灵活。我同意无模式是很不错的一个特性,但不是因为大多数人说的那些原因。 + +当人们谈到无模式时,神奇得就好像忽然间我们就可以把一堆毫不匹配的数据通通塞进去一样。确实有这么一些领域或是数据格式,如果用关系数据库来建模是很痛苦的,但我认为这些只是一些不常见的边缘情况。无模式是很酷,不过系统的大部分数据都将是有着良好结构的。偶尔使用不匹配是会很方便,尤其是在引入新功能的时候。可是事实上也可以考虑一下添加一个可以为空的列,这样问题就解决了。 + +我认为无模式设计的真正益处在于不需要过多的设置,以及与面向对象编程语言结合使用的时候更少的阻力。这一点在使用静态语言的时候更是如此。我曾在C#和Ruby上使用过MongoDB,其间的差别不是一般的大。Ruby的动态特性还有广受欢迎的ActiveRecord实现使得这门语言本身就已经降低了面向关系和面向对象编程之间差异带来的难度。并不是说MongoDB和Ruby不是一个好的组合,相反的它们还真的很搭。我要说的是Ruby程序员眼中MongoDB带来的应该只是一些改进,而对于C#或者Java开发者来说,MongoDB带来的是处理数据方式的翻天覆地的转变。 + +以一个驱动开发者的角度来看,如果想要保存一个对象,可以把数据串行化为JSON(严格来说应该是BSON,不过JSON也足够接近了)再发送给MongoDB。不需要做属性或类型映射。作为终端开发者的您自然也直接得到了好处。 + +### 写操作 ### +MongoDB擅长的一个特别角色是日志的记录。MongoDB有两点使得它的写操作非常快。第一,发出一条写命令后它会马上返回而不等待真正的写动作执行。第二,随着1.8中引入的日记功能(journaling),以及2.0中所做的优化加强,现在已经可以根据数据持久性来控制写操作的行为。这些设定的值,加上指定多少个服务器得到一份数据后才算是一次成功的写操作,在每次写的时候都是可以设置的。这些使得对写操作的性能以及数据的持久性的控制都上了一个档次。 + +除了上述性能上的因素,日志数据还是这样的一种数据格式:它们用无模式集合往往更有优势。最后,MongoDB还有一项技术叫做[定量集合(capped collection)](http://www.mongodb.org/display/DOCS/Capped+Collections)。目前为止,我们所创建的集合都是隐式创建的普通集合。我们可以用`db.createCollection`命令创建并标明它是给标量集合: + + //限制标量集合的大小为1MB + db.createCollection('logs', {capped: true, size: 1048576}) + +当上面的定量集合增长到1MB的限制时后,旧的文档就会被自动删除。可以用`max`来限制文档的个数而不是整个集合的尺寸。定量集合有一些有意思的特性。比如说,你可以不断的更新文档,但是文档不会变大。同时,它会保存插入的顺序,因此没有需要添加额外的索引来实现基于时间的排序。 + +是时候说明这个了:如果需要知道写操作有没有出错(这和默认的射后不理的写行为相反)(译者:fire-and-forget,[射后不理](http://zh.wikipedia.org/wiki/%E5%B0%84%E5%BE%8C%E4%B8%8D%E7%90%86))只需要再发一个命令:`db.getLastError()`。多数的驱动都会把这种行为封装成一个*安全的写操作*,用`{:safe => true}`作为`insert`的第二个参数来声明。 + +### 持久性(Durability) ### +在1.8版之前,MongoDB是不支持单服务器的持久性的。也就是说,一个服务器宕机就会导致数据丢失。当时的解决方法就是在多台服务器上运行MongoDB(MongoDB支持复制)。新加入到1.8的一个重要特性就是日记(journaling)。打开这个功能需要在我们最早设置MongoDB时创建的`mongodb.config`文件中加入一行`journal=true`(如果想要立即生效,还需要重启服务器)。您应该是会想要打开这项功能的。(在以后的版本中将会默认打开)。不过,在有些情况下,您可能会要关闭日记以增加吞吐量,哪怕这样做存在风险。(需要指出的是有些应用对损失一些数据还是可以接受的) + +关于持久性的话题只会在这里提及,因为为了克服MongoDB缺乏单服务器持久性的弊病人们已经做了大量的工作。这段话还可能会出现在以后的Google搜索结果中。您看到的那些说MongoDB这个缺点的信息都是过时了的。 + +### 全文搜索 ### +真正的全文搜索功能希望能够在将来的MongoDB版本中实现。有了它对数组的支持,基本的全文搜索应该是很容易实现的。至于更高级一点的功能,就需要依仗像Lucene/Solr之类的方案了。当然,这一点上其他很多关系数据库也是一样的。 + +### 事务(transaction) ### +MongoDB是不支持事务的。不过它有两个替代的方案,其中一个很不错但是不怎么有人用,另外一个很麻烦同时又很灵活。 + +第一个就是MongoDB的众多原子操作。只要它们能够解决你的问题,都很好用。之前已经介绍过一些简单的诸如`$inc`和`$set`。还有像`findAndModify`这样的原子操作,可以更新或是删除一个文档并返回修改过后的文档,且所有动作在一个原子操作内完成。 + +当原子操作不能满足要求时,可以退而尝试第二种方案:两阶段提交。两阶段提交之于事务就好比手工解引用(dereference,译者:关于这个词的翻译有很多种,用引、提领、解引用等,大家知道是什么意思就好。)之于连接。这是一个代码中实现的独立于存储系统的方案。两阶段提交其实在关系数据库中有普遍的应用,用以在多个数据库之间实现事务。MongoDB的网站有[一个例子](http://www.mongodb.org/display/DOCS/two-phase+commit)演示了最常见的场合(资金转账)。其主要思想就是把事务的状态储存在需要更新的文档中,并手工一步一步完成初始化-等待-提交或是回滚的每一个步骤。 + +MongoDB对嵌套文档以及无模式的支持使得两阶段提交稍微不那么痛苦了,不过这依旧不是一个很好的流程,尤其是当您刚刚开始学/用它的时候。 + +### 数据处理 ### +MongoDB依靠MapReduce来完成大部分的数据处理工作。它有一些[基本的聚合能力](http://www.mongodb.org/display/DOCS/Aggregation),尽管如此,无论在何种情况下您还是应该使用MapReduce。下一章我们将详细讨论MapReduce。现在把它当成是一个非常强大的工具,另外一种实现`group by`的方法。(这样说事实上低估了MapReduce)MapReduce的一个长处是它可以并行地处理大量的数据。可是MongoDB的实现却依赖于单线程的JavaScript。这又意味着什么呢?意味着如果是处理大量的数据,很可能还是需要用其他的工具,比如说Hadoop。幸好,这两个系统很好的实现了互补,且看这个[MongoDB的Hadoop适配器](https://github.com/mongodb/mongo-hadoop)。 + +当然,并行处理数据也不是关系数据库所擅长的。现在已经有计划在将来的MongoDB版本中加强处理非常大的数据的能力。 + +### 地理空间 ### +MongoDB的另外一个很强大的功能就是它对地理信息索引功能的支持。这个功能允许把X和Y坐标储存在文档中,然后可以用`$near`查找文档中靠近某个坐标的点,或是用`$within`找出位于某个矩形或是一个圆形中的点。这个功能可以用一些可视的例子来演示,如果您想了解多一些,我邀请您来试试这个[5分钟交互式地理空间教程](http://tutorial.mongly.com/geo/index) + +### 成熟度与可用工具 ### +你应该很可能早已经知道了答案,不过MongoDB明显比大多数的关系数据库要年轻。这个当然是您需要考虑的。究竟这个因素有多重要取决于您在做的是什么系统以及将怎样实现它。无论怎样,一个客观的评价是不会忽略这一事实:MongoDB很年轻而且周边可用的工具也不是很好用(虽然很多非常成熟的关系数据库可用的工具也很糟糕!)例如,不能支持十进制浮点数对货币数据系统来说就是一个很明显的问题(虽然不一定是致命的缺陷) + +正面点来看,MongoDB为很多语言提供了驱动,它的协议很现代简约,开发速度非常快。有很多对不成熟的工具心存疑虑的公司都将MongoDB用在了自己已经发布的产品中。这些疑虑当然是有根据的,可是短时间后都已经成为了历史。 + + +### 本章小结 ### +本章要传递的信息是大多数情况下MongoDB都可以替代关系数据库。它更简单更直接,更快而且对应用程序开发者的约束更少。MongoDB不支持事务确实是一个缺点也值得慎重考虑。但是当人们问道*在新数据存储阵营中MongoDB是怎样的一种技术?*时,答案很简单:**它很中庸**。(译者:这里的意思是,作为NoSQL方案的一种,MongoDB显得更通用一些,不像Lucene那样专为搜索而生,也不像结构严格的Redis。) + +\clearpage + +## 第六章 - MapReduce ## +MapReduce是一种数据处理的方法,有相比较为传统的方案它有两个显著的优势。第一个优势是它卓越的性能,也是最初开发MapReduce的主要目的。理论上MapReduce可以并行工作,可以利用多核/多CPU/多机器同时处理非常大量的数据。我们也说过,这点优势MongoDB无法利用上。第二个优势就是用户可以为数据处理编写真正的程序。与SQL相比,用MapReduce可以实现无限多种功能,在逼不得已寻求更专业的方案之前,MapReduce提供了更多的可能。 + +MapReduce这种模式越来越普及,几乎任何语言上都有它的实现:C#,Ruby,Java,Python等等。我要说的是一开始它看起来和其他方案很不一样而且很复杂,不过不要泄气,花些时间来实践。无论您用不用MongoDB,它都很值得您去了解。 + +### 理论与实践 ### +MapReduce的流程分两步。首先做映射(map)然后做缩减(reduce)。在映射时转换输入的文档并输出(emit)键-值组合(键或值可以很复杂)。在缩减时将一个键以及为该键输出的值的数组生成最终的结果。我们来看看这当中的每一步以及相应的输出。 + +下面的例子假设为某个数据源(比如说一个网页)生成每天的点击数。这相当于MapReduce的*hello world*。为了实现这个应用,我们需要有一个`hits`集合,其中有两个域:`resource`和`date`。我们设计的输出分为:`resource`,`year`,`month`,`day`以及`count`。 + +又假设`hits`的数据如下: + + resource date + index Jan 20 2010 4:30 + index Jan 20 2010 5:30 + about Jan 20 2010 6:00 + index Jan 20 2010 7:00 + about Jan 21 2010 8:00 + about Jan 21 2010 8:30 + index Jan 21 2010 8:30 + about Jan 21 2010 9:00 + index Jan 21 2010 9:30 + index Jan 22 2010 5:00 + +我们希望最终有下面的输出: + + resource year month day count + index 2010 1 20 3 + about 2010 1 20 1 + about 2010 1 21 3 + index 2010 1 21 2 + index 2010 1 22 1 + +当前分析的这个方法有一个好处,那就是通过存储输出的数据,报告很快就可以生成,且数据的增长是可控的。(对于上面的数据源,每天只需要增加最多一个文档) + +我们先专注于概念的理解,到了本章快结束时,会有数据和代码的示例供您亲自实验。 + +首先来看看映射函数。映射的目的在于输出(emit)值以便后续缩减。一个映射有可能不输出或者输出多次值。在我们的例子中,映射总是会输出一次(很正常的做法)。可以把这里的映射想象成遍历hits中的每一个文档。对于每个文档我们要输出一个包含了resource,year,month和day的键,还有一个简单的值,1: + + function() { + var key = { + resource: this.resource, + year: this.date.getFullYear(), + month: this.date.getMonth(), + day: this.date.getDate() + }; + emit(key, {count: 1}); + } + +`this`指的是当前正在分析的文档。希望看到下面映射输出可以让这个过程清楚一些。基于前面的数据,完整的映射输出应该是: + + {resource: 'index', year: 2010, month: 0, day: 20} => [{count: 1}, {count: 1}, {count:1}] + {resource: 'about', year: 2010, month: 0, day: 20} => [{count: 1}] + {resource: 'about', year: 2010, month: 0, day: 21} => [{count: 1}, {count: 1}, {count:1}] + {resource: 'index', year: 2010, month: 0, day: 21} => [{count: 1}, {count: 1}] + {resource: 'index', year: 2010, month: 0, day: 22} => [{count: 1}] + +了解这一中间步骤是了解MapReduce的关键。输出的值根据键的不同被组织成相应的数组。.NET和Java的程序员可以把这视为类型`IDictionary>`(.NET)或者是`HashMap`(Java)。 + + +接下来我们人为的修改一下映射函数的行为: + + function() { + var key = {resource: this.resource, year: this.date.getFullYear(), month: this.date.getMonth(), day: this.date.getDate()}; + if (this.resource == 'index' && this.date.getHours() == 4) { + emit(key, {count: 5}); + } else { + emit(key, {count: 1}); + } + } + +第一个中间输出因此变成: + + {resource: 'index', year: 2010, month: 0, day: 20} => [{count: 5}, {count: 1}, {count:1}] + +值得注意的是每一次输出是如何按照键的不同来分组生成新的值的。 + +缩减函数接受中间结果后产生了最后的结果。例子中的缩减函数见下: + + function(key, values) { + var sum = 0; + values.forEach(function(value) { + sum += value['count']; + }); + return {count: sum}; + }; + +得到的结果是: + + {resource: 'index', year: 2010, month: 0, day: 20} => {count: 3} + {resource: 'about', year: 2010, month: 0, day: 20} => {count: 1} + {resource: 'about', year: 2010, month: 0, day: 21} => {count: 3} + {resource: 'index', year: 2010, month: 0, day: 21} => {count: 2} + {resource: 'index', year: 2010, month: 0, day: 22} => {count: 1} + +MongoDB中的输出是: + + _id: {resource: 'home', year: 2010, month: 0, day: 20}, value: {count: 3} + +希望您注意到这个就是我们想要的结果了。 + +如果您有注意到,可能会问*为什么不直接用`sum = values.length`?*如果在计算值都是1的数组,这个方法确实是很有效的。可是实际上缩减函数不见得总是会得到完整的中间数据,比如说,不是: + + {resource: 'home', year: 2010, month: 0, day: 20} => [{count: 1}, {count: 1}, {count:1}] + +而是像下面这样调用Reduce: + + {resource: 'home', year: 2010, month: 0, day: 20} => [{count: 1}, {count: 1}] + {resource: 'home', year: 2010, month: 0, day: 20} => [{count: 2}, {count: 1}] + +结果应该还是3,不过计算的路径就不一样了。因此,缩减函数必须具有幂等性。也就是说,多次调用该函数和只调用一次的效果应该是一样的。 + +一个比较常见的做法是将多个缩减函数链接起来实现更加复杂的分析功能,不过我们在这里就不再深入了。 + +### Pure Practical ### +MongoDB中是对集合使用`mapReduce`的。`mapReduce`需要一个映射函数,一个缩减函数以及一个输出指令。在shell中我们可以创建并传递一个JavaScript函数的调用。大多数的库都支持这种将函数当作字符串值的方式(虽然有点难看)。首先我们还是来创建一些数据: + + db.hits.insert({resource: 'index', date: new Date(2010, 0, 20, 4, 30)}); + db.hits.insert({resource: 'index', date: new Date(2010, 0, 20, 5, 30)}); + db.hits.insert({resource: 'about', date: new Date(2010, 0, 20, 6, 0)}); + db.hits.insert({resource: 'index', date: new Date(2010, 0, 20, 7, 0)}); + db.hits.insert({resource: 'about', date: new Date(2010, 0, 21, 8, 0)}); + db.hits.insert({resource: 'about', date: new Date(2010, 0, 21, 8, 30)}); + db.hits.insert({resource: 'index', date: new Date(2010, 0, 21, 8, 30)}); + db.hits.insert({resource: 'about', date: new Date(2010, 0, 21, 9, 0)}); + db.hits.insert({resource: 'index', date: new Date(2010, 0, 21, 9, 30)}); + db.hits.insert({resource: 'index', date: new Date(2010, 0, 22, 5, 0)}); + +然后创建我们自己的映射和缩减函数(MongoDB的shell允许多行声明,回车之后您会看到*...*说明shell在等待后续的输入): + + var map = function() { + var key = {resource: this.resource, year: this.date.getFullYear(), month: this.date.getMonth(), day: this.date.getDate()}; + emit(key, {count: 1}); + }; + + var reduce = function(key, values) { + var sum = 0; + values.forEach(function(value) { + sum += value['count']; + }); + return {count: sum}; + }; +有了上面两个函数,就可以对`hits`集合使用`mapReduce`命令了: + + db.hits.mapReduce(map, reduce, {out: {inline:1}}) +执行上面的命令后,应该就可以看到期望的输出了。把`out`设成`inline`是为了把`mapReduce`的输出流直接返回到shell中显示。这个功能目前只能用于最多16MB的结果。另外的一个方法就是使用`{out: 'hit_stats'}`以把结果存储在`hit_stats`集合里: + + db.hits.mapReduce(map, reduce, {out: 'hit_stats'}); + db.hit_stats.find(); + +上面的命令执行之后,`hit_stats`中既有的数据就会丢失。如果用的是`{out: {merge: 'hit_stats'}}`已有键的值就会被新的值覆盖且新的键-值组合就会被作为新的文档插入到集合中。最后,我们可以用`reduce`函数中的`out`选项处理更复杂的情况(比如说插新)。 + +第三个参数是一个可选项,比如可以用来过滤、排序或是限制需要分析的文档。也可以将一个`finalize`方法应用到`reduce`之后的结果上。 + +### 本章小结 ### +这是介绍了MongoDB真正与众不同指出的第一个章节。如果您觉得很不自在,要知道您总是可以使用MongoDB的其他[聚合能力](http://www.mongodb.org/display/DOCS/Aggregation)事情变得简单一些。不过归根结底,MapReduce是MongoDB最吸引人的功能之一。学会编写映射函数和缩减函数的关键在于把`映射`输出的数据以及`缩减`所需要的数据可视化,并真正了解这些数据。 + +\clearpage + +## 第七章 - 性能与工具 ## +最后一章,我们将介绍一些性能相关的话题,以及MongoDB开发者可以使用的工具。我们不会过深地涉及这些话题,但当中重要的部分都会有所介绍。 + +### 索引 ### +最开始的时候我们介绍了特殊的`system.indexes`集合,它含有数据库中所有索引的信息。MongoDB中的索引和关系数据库很像:都有助于改进查询和排序的性能。索引是通过`ensureIndex`创建的: + + // "name"是域的名字 + db.unicorns.ensureIndex({name: 1}); + +并由`dropIndex`丢弃: + + db.unicorns.dropIndex({name: 1}); + +要创建唯一的索引可以将第二个参数`unique`设为`true`: + + db.unicorns.ensureIndex({name: 1}, {unique: true}); + +索引可以在嵌入域(使用`.`符号)和数组域上。也可以创建复合索引: + + db.unicorns.ensureIndex({name: 1, vampires: -1}); + +索引的顺序(1为升序,-1为降序)对于单键索引没有关系,不过对于复合索引来说在排序或是使用范围条件时就有影响了。 + +[索引页](http://www.mongodb.org/display/DOCS/Indexes)有更多关于索引的信息。 + +### Explain ### +要知道索引有没有使用索引,可以对游标使用`explain`方法: + + db.unicorns.find().explain() + +命令的结果告诉我们查询使用的是`BasicCursor`(也就是说没有用索引),扫描了12个对象,用了多长时间, 是否用了什么索引,如果有用索引的话还有一些额外有用的信息。 + +如果我们改用索引来查询,我们会看到的查询使用的`BtreeCursor`,以及用以查询的索引: + + db.unicorns.find({name: 'Pilot'}).explain() + +### 射后不理的写操作 ### +之前我们有提到过,MongoDB中的写操作默认为[射后不理](http://zh.wikipedia.org/wiki/%E5%B0%84%E5%BE%8C%E4%B8%8D%E7%90%86)。这样做可以获得一定的性能提高,同时也带来了系统崩溃时丢失数据的风险。有意思的是这种类型的写操作在插入/更新破坏了某唯一的约束时,是不返回错误的。若需要得到写失败的通知,就要在插入后调用`db.getLastError()`。很多驱动都把这一细节封装起来了,取而代之的是*安全的写*操作——往往会多提供一个参数用来设置。 + +可惜的是shell并不提供安全的插入,因此我们就无法实验这一特性了。 + +### 分片(sharding) ### +MongoDB支持自动分片。分片技术是将数据水平切分存储在多台服务器上以实现可扩展性的一种方法。比较简单的实现可以将所有以A至M字母开头的用户信息存在1号服务器上然后剩下的都存在2号服务器上。幸运的是,MongoDB的分片功能远远超过了这种简单的方法。不过分片已经超出了本书要讨论的范畴,但您应该知道它的存在并且在系统需要用到多台服务器时会考虑这种技术。 + +### 复制 ### +MongoDB的复制于关系数据库的复制类似。写入的数据发送到主服务器,主服务器再与其他从服务器进行同步。读操作可以选择在从服务器上做或者是在主服务器上做。当主服务器宕机的时候,可以将一台从服务器升级为新的主服务器。这样做可以分散系统的负荷,不过有读到陈旧数据的可能。MongoDB的复制也超出了本书要讨论的范围。 + +虽然复制可以提高性能(通过分散读操作),它的主要作用还是增加可靠性。将分片和复制结合是一种很普遍的做法。例如,每一个分片都可以由一个主服务器和一个从服务器维护。(从技术角度上还需要一个仲裁机以解决两个从服务器试图升级为主服务器的问题。不过仲裁机耗费的资源非常少,因此可以用在多个分片上) + +### 统计 ### +可以通过`db.stats()`获得数据库的数据统计信息。当中的大多数都和数据库的大小有关。也可以获取某个集合的统计信息。比如说可以用`db.unicorns.stats()`获得`unicorns`的相关信息。这些信息大部分还是和集合的大小相关。 + +### 网络接口 ### +在MongoDB启动时的信息中有一个基于网络的管理工具的链接(如果您在shell中向上翻页到启动`mongod`时的部分应该还可以看到)。可以在浏览器中输入以访问该工具。为了更好的使用这个工具,还需要在配置文件中加入`rest=true`并重启`mongod`进程。网络接口提供了很多关于服务器当前状态的信息。 + +### 分析器(Profiler) ### +下面的命令将启动MongoDB的分析器: + + db.setProfilingLevel(2); + +启动之后,可以运行下面的命令: + + db.unicorns.find({weight: {$gt: 600}}); + +再读取分析器中的值: + + db.system.profile.find() + +最后的输出提供了这些信息:什么时候运行了什么命令,有多少文档被扫描过以及返回了多少数据。 + +可以将参数设成`0`,用`setProfileLevel`再次关闭分析器。当把参数设为`1`时,只有对耗时多于100毫秒的查询才会进行分析。或者也可以在第二个参数中指明最少的时间,以毫秒为单位: + + //分析所有耗费时间多于一秒的操作 + db.setProfilingLevel(1, 1000); + +### 备份和恢复 ### +在MongoDB的`bin`目录中有一个`mongodump`可执行文件。执行这一文件将连接到localhost并且将所有的数据库备份到一个叫做`dump`的子目录中。更多的选项可以通过`mongodump --help`获得。常见的选项有`--db DBNAME`,用以对某一特定的数据库进行备份,还有`--collection COLLECTIONAME`可以用来备份一个集合。利用`bin`下的`mongorestore`可以恢复上一次的备份。同样的,用`--db`和`--collection`参数可以恢复某一指定的数据库或者集合。 + +例如,以下命令将把我们创建的`learn`数据库备份到`backup`目录(该命令是在终端窗口下输入的,而非mongo的shell中): + + mongodump --db learn --out backup + +如果只需要恢复`unicorns`集合,可以这样做: + + mongorestore --collection unicorns backup/learn/unicorns.bson + +需要指出的是`mongoexport`和`mongoimport`这两个可执行文件可以用以输出和导入JSON或是CSV中的数据。例如可以输出为JSON格式的数据: + + mongoexport --db learn -collection unicorns + +或者是CSV格式的输出: + + mongoexport --db learn -collection unicorns --csv -fields name,weight,vampires + +请注意,`mongoexport`和`mongoimport`并不总是能反应真实的数据。只有`mongodump`和`mongorestore`可以用以真正的备份。 + +### 本章小结 ### +在这章中我们看到了MongoDB的不同命令、工具以及性能方面的细节。并不是所有的东西都有介绍,我们只选了最常见的那些。MongoDB的索引和关系数据库中的索引很相像,其他的很多工具也是这样。但是,很多工具的使用在MongoDb中更简洁扼要。 + +\clearpage + +## 总结 ## +至此您对MongoDB的了解已经足以开始在实际项目中使用它了。关于MongoDB的远不止我们所介绍的这些,不过您需要做的下一件事应该是把在这里所学到的只是汇总起来,并熟悉您即将用到的驱动。MongoDb的[网站](http://www.mongodb.com/)有很多有用的信息。其[官方讨论组](http://groups.google.com/group/mongodb-user)则是一个问问题的好地方。 + +NoSQL的诞生不仅仅因为有必要,同时也是为了实践新的方法。应该承认的是这个领域一直在向前发展,尽管有时候会失败,但是如果我们不去尝试,就无法拥抱成功。我想,这应该是我们推进职业生涯的正确方法。 diff --git a/docs/redis.md b/docs/redis.md new file mode 100755 index 0000000..fb28aa3 --- /dev/null +++ b/docs/redis.md @@ -0,0 +1,694 @@ +\thispagestyle{empty} +\changepage{}{}{}{-0.5cm}{}{2cm}{}{}{} +![The Little Redis Book cn, By Karl Seguin, Translate By Jason Lai](title.png)\ + +\clearpage +\changepage{}{}{}{0.5cm}{}{-2cm}{}{}{} + +## 关于此书 + +### 许可证 + +《The Little Redis Book》是经由Attribution-NonCommercial 3.0 Unported license许可的,你不需要为此书付钱。 + +你可以自由地对此书进行复制,分发,修改或者展示等操作。当然,你必须知道且认可这本书的作者是Karl Seguin,译者是赖立维,而且不应该将此书用于商业用途。 + +关于这个**许可证**的*详细描述*在这里: + + + +### 关于作者 + +作者Karl Seguin是一名在多项技术领域浸淫多年的开发者。他是开源软件计划的活跃贡献者,同时也是一名技术作者以及业余演讲者。他写过若干关于Radis的文章以及一些工具。在他的一个面向业余游戏开发者的免费服务里,Redis为其中的评级和统计功能提供了支持:[mogade.com](http://mogade.com/)。 + +Karl之前还写了[《The Little MongoDB Book》](http://openmymind.net/2011/3/28/The-Little-MongoDB-Book/),这是一本免费且受好评,关于MongoDB的书。 + +他的博客是,你也可以关注他的Twitter帐号,via [@karlseguin](http://twitter.com/karlseguin)。 + +### 关于译者 + +译者 赖立维 是一名长在天朝的普通程序员,对许多技术都有浓厚的兴趣,是开源软件的支持者,Emacs的轻度使用者。 + +虽然译者已经很认真地对待这次翻译,但是限于水平有限,肯定会有不少错漏,如果发现该书的翻译有什么需要修改,可以通过他的邮箱与他联系。他的邮箱是。 + +### 致谢 + +必须特别感谢[Perry Neal](https://twitter.com/perryneal)一直以来的指导,我的眼界、触觉以及激情都来源于你。你为我提供了无价的帮助,感谢你。 + +### 最新版本 + +此书的最新有效资源在: + + +中文版是英文版的一个分支,最新的中文版本在: + + +\clearpage + +## 简介 + +最近几年来,关于持久化和数据查询的相关技术,其需求已经增长到了让人惊讶的程度。可以断言,关系型数据库再也不是放之四海皆准。换一句话说,围绕数据的解决方案不可能再只有唯一一种。 + +对于我来说,在众多新出现的解决方案和工具里,最让人兴奋的,无疑是Redis。为什么?首先是因为其让人不可思议的容易学习,只需要简短的几个小时学习时间,就能对Redis有个大概的认识。还有,Redis在处理一组特定的问题集的同时能保持相当的通用性。更准确地说就是,Redis不会尝试去解决关于数据的所有事情。在你足够了解Redis后,事情就会变得越来越清晰,什么是可行的,什么是不应该由Redis来处理的。作为一名开发人员,如此的经验当是相当的美妙。 + +当你能仅使用Redis去构建一个完整系统时,我想大多数人将会发现,Redis能使得他们的许多数据方案变得更为通用,不论是一个传统的关系型数据库,一个面向文档的系统,或是其它更多的东西。这是一种用来实现某些特定特性的解决方法。就类似于一个索引引擎,你不会在Lucene上构建整个程序,但当你需要足够好的搜索,为什么不使用它呢?这对你和你的用户都有好处。当然,关于Redis和索引引擎之间相似性的讨论到此为止。 + +本书的目的是向读者传授掌握Redis所需要的基本知识。我们将会注重于学习Redis的5种数据结构,并研究各种数据建模方法。我们还会接触到一些主要的管理细节和调试技巧。 + +## 入门 + +每个人的学习方式都不一样,有的人喜欢亲自实践学习,有的喜欢观看教学视频,还有的喜欢通过阅读来学习。对于Redis,没有什么比亲自实践学习来得效果更好的了。Redis的安装非常简单。而且通过随之安装的一个简单的命令解析程序,就能处理我们想做的一切事情。让我们先花几分钟的时间把Redis安装到我们的机器上。 + +### Windows平台 + +Redis并没有官方支持Windows平台,但还是可供选择。你不会想在这里配置实际的生产环境,不过在我过往的开发经历里并没有感到有什么限制。 + +首先进入,然后下载最新的版本(应该会在列表的最上方)。 + +获取zip文件,然后根据你的系统架构,打开`64bit`或`32bit`文件夹。 + +### *nix和MacOSX平台 + +对于*nix和MacOSX平台的用户,从源文件来安装是你的最佳选择。通过最新的版本号来选择,有效地址于。在编写此书的时候,最新的版本是2.4.6,我们可以运行下面的命令来安装该版本: + + wget http://redis.googlecode.com/files/redis-2.4.6.tar.gz + tar xzf redis-2.4.6.tar.gz + cd redis-2.4.6 + make + +(当然,Redis同样可以通过套件管理程序来安装。例如,使用Homebrew的MaxOSX用户可以只键入`brew install redis`即可。) + +如果你是通过源文件来安装,二进制可执行文件会被放置在`src`目录里。通过运行`cd src`可跳转到`src`目录。 + +### 运行和连接Redis + +如果一切都工作正常,那Redis的二进制文件应该已经可以曼妙地跳跃于你的指尖之下。Redis只有少量的可执行文件,我们将着重于Redis的服务器和命令行界面(一个类DOS的客户端)。首先,让我们来运行服务器。在Windows平台,双击`redis-server`,在*nix/MacOSX平台则运行`./redis-server`. + +如果你仔细看了启动信息,你会看到一个警告,指没能找到`redis.conf`文件。Redis将会采用内置的默认设置,这对于我们将要做的已经足够了。 + +然后,通过双击`redis-cli`(Windows平台)或者运行`./redis-cli`(*nix/MacOSX平台),启动Redis的控制台。控制台将会通过默认的端口(6379)来连接本地运行的服务器。 + +可以在命令行界面键入`info`命令来查看一切是不是都运行正常。你会很乐意看到这么一大组关键字-值(key-value)对的显示,这为我们查看服务器的状态提供了大量有效信息。 + +如果在上面的启动步骤里遇到什么问题,我建议你到[Redis的官方支持组](https://groups.google.com/forum/#!forum/redis-db)里获取帮助。 + +## 驱动Redis + +很快你就会发现,Redis的API就如一组定义明确的函数那般容易理解。Redis具有让人难以置信的简单性,其操作过程也同样如此。这意味着,无论你是使用命令行程序,或是使用你喜欢的语言来驱动,整体的感觉都不会相差多少。因此,相对于命令行程序,如果你更愿意通过一种编程语言去驱动Redis,你不会感觉到有任何适应的问题。如果真想如此,可以到Redis的[客户端推荐页面](http://redis.io/clients)下载适合的Redis载体。 + +\clearpage + +## 第1章 - 基础知识 + +是什么使Redis显得这么特别?Redis具体能解决什么类型的问题?要实际应用Redis,开发者必须储备什么知识?在我们能回答这么一些问题之前,我们需要明白Redis到底是什么。 + +Redis通常被人们认为是一种持久化的存储器关键字-值型存储(in-memory persistent key-value store)。我认为这种对Redis的描述并不太准确。Redis的确是将所有的数据存放于存储器(更多是是按位存储),而且也确实通过将数据写入磁盘来实现持久化,但是Redis的实际意义比单纯的关键字-值型存储要来得深远。纠正脑海里的这种误解观点非常关键,否则你对于Redis之道以及其应用的洞察力就会变得越发狭义。 + +事实是,Redis引入了5种不同的数据结构,只有一个是典型的关键字-值型结构。理解Redis的关键就在于搞清楚这5种数据结构,其工作的原理都是如何,有什么关联方法以及你能怎样应用这些数据结构去构建模型。首先,让我们来弄明白这些数据结构的实际意义。 + +应用上面提及的数据结构概念到我们熟悉的关系型数据库里,我们可以认为其引入了一个单独的数据结构——表格。表格既复杂又灵活,基于表格的存储和管理,没有多少东西是你不能进行建模的。然而,这种通用性并不是没有缺点。具体来说就是,事情并不是总能达到假设中的简单或者快速。相对于这种普遍适用(one-size-fits-all)的结构体系,我们可以使用更为专门化的结构体系。当然,因此可能有些事情我们会完成不了(至少,达不到很好的程度)。但话说回来,这样做就能确定我们可以获得想象中的简单性和速度吗? + +针对特定类型的问题使用特定的数据结构?我们不就是这样进行编程的吗?你不会使用一个散列表去存储每份数据,也不会使用一个标量变量去存储。对我来说,这正是Redis的做法。如果你需要处理标量、列表、散列或者集合,为什么不直接就用标量、列表、散列和集合去存储他们?为什么不是直接调用`exists(key)`去检测一个已存在的值,而是要调用其他比O(1)(常量时间查找,不会因为待处理元素的增长而变慢)慢的操作? + +### 数据库(Databases) + +与你熟悉的关系型数据库一致,Redis有着相同的数据库基本概念,即一个数据库包含一组数据。典型的数据库应用案例是,将一个程序的所有数据组织起来,使之与另一个程序的数据保持独立。 + +在Redis里,数据库简单的使用一个数字编号来进行辨认,默认数据库的数字编号是`0`。如果你想切换到一个不同的数据库,你可以使用`select`命令来实现。在命令行界面里键入`select 1`,Redis应该会回复一条`OK`的信息,然后命令行界面里的提示符会变成类似`redis 127.0.0.1:6379[1]>`这样。如果你想切换回默认数据库,只要在命令行界面键入`select 0`即可。 + +### 命令、关键字和值(Commands, Keys and Values) + +Redis不仅仅是一种简单的关键字-值型存储,从其核心概念来看,Redis的5种数据结构中的每一个都至少有一个关键字和一个值。在转入其它关于Redis的有用信息之前,我们必须理解关键字和值的概念。 + +关键字(Keys)是用来标识数据块。我们将会很常跟关键字打交道,不过在现在,明白关键字就是类似于`users:leto`这样的表述就足够了。一般都能很好地理解到,这样关键字包含的信息是一个名为`leto`的用户。这个关键字里的冒号没有任何特殊含义,对于Redis而言,使用分隔符来组织关键字是很常见的方法。 + +值(Values)是关联于关键字的实际值,可以是任何东西。有时候你会存储字符串,有时候是整数,还有时候你会存储序列化对象(使用JSON、XML或其他格式)。在大多数情况下,Redis会把值看做是一个字节序列,而不会关注它们实质上是什么。要注意,不同的Redis载体处理序列化会有所不同(一些会让你自己决定)。因此,在这本书里,我们将仅讨论字符串、整数和JSON。 + +现在让我们活动一下手指吧。在命令行界面键入下面的命令: + + set users:leto "{name: leto, planet: dune, likes: [spice]}" + +这就是Redis命令的基本构成。首先我们要有一个确定的命令,在上面的语句里就是`set`。然后就是相应的参数,`set`命令接受两个参数,包括要设置的关键字,以及相应要设置的值。很多的情况是,命令接受一个关键字(当这种情况出现,其经常是第一个参数)。你能想到如何去获取这个值吗?我想你会说(当然一时拿不准也没什么): + + get users:leto + +关键字和值的是Redis的基本概念,而`get`和`set`命令是对此最简单的使用。你可以创建更多的用户,去尝试不同类型的关键字以及不同的值,看看一些不同的组合。 + +### 查询(Querying) + +随着学习的持续深入,两件事情将变得清晰起来。对于Redis而言,关键字就是一切,而值是没有任何意义。更通俗来看就是,Redis不允许你通过值来进行查询。回到上面的例子,我们就不能查询生活在`dune`行星上的用户。 + +对许多人来说,这会引起一些担忧。在我们生活的世界里,数据查询是如此的灵活和强大,而Redis的方式看起来是这么的原始和不高效。不要让这些扰乱你太久。要记住,Redis不是一种普遍使用(one-size-fits-all)的解决方案,确实存在这么一些事情是不应该由Redis来解决的(因为其查询的限制)。事实上,在考虑了这些情况后,你会找到新的方法去构建你的数据。 + +很快,我们就能看到更多实际的用例。很重要的一点是,我们要明白关于Redis的这些基本事实。这能帮助我们弄清楚为什么值可以是任何东西,因为Redis从来不需要去读取或理解它们。而且,这也可以帮助我们理清思路,然后去思考如何在这个新世界里建立模型。 + +### 存储器和持久化(Memory and Persistence) + +我们之前提及过,Redis是一种持久化的存储器内存储(in-memory persistent store)。对于持久化,默认情况下,Redis会根据已变更的关键字数量来进行判断,然后在磁盘里创建数据库的快照(snapshot)。你可以对此进行设置,如果X个关键字已变更,那么每隔Y秒存储数据库一次。默认情况下,如果1000个或更多的关键字已变更,Redis会每隔60秒存储数据库;而如果9个或更少的关键字已变更,Redis会每隔15分钟存储数据库。 + +除了创建磁盘快照外,Redis可以在附加模式下运行。任何时候,如果有一个关键字变更,一个单一附加(append-only)的文件会在磁盘里进行更新。在一些情况里,虽然硬件或软件可能发生错误,但用那60秒有效数据存储去换取更好性能是可以接受的。而在另一些情况里,这种损失就难以让人接受,Redis为你提供了选择。在第5章里,我们将会看到第三种选择,其将持久化任务减荷到一个从属数据库里。 + +至于存储器,Redis会将所有数据都保留在存储器中。显而易见,运行Redis具有不低的成本:因为RAM仍然是最昂贵的服务器硬件部件。 + +我很清楚有一些开发者对即使是一点点的数据空间都是那么的敏感。一本《威廉·莎士比亚全集》需要近5.5MB的存储空间。对于缩放的需求,其它的解决方案趋向于IO-bound或者CPU-bound。这些限制(RAM或者IO)将会需要你去理解更多机器实际依赖的数据类型,以及应该如何去进行存储和查询。除非你是存储大容量的多媒体文件到Redis中,否则存储器内存储应该不会是一个问题。如果这对于一个程序是个问题,你就很可能不会用IO-bound的解决方案。 + +Redis有虚拟存储器的支持。然而,这个功能已经被认为是失败的了(通过Redis的开发者),而且它的使用已经被废弃了。 + +(从另一个角度来看,一本5.5MB的《威廉·莎士比亚全集》可以通过压缩减小到近2MB。当然,Redis不会自动对值进行压缩,但是因为其将所有值都看作是字节,没有什么限制让你不能对数据进行压缩/解压,通过牺牲处理时间来换取存储空间。) + +### 整体来看(Putting It Together) + +我们已经接触了好几个高层次的主题。在继续深入Redis之前,我想做的最后一件事情是将这些主题整合起来。这些主题包括,查询的限制,数据结构以及Redis在存储器内存储数据的方法。 + +当你将这3个主题整合起来,你最终会得出一个绝妙的结论:速度。一些人可能会想,当然Redis会很快速,要知道所有的东西都在存储器里。但这仅仅是其中的一部分,让Redis闪耀的真正原因是其不同于其它解决方案的特殊数据结构。 + +能有多快速?这依赖于很多东西,包括你正在使用着哪个命令,数据的类型等等。但Redis的性能测试是趋向于数万或数十万次操作**每秒**。你可以通过运行`redis-benchmark`(就在`redis-server`和`redis-cli`的同一个文件夹里)来进行测试。 + +我曾经试过将一组使用传统模型的代码转向使用Redis。在传统模型里,运行一个我写的载入测试,需要超过5分钟的时间来完成。而在Redis里,只需要150毫秒就完成了。你不会总能得到这么好的收获,但希望这能让你对我们所谈的东西有更清晰的理解。 + +理解Redis的这个特性很重要,因为这将影响到你如何去与Redis进行交互。拥有SQL背景的程序员通常会致力于让数据库的数据往返次数减至最小。这对于任何系统都是个好建议,包括Redis。然而,考虑到我们是在处理比较简单的数据结构,有时候我们还是需要与Redis服务器频繁交互,以达到我们的目的。刚开始的时候,可能会对这种数据访问模式感到不太自然。实际上,相对于我们通过Redis获得的高性能而言,这仅仅是微不足道的损失。 + +### 小结 + +虽然我们只接触和摆弄了Redis的冰山一角,但我们讨论的主题已然覆盖了很大范围内的东西。如果觉得有些事情还是不太清楚(例如查询),不用为此而担心,在下一章我们将会继续深入探讨,希望你的问题都能得到解答。 + +这一章的要点包括: + +* 关键字(Keys)是用于标识一段数据的一个字符串 + +* 值(Values)是一段任意的字节序列,Redis不会关注它们实质上是什么 + +* Redis展示了(也实现了)5种专门的数据结构 + +* 上面的几点使得Redis快速而且容易使用,但要知道Redis并不适用于所有的应用场景 + +\clearpage + +## 第2章 - 数据结构 + +现在开始将探究Redis的5种数据结构,我们会解释每种数据结构都是什么,包含了什么有效的方法(Method),以及你能用这些数据结构处理哪些类型的特性和数据。 + +目前为止,我们所知道的Redis构成仅包括命令、关键字和值,还没有接触到关于数据结构的具体概念。当我们使用`set`命令时,Redis是怎么知道我们是在使用哪个数据结构?其解决方法是,每个命令都相对应于一种特定的数据结构。例如,当你使用`set`命令,你就是将值存储到一个字符串数据结构里。而当你使用`hset`命令,你就是将值存储到一个散列数据结构里。考虑到Redis的关键字集很小,这样的机制具有相当的可管理性。 + +**[Redis的网站](http://redis.io/commands)里有着非常优秀的参考文档,没有任何理由去重造轮子。但为了搞清楚这些数据结构的作用,我们将会覆盖那些必须知道的重要命令。** + +没有什么事情比高兴的玩和试验有趣的东西来得更重要的了。在任何时候,你都能通过键入`flushdb`命令将你数据库里的所有值清除掉,因此,不要再那么害羞了,去尝试做些疯狂的事情吧! + +### 字符串(Strings) + +在Redis里,字符串是最基本的数据结构。当你在思索着关键字-值对时,你就是在思索着字符串数据结构。不要被名字给搞混了,如之前说过的,你的值可以是任何东西。我更喜欢将他们称作“标量”(Scalars),但也许只有我才这样想。 + +我们已经看到了一个常见的字符串使用案例,即通过关键字存储对象的实例。有时候,你会频繁地用到这类操作: + + set users:leto "{name: leto, planet: dune, likes: [spice]}" + +除了这些外,Redis还有一些常用的操作。例如,`strlen `能用来获取一个关键字对应值的长度;`getrange `将返回指定范围内的关键字对应值;`append `会将value附加到已存在的关键字对应值中(如果该关键字并不存在,则会创建一个新的关键字-值对)。不要犹豫,去试试看这些命令吧。下面是我得到的: + + > strlen users:leto + (integer) 42 + + > getrange users:leto 27 40 + "likes: [spice]" + + > append users:leto " OVER 9000!!" + (integer) 54 + +现在你可能会想,这很好,但似乎没有什么意义。你不能有效地提取出一段范围内的JSON文件,或者为其附加一些值。你是对的,这里的经验是,一些命令,尤其是关于字符串数据结构的,只有在给定了明确的数据类型后,才会有实际意义。 + +之前我们知道了,Redis不会去关注你的值是什么东西。通常情况下,这没有错。然而,一些字符串命令是专门为一些类型或值的结构而设计的。作为一个有些含糊的用例,我们可以看到,对于一些自定义的空间效率很高的(space-efficient)串行化对象,`append`和`getrange`命令将会很有用。对于一个更为具体的用例,我们可以再看一下`incr`、`incrby`、`decr`和`decrby`命令。这些命令会增长或者缩减一个字符串数据结构的值: + + > incr stats:page:about + (integer) 1 + > incr stats:page:about + (integer) 2 + + > incrby ratings:video:12333 5 + (integer) 5 + > incrby ratings:video:12333 3 + (integer) 8 + +由此你可以想象到,Redis的字符串数据结构能很好地用于分析用途。你还可以去尝试增长`users:leto`(一个不是整数的值),然后看看会发生什么(应该会得到一个错误)。 + +更为进阶的用例是`setbit`和`getbit`命令。“今天我们有多少个独立用户访问”是个在Web应用里常见的问题,有一篇[精彩的博文](http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/),在里面可以看到Spool是如何使用这两个命令有效地解决此问题。对于1.28亿个用户,一部笔记本电脑在不到50毫秒的时间里就给出了答复,而且只用了16MB的存储空间。 + +最重要的事情不是在于你是否明白位图(Bitmaps)的工作原理,或者Spool是如何去使用这些命令,而是应该要清楚Redis的字符串数据结构比你当初所想的要有用许多。然而,最常见的应用案例还是上面我们给出的:存储对象(简单或复杂)和计数。同时,由于通过关键字来获取一个值是如此之快,字符串数据结构很常被用来缓存数据。 + +### 散列(Hashes) + +我们已经知道把Redis称为一种关键字-值型存储是不太准确的,散列数据结构是一个很好的例证。你会看到,在很多方面里,散列数据结构很像字符串数据结构。两者显著的区别在于,散列数据结构提供了一个额外的间接层:一个域(Field)。因此,散列数据结构中的`set`和`get`是: + + hset users:goku powerlevel 9000 + hget users:goku powerlevel + +相关的操作还包括在同一时间设置多个域、同一时间获取多个域、获取所有的域和值、列出所有的域或者删除指定的一个域: + + hmset users:goku race saiyan age 737 + hmget users:goku race powerlevel + hgetall users:goku + hkeys users:goku + hdel users:goku age + +如你所见,散列数据结构比普通的字符串数据结构具有更多的可操作性。我们可以使用一个散列数据结构去获得更精确的描述,是存储一个用户,而不是一个序列化对象。从而得到的好处是能够提取、更新和删除具体的数据片段,而不必去获取或写入整个值。 + +对于散列数据结构,可以从一个经过明确定义的对象的角度来考虑,例如一个用户,关键之处在于要理解他们是如何工作的。从性能上的原因来看,这是正确的,更具粒度化的控制可能会相当有用。在下一章我们将会看到,如何用散列数据结构去组织你的数据,使查询变得更为实效。在我看来,这是散列真正耀眼的地方。 + +### 列表(Lists) + +对于一个给定的关键字,列表数据结构让你可以存储和处理一组值。你可以添加一个值到列表里、获取列表的第一个值或最后一个值以及用给定的索引来处理值。列表数据结构维护了值的顺序,提供了基于索引的高效操作。为了跟踪在网站里注册的最新用户,我们可以维护一个`newusers`的列表: + + lpush newusers goku + ltrim newusers 0 50 + +**(译注:`ltrim`命令的具体构成是`LTRIM Key start stop`。要理解`ltrim`命令,首先要明白Key所存储的值是一个列表,理论上列表可以存放任意个值。对于指定的列表,根据所提供的两个范围参数start和stop,`ltrim`命令会将指定范围外的值都删除掉,只留下范围内的值。)** + +首先,我们将一个新用户推入到列表的前端,然后对列表进行调整,使得该列表只包含50个最近被推入的用户。这是一种常见的模式。`ltrim`是一个具有O(N)时间复杂度的操作,N是被删除的值的数量。从上面的例子来看,我们总是在插入了一个用户后再进行列表调整,实际上,其将具有O(1)的时间复杂度(因为N将永远等于1)的常数性能。 + +这是我们第一次看到一个关键字的对应值索引另一个值。如果我们想要获取最近的10个用户的详细资料,我们可以运行下面的组合操作: + + keys = redis.lrange('newusers', 0, 10) + redis.mget(*keys.map {|u| "users:#{u}"}) + +我们之前谈论过关于多次往返数据的模式,上面的两行Ruby代码为我们进行了很好的演示。 + +当然,对于存储和索引关键字的功能,并不是只有列表数据结构这种方式。值可以是任意的东西,你可以使用列表数据结构去存储日志,也可以用来跟踪用户浏览网站时的路径。如果你过往曾构建过游戏,你可能会使用列表数据结构去跟踪用户的排队活动。 + +### 集合(Sets) + +集合数据结构常常被用来存储只能唯一存在的值,并提供了许多的基于集合的操作,例如并集。集合数据结构没有对值进行排序,但是其提供了高效的基于值的操作。使用集合数据结构的典型用例是朋友名单的实现: + + sadd friends:leto ghanima paul chani jessica + sadd friends:duncan paul jessica alia + +不管一个用户有多少个朋友,我们都能高效地(O(1)时间复杂度)识别出用户X是不是用户Y的朋友: + + sismember friends:leto jessica + sismember friends:leto vladimir + +而且,我们可以查看两个或更多的人是不是有共同的朋友: + + sinter friends:leto friends:duncan + +甚至可以在一个新的关键字里存储结果: + + sinterstore friends:leto_duncan friends:leto friends:duncan + +有时候需要对值的属性进行标记和跟踪处理,但不能通过简单的复制操作完成,集合数据结构是解决此类问题的最好方法之一。当然,对于那些需要运用集合操作的地方(例如交集和并集),集合数据结构就是最好的选择。 + +### 分类集合(Sorted Sets) + +最后也是最强大的数据结构是分类集合数据结构。如果说散列数据结构类似于字符串数据结构,主要区分是域(field)的概念;那么分类集合数据结构就类似于集合数据结构,主要区分是标记(score)的概念。标记提供了排序(sorting)和秩划分(ranking)的功能。如果我们想要一个秩分类的朋友名单,可以这样做: + + zadd friends:duncan 70 ghanima 95 paul 95 chani 75 jessica 1 vladimir + +对于`duncan`的朋友,要怎样计算出标记(score)为90或更高的人数? + + zcount friends:duncan 90 100 + +如何获取`chani`在名单里的秩(rank)? + + zrevrank friends:duncan chani + +**(译注:`zrank`命令的具体构成是`ZRANK Key menber`,要知道Key存储的Sorted Set默认是根据Score对各个menber进行升序的排列,该命令就是用来获取menber在该排列里的次序,这就是所谓的秩。)** + +我们使用了`zrevrank`命令而不是`zrank`命令,这是因为Redis的默认排序是从低到高,但是在这个例子里我们的秩划分是从高到低。对于分类集合数据结构,最常见的应用案例是用来实现排行榜系统。事实上,对于一些基于整数排序,且能以标记(score)来进行有效操作的东西,使用分类集合数据结构来处理应该都是不错的选择。 + +### 小结 + +对于Redis的5种数据结构,我们进行了高层次的概述。一件有趣的事情是,相对于最初构建时的想法,你经常能用Redis创造出一些更具实效的事情。对于字符串数据结构和分类集合数据结构的使用,很有可能存在一些构建方法是还没有人想到的。当你理解了那些常用的应用案例后,你将发现Redis对于许多类型的问题,都是很理想的选择。还有,不要因为Redis展示了5种数据结构和相应的各种方法,就认为你必须要把所有的东西都用上。只使用一些命令去构建一个特性是很常见的。 + +\clearpage + +## 第3章 - 使用数据结构 + +在上一章里,我们谈论了Redis的5种数据结构,对于一些可能的用途也给出了用例。现在是时候来看看一些更高级,但依然很常见的主题和设计模式。 + +### 大O表示法(Big O Notation) + +在本书中,我们之前就已经看到过大O表示法,包括O(1)和O(N)的表示。大O表示法的惯常用途是,描述一些用于处理一定数量元素的行为的综合表现。在Redis里,对于一个要处理一定数量元素的命令,大O表示法让我们能了解该命令的大概运行速度。 + +在Redis的文档里,每一个命令的时间复杂度都用大O表示法进行了描述,还能知道各命令的具体性能会受什么因素影响。让我们来看看一些用例。 + +常数时间复杂度O(1)被认为是最快速的,无论我们是在处理5个元素还是5百万个元素,最终都能得到相同的性能。对于`sismember`命令,其作用是告诉我们一个值是否属于一个集合,时间复杂度为O(1)。`sismember`命令很强大,很大部分的原因是其高效的性能特征。许多Redis命令都具有O(1)的时间复杂度。 + +对数时间复杂度O(log(N))被认为是第二快速的,其通过使需扫描的区间不断皱缩来快速完成处理。使用这种“分而治之”的方式,大量的元素能在几个迭代过程里被快速分解完整。`zadd`命令的时间复杂度就是O(log(N)),其中N是在分类集合中的元素数量。 + +再下来就是线性时间复杂度O(N),在一个表格的非索引列里进行查找就需要O(N)次操作。`ltrim`命令具有O(N)的时间复杂度,但是,在`ltrim`命令里,N不是列表所拥有的元素数量,而是被删除的元素数量。从一个具有百万元素的列表里用`ltrim`命令删除1个元素,要比从一个具有一千个元素的列表里用`ltrim`命令删除10个元素来的快速(实际上,两者很可能会是一样快,因为两个时间都非常的小)。 + +根据给定的最小和最大的值的标记,`zremrangebyscore`命令会在一个分类集合里进行删除元素操作,其时间复杂度是O(log(N)+M)。这看起来似乎有点儿杂乱,通过阅读文档可以知道,这里的N指的是在分类集合里的总元素数量,而M则是被删除的元素数量。可以看出,对于性能而言,被删除的元素数量很可能会比分类集合里的总元素数量更为重要。 + +**(译注:`zremrangebyscore`命令的具体构成是`ZREMRANGEBYSCORE Key max mix`。)** + +对于`sort`命令,其时间复杂度为O(N+M*log(M)),我们将会在下一章谈论更多的相关细节。从`sort`命令的性能特征来看,可以说这是Redis里最复杂的一个命令。 + +还存在其他的时间复杂度描述,包括O(N^2)和O(C^N)。随着N的增大,其性能将急速下降。在Redis里,没有任何一个命令具有这些类型的时间复杂度。 + +值得指出的一点是,在Redis里,当我们发现一些操作具有O(N)的时间复杂度时,我们可能可以找到更为好的方法去处理。 + +**(译注:对于Big O Notation,相信大家都非常的熟悉,虽然原文仅仅是对该表示法进行简单的介绍,但限于个人的算法知识和文笔水平实在有限,此小节的翻译让我头痛颇久,最终成果也确实难以让人满意,望见谅。)** + +### 仿多关键字查询(Pseudo Multi Key Queries) + +时常,你会想通过不同的关键字去查询相同的值。例如,你会想通过电子邮件(当用户开始登录时)去获取用户的具体信息,或者通过用户id(在用户登录后)去获取。有一种很不实效的解决方法,其将用户对象分别放置到两个字符串值里去: + + set users:leto@dune.gov "{id: 9001, email: 'leto@dune.gov', ...}" + set users:9001 "{id: 9001, email: 'leto@dune.gov', ...}" + +这种方法很糟糕,如此不但会产生两倍数量的内存,而且这将会成为数据管理的恶梦。 + +如果Redis允许你将一个关键字链接到另一个的话,可能情况会好很多,可惜Redis并没有提供这样的功能(而且很可能永远都不会提供)。Redis发展到现在,其开发的首要目的是要保持代码和API的整洁简单,关键字链接功能的内部实现并不符合这个前提(对于关键字,我们还有很多相关方法没有谈论到)。其实,Redis已经提供了解决的方法:散列。 + +使用散列数据结构,我们可以摆脱重复的缠绕: + + set users:9001 "{id: 9001, email: leto@dune.gov, ...}" + hset users:lookup:email leto@dune.gov 9001 + +我们所做的是,使用域来作为一个二级索引,然后去引用单个用户对象。要通过id来获取用户信息,我们可以使用一个普通的`get`命令: + + get users:9001 + +而如果想通过电子邮箱来获取用户信息,我们可以使用`hget`命令再配合使用`get`命令(Ruby代码): + + id = redis.hget('users:lookup:email', 'leto@dune.gov') + user = redis.get("users:#{id}") + +你很可能将会经常使用这类用法。在我看来,这就是散列真正耀眼的地方。在你了解这类用法之前,这可能不是一个明显的用例。 + +### 引用和索引(References and Indexes) + +我们已经看过几个关于值引用的用例,包括介绍列表数据结构时的用例,以及在上面使用散列数据结构来使查询更灵活一些。进行归纳后会发现,对于那些值与值间的索引和引用,我们都必须手动的去管理。诚实来讲,这确实会让人有点沮丧,尤其是当你想到那些引用相关的操作,如管理、更新和删除等,都必须手动的进行时。在Redis里,这个问题还没有很好的解决方法。 + +我们已经看到,集合数据结构很常被用来实现这类索引: + + sadd friends:leto ghanima paul chani jessica + +这个集合里的每一个成员都是一个Redis字符串数据结构的引用,而每一个引用的值则包含着用户对象的具体信息。那么如果`chani`改变了她的名字,或者删除了她的帐号,应该如何处理?从整个朋友圈的关系结构来看可能会更好理解,我们知道,`chani`也有她的朋友: + + sadd friends_of:chani leto paul + +如果你有什么待处理情况像上面那样,那在维护成本之外,还会有对于额外索引值的处理和存储空间的成本。这可能会令你感到有点退缩。在下一小节里,我们将会谈论减少使用额外数据交互的性能成本的一些方法(在第1章我们粗略地讨论了下)。 + +如果你确实在担忧着这些情况,其实,关系型数据库也有同样的开销。索引需要一定的存储空间,必须通过扫描或查找,然后才能找到相应的记录。其开销也是存在的,当然他们对此做了很多的优化工作,使之变得更为有效。 + +再次说明,需要在Redis里手动地管理引用确实是颇为棘手。但是,对于你关心的那些问题,包括性能或存储空间等,应该在经过测试后,才会有真正的理解。我想你会发现这不会是一个大问题。 + +### 数据交互和流水线(Round Trips and Pipelining) + +我们已经提到过,与服务器频繁交互是Redis的一种常见模式。这类情况可能很常出现,为了使我们能获益更多,值得仔细去看看我们能利用哪些特性。 + +许多命令能接受一个或更多的参数,也有一种关联命令(sister-command)可以接受多个参数。例如早前我们看到过`mget`命令,接受多个关键字,然后返回值: + + keys = redis.lrange('newusers', 0, 10) + redis.mget(*keys.map {|u| "users:#{u}"}) + +或者是`sadd`命令,能添加一个或多个成员到集合里: + + sadd friends:vladimir piter + sadd friends:paul jessica leto "leto II" chani + +Redis还支持流水线功能。通常情况下,当一个客户端发送请求到Redis后,在发送下一个请求之前必须等待Redis的答复。使用流水线功能,你可以发送多个请求,而不需要等待Redis响应。这不但减少了网络开销,还能获得性能上的显著提高。 + +值得一提的是,Redis会使用存储器去排列命令,因此批量执行命令是一个好主意。至于具体要多大的批量,将取决于你要使用什么命令(更明确来说,该参数有多大)。另一方面来看,如果你要执行的命令需要差不多50个字符的关键字,你大概可以对此进行数千或数万的批量操作。 + +对于不同的Redis载体,在流水线里运行命令的方式会有所差异。在Ruby里,你传递一个代码块到`pipelined`方法: + + redis.pipelined do + 9001.times do + redis.incr('powerlevel') + end + end + +正如你可能猜想到的,流水线功能可以实际地加速一连串命令的处理。 + +### 事务(Transactions) + +每一个Redis命令都具有原子性,包括那些一次处理多项事情的命令。此外,对于使用多个命令,Redis支持事务功能。 + +你可能不知道,但Redis实际上是单线程运行的,这就是为什么每一个Redis命令都能够保证具有原子性。当一个命令在执行时,没有其他命令会运行(我们会在往后的章节里简略谈论一下Scaling)。在你考虑到一些命令去做多项事情时,这会特别的有用。例如: + +`incr`命令实际上就是一个`get`命令然后紧随一个`set`命令。 + +`getset`命令设置一个新的值然后返回原始值。 + +`setnx`命令首先测试关键字是否存在,只有当关键字不存在时才设置值 + +虽然这些都很有用,但在实际开发时,往往会需要运行具有原子性的一组命令。若要这样做,首先要执行`multi`命令,紧随其后的是所有你想要执行的命令(作为事务的一部分),最后执行`exec`命令去实际执行命令,或者使用`discard`命令放弃执行命令。Redis的事务功能保证了什么? + +* 事务中的命令将会按顺序地被执行 + +* 事务中的命令将会如单个原子操作般被执行(没有其它的客户端命令会在中途被执行) + +* 事务中的命令要么全部被执行,要么不会执行 + +你可以(也应该)在命令行界面对事务功能进行一下测试。还有一点要注意到,没有什么理由不能结合流水线功能和事务功能。 + + multi + hincrby groups:1percent balance -9000000000 + hincrby groups:99percent balance 9000000000 + exec + +最后,Redis能让你指定一个关键字(或多个关键字),当关键字有改变时,可以查看或者有条件地应用一个事务。这是用于当你需要获取值,且待运行的命令基于那些值时,所有都在一个事务里。对于上面展示的代码,我们不能去实现自己的`incr`命令,因为一旦`exec`命令被调用,他们会全部被执行在一块。我们不能这么做: + + redis.multi() + current = redis.get('powerlevel') + redis.set('powerlevel', current + 1) + redis.exec() + +**(译注:虽然Redis是单线程运行的,但是我们可以同时运行多个Redis客户端进程,常见的并发问题还是会出现。像上面的代码,在`get`运行之后,`set`运行之前,`powerlevel`的值可能会被另一个Redis客户端给改变,从而造成错误。)** + +这些不是Redis的事务功能的工作。但是,如果我们增加一个`watch`到`powerlevel`,我们可以这样做: + + redis.watch('powerlevel') + current = redis.get('powerlevel') + redis.multi() + redis.set('powerlevel', current + 1) + redis.exec() + +在我们调用`watch`后,如果另一个客户端改变了`powerlevel`的值,我们的事务将会运行失败。如果没有客户端改变`powerlevel`的值,那么事务会继续工作。我们可以在一个循环里运行这些代码,直到其能正常工作。 + +### 关键字反模式(Keys Anti-Pattern) + +在下一章中,我们将会谈论那些没有确切关联到数据结构的命令,其中的一些是管理或调试工具。然而有一个命令我想特别地在这里进行谈论:`keys`命令。这个命令需要一个模式,然后查找所有匹配的关键字。这个命令看起来很适合一些任务,但这不应该用在实际的产品代码里。为什么?因为这个命令通过线性扫描所有的关键字来进行匹配。或者,简单地说,这个命令太慢了。 + +人们会如此去使用这个命令?一般会用来构建一个本地的Bug追踪服务。每一个帐号都有一个`id`,你可能会通过一个看起来像`bug:account_id:bug_id`的关键字,把每一个Bug存储到一个字符串数据结构值中去。如果你在任何时候需要查询一个帐号的Bug(显示它们,或者当用户删除了帐号时删除掉这些Bugs),你可能会尝试去使用`keys`命令: + + keys bug:1233:* + +更好的解决方法应该使用一个散列数据结构,就像我们可以使用散列数据结构来提供一种方法去展示二级索引,因此我们可以使用域来组织数据: + + hset bugs:1233 1 "{id:1, account: 1233, subject: '...'}" + hset bugs:1233 2 "{id:2, account: 1233, subject: '...'}" + +从一个帐号里获取所有的Bug标识,可以简单地调用`hkeys bugs:1233`。去删除一个指定的Bug,可以调用`hdel bugs:1233 2`。如果要删除了一个帐号,可以通过`del bugs:1233`把关键字删除掉。 + +### 小结 + +结合这一章以及前一章,希望能让你得到一些洞察力,了解如何使用Redis去支持(Power)实际项目。还有其他的模式可以让你去构建各种类型的东西,但真正的关键是要理解基本的数据结构。你将能领悟到,这些数据结构是如何能够实现你最初视角之外的东西。 + +\clearpage + +## 第4章 超越数据结构 + +5种数据结构组成了Redis的基础,其他没有关联特定数据结构的命令也有很多。我们已经看过一些这样的命令:`info`, `select`, `flushdb`, `multi`, `exec`, `discard`, `watch`和`keys `。这一章将看看其他的一些重要命令。 + +### 使用期限(Expiration) + +Redis允许你标记一个关键字的使用期限。你可以给予一个Unix时间戳形式(自1970年1月1日起)的绝对时间,或者一个基于秒的存活时间。这是一个基于关键字的命令,因此其不在乎关键字表示的是哪种类型的数据结构。 + + expire pages:about 30 + expireat pages:about 1356933600 + +第一个命令将会在30秒后删除掉关键字(包括其关联的值)。第二个命令则会在2012年12月31日上午12点删除掉关键字。 + +这让Redis能成为一个理想的缓冲引擎。通过`ttl`命令,你可以知道一个关键字还能够存活多久。而通过`persist`命令,你可以把一个关键字的使用期限删除掉。 + + ttl pages:about + persist pages:about + +最后,有个特殊的字符串命令,`setex`命令让你可以在一个单独的原子命令里设置一个字符串值,同时里指定一个生存期(这比任何事情都要方便)。 + + setex pages:about 30 '

about us

....' + +### 发布和订阅(Publication and Subscriptions) + +Redis的列表数据结构有`blpop`和`brpop`命令,能从列表里返回且删除第一个(或最后一个)元素,或者被堵塞,直到有一个元素可供操作。这可以用来实现一个简单的队列。 + +**(译注:对于`blpop`和`brpop`命令,如果列表里没有关键字可供操作,连接将被堵塞,直到有另外的Redis客户端使用`lpush`或`rpush`命令推入关键字为止。)** + +此外,Redis对于消息发布和频道订阅有着一流的支持。你可以打开第二个`redis-cli`窗口,去尝试一下这些功能。在第一个窗口里订阅一个频道(我们会称它为`warnings`): + + subscribe warnings + +其将会答复你订阅的信息。现在,在另一个窗口,发布一条消息到`warnings`频道: + + publish warnings "it's over 9000!" + +如果你回到第一个窗口,你应该已经接收到`warnings`频道发来的消息。 + +你可以订阅多个频道(`subscribe channel1 channel2 ...`),订阅一组基于模式的频道(`psubscribe warnings:*`),以及使用`unsubscribe`和`punsubscribe`命令停止监听一个或多个频道,或一个频道模式。 + +最后,可以注意到`publish`命令的返回值是1,这指出了接收到消息的客户端数量。 + +### 监控和延迟日志(Monitor and Slow Log) + +`monitor`命令可以让你查看Redis正在做什么。这是一个优秀的调试工具,能让你了解你的程序如何与Redis进行交互。在两个`redis-cli`窗口中选一个(如果其中一个还处于订阅状态,你可以使用`unsubscribe`命令退订,或者直接关掉窗口再重新打开一个新窗口)键入`monitor`命令。在另一个窗口,执行任何其他类型的命令(例如`get`或`set`命令)。在第一个窗口里,你应该可以看到这些命令,包括他们的参数。 + +在实际生产环境里,你应该谨慎运行`monitor`命令,这真的仅仅就是一个很有用的调试和开发工具。除此之外,没有更多要说的了。 + +随同`monitor`命令一起,Redis拥有一个`slowlog`命令,这是一个优秀的性能剖析工具。其会记录执行时间超过一定数量**微秒**的命令。在下一章节,我们会简略地涉及如何配置Redis,现在你可以按下面的输入配置Redis去记录所有的命令: + + config set slowlog-log-slower-than 0 + +然后,执行一些命令。最后,你可以检索到所有日志,或者检索最近的那些日志: + + slowlog get + slowlog get 10 + +通过键入`slowlog len`,你可以获取延迟日志里的日志数量。 + +对于每个被你键入的命令,你应该查看4个参数: + +* 一个自动递增的id + +* 一个Unix时间戳,表示命令开始运行的时间 + +* 一个微妙级的时间,显示命令运行的总时间 + +* 该命令以及所带参数 + +延迟日志保存在存储器中,因此在生产环境中运行(即使有一个低阀值)也应该不是一个问题。默认情况下,它将会追踪最近的1024个日志。 + +### 排序(Sort) + +`sort`命令是Redis最强大的命令之一。它让你可以在一个列表、集合或者分类集合里对值进行排序(分类集合是通过标记来进行排序,而不是集合里的成员)。下面是一个`sort`命令的简单用例: + + rpush users:leto:guesses 5 9 10 2 4 10 19 2 + sort users:leto:guesses + +这将返回进行升序排序后的值。这里有一个更高级的例子: + + sadd friends:ghanima leto paul chani jessica alia duncan + sort friends:ghanima limit 0 3 desc alpha + +上面的命令向我们展示了,如何对已排序的记录进行分页(通过`limit`),如何返回降序排序的结果(通过`desc`),以及如何用字典序排序代替数值序排序(通过`alpha`)。 + +`sort`命令的真正力量是其基于引用对象来进行排序的能力。早先的时候,我们说明了列表、集合和分类集合很常被用于引用其他的Redis对象,`sort`命令能够解引用这些关系,而且通过潜在值来进行排序。例如,假设我们有一个Bug追踪器能让用户看到各类已存在问题。我们可能使用一个集合数据结构去追踪正在被监视的问题: + + sadd watch:leto 12339 1382 338 9338 + +你可能会有强烈的感觉,想要通过id来排序这些问题(默认的排序就是这样的),但是,我们更可能是通过问题的严重性来对这些问题进行排序。为此,我们要告诉Redis将使用什么模式来进行排序。首先,为了可以看到一个有意义的结果,让我们添加多一点数据: + + set severity:12339 3 + set severity:1382 2 + set severity:338 5 + set severity:9338 4 + +要通过问题的严重性来降序排序这些Bug,你可以这样做: + + sort watch:leto by severity:* desc + +Redis将会用存储在列表(集合或分类集合)中的值去替代模式中的`*`(通过`by`)。这会创建出关键字名字,Redis将通过查询其实际值来排序。 + +在Redis里,虽然你可以有成千上万个关键字,类似上面展示的关系还是会引起一些混乱。幸好,`sort`命令也可以工作在散列数据结构及其相关域里。相对于拥有大量的高层次关键字,你可以利用散列: + + hset bug:12339 severity 3 + hset bug:12339 priority 1 + hset bug:12339 details "{id: 12339, ....}" + + hset bug:1382 severity 2 + hset bug:1382 priority 2 + hset bug:1382 details "{id: 1382, ....}" + + hset bug:338 severity 5 + hset bug:338 priority 3 + hset bug:338 details "{id: 338, ....}" + + hset bug:9338 severity 4 + hset bug:9338 priority 2 + hset bug:9338 details "{id: 9338, ....}" + +所有的事情不仅变得更为容易管理,而且我们能通过`severity`或`priority`来进行排序,还可以告诉`sort`命令具体要检索出哪一个域的数据: + + sort watch:leto by bug:*->priority get bug:*->details + +相同的值替代出现了,但Redis还能识别`->`符号,用它来查看散列中指定的域。里面还包括了`get`参数,这里也会进行值替代和域查看,从而检索出Bug的细节(details域的数据)。 + +对于太大的集合,`sort`命令的执行可能会变得很慢。好消息是,`sort`命令的输出可以被存储起来: + + sort watch:leto by bug:*->priority get bug:*->details store watch_by_priority:leto + +使用我们已经看过的`expiration`命令,再结合`sort`命令的`store`能力,这是一个美妙的组合。 + +### 小结 + +这一章主要关注那些非特定数据结构关联的命令。和其他事情一样,它们的使用依情况而定。构建一个程序或特性时,可能不会用到使用期限、发布和订阅或者排序等功能。但知道这些功能的存在是很好的。而且,我们也只接触到了一些命令。还有更多的命令,当你消化理解完这本书后,非常值得去浏览一下[完整的命令列表](http://redis.io/commands)。 + +\clearpage + +## 第5章 - 管理 + +在最后一章里,我们将集中谈论Redis运行中的一些管理方面内容。这是一个不完整的Redis管理指南,我们将会回答一些基本的问题,初接触Redis的新用户可能会很感兴趣。 + +### 配置(Configuration) + +当你第一次运行Redis的服务器,它会向你显示一个警告,指`redis.conf`文件没有被找到。这个文件可以被用来配置Redis的各个方面。一个充分定义(well-documented)的`redis.conf`文件对各个版本的Redis都有效。范例文件包含了默认的配置选项,因此,对于想要了解设置在干什么,或默认设置是什么,都会很有用。你可以在找到这个文件。 + +**这个配置文件针对的是Redis 2.4.6,你应该用你的版本号替代上面URL里的"2.4.6"。运行`info`命令,其显示的第一个值就是Redis的版本号。** + +因为这个文件已经是充分定义(well-documented),我们就不去再进行设置了。 + +除了通过`redis.conf`文件来配置Redis,`config set`命令可以用来对个别值进行设置。实际上,在将`slowlog-log-slower-than`设置为0时,我们就已经使用过这个命令了。 + +还有一个`config get`命令能显示一个设置值。这个命令支持模式匹配,因此如果我们想要显示关联于日志(logging)的所有设置,我们可以这样做: + + config get *log* + +### 验证(Authentication) + +通过设置`requirepass`(使用`config set`命令或`redis.conf`文件),可以让Redis需要一个密码验证。当`requirepass`被设置了一个值(就是待用的密码),客户端将需要执行一个`auth password`命令。 + +一旦一个客户端通过了验证,就可以在任意数据库里执行任何一条命令,包括`flushall`命令,这将会清除掉每一个数据库里的所有关键字。通过配置,你可以重命名一些重要命令为混乱的字符串,从而获得一些安全性。 + + rename-command CONFIG 5ec4db169f9d4dddacbfb0c26ea7e5ef + rename-command FLUSHALL 1041285018a942a4922cbf76623b741e + +或者,你可以将新名字设置为一个空字符串,从而禁用掉一个命令。 + +### 大小限制(Size Limitations) + +当你开始使用Redis,你可能会想知道,我能使用多少个关键字?还可能想知道,一个散列数据结构能有多少个域(尤其是当你用它来组织数据时),或者是,一个列表数据结构或集合数据结构能有多少个元素?对于每一个实例,实际限制都能达到亿万级别(hundreds of millions)。 + +### 复制(Replication) + +Redis支持复制功能,这意味着当你向一个Redis实例(Master)进行写入时,一个或多个其他实例(Slaves)能通过Master实例来保持更新。可以在配置文件里设置`slaveof`,或使用`slaveof`命令来配置一个Slave实例。对于那些没有进行这些设置的Redis实例,就可能一个Master实例。 + +为了更好保护你的数据,复制功能拷贝数据到不同的服务器。复制功能还能用于改善性能,因为读取请求可以被发送到Slave实例。他们可能会返回一些稍微滞后的数据,但对于大多数程序来说,这是一个值得做的折衷。 + +遗憾的是,Redis的复制功能还没有提供自动故障恢复。如果Master实例崩溃了,一个Slave实例需要手动的进行升级。如果你想使用Redis去达到某种高可用性,对于使用心跳监控(heartbeat monitoring)和脚本自动开关(scripts to automate the switch)的传统高可用性工具来说,现在还是一个棘手的难题。 + +### 备份文件(Backups) + +备份Redis非常简单,你可以将Redis的快照(snapshot)拷贝到任何地方,包括S3、FTP等。默认情况下,Redis会把快照存储为一个名为`dump.rdb`的文件。在任何时候,你都可以对这个文件执行`scp`、`ftp`或`cp`等常用命令。 + +有一种常见情况,在Master实例上会停用快照以及单一附加文件(aof),然后让一个Slave实例去处理备份事宜。这可以帮助减少Master实例的载荷。在不损害整体系统响应性的情况下,你还可以在Slave实例上设置更多主动存储的参数。 + +### 缩放和Redis集群(Scaling and Redis Cluster) + +复制功能(Replication)是一个成长中的网站可以利用的第一个工具。有一些命令会比另外一些来的昂贵(例如`sort`命令),将这些运行载荷转移到一个Slave实例里,可以保持整体系统对于查询的快速响应。 + +此外,通过分发你的关键字到多个Redis实例里,可以达到真正的缩放Redis(记住,Redis是单线程的,这些可以运行在同一个逻辑框里)。随着时间的推移,你将需要特别注意这些事情(尽管许多的Redis载体都提供了consistent-hashing算法)。对于数据水平分布(horizontal distribution)的考虑不在这本书所讨论的范围内。这些东西你也很可能不需要去担心,但是,无论你使用哪一种解决方案,有一些事情你还是必须意识到。 + +好消息是,这些工作都可在Redis集群下进行。不仅提供水平缩放(包括均衡),为了高可用性,还提供了自动故障恢复。 + +高可用性和缩放是可以达到的,只要你愿意为此付出时间和精力,Redis集群也使事情变得简单多了。 + +### 小结 + +在过去的一段时间里,已经有许多的计划和网站使用了Redis,毫无疑问,Redis已经可以应用于实际生产中了。然而,一些工具还是不够成熟,尤其是一些安全性和可用性相关的工具。对于Redis集群,我们希望很快就能看到其实现,这应该能为一些现有的管理挑战提供处理帮忙。 + +\clearpage + +## 总结 + +在许多方面,Redis体现了一种简易的数据处理方式,其剥离掉了大部分的复杂性和抽象,并可有效的在不同系统里运行。不少情况下,选择Redis不是最佳的选择。在另一些情况里,Redis就像是为你的数据提供了特别定制的解决方案。 + +最终,回到我最开始所说的:Redis很容易学习。现在有许多的新技术,很难弄清楚哪些才真正值得我们花时间去学习。如果你从实际好处来考虑,Redis提供了他的简单性。我坚信,对于你和你的团队,学习Redis是最好的技术投资之一。