Redis实战

978-7-115-40284-4
作者: 【美】Josiah L. Carlson(约西亚 L.卡尔森)
译者: 黄健宏
编辑: 杨海玲
分类: Redis

图书目录:

详情

本书深入浅出地介绍了Redis的五种数据类型,并通过多个实用例子展示了Redis的用法;除此之外,书中还讲述了Redis的优化方法以及扩展方法,是一本对于学习和使用 Redis 来说不可多得的参考书籍。本书既涵盖了命令用法等入门主题,也包含了复制、集群、性能扩展等深入主题,所以无论是Redis新手还是有一定经验的Redis使用者,应该都能从本书中获益。

图书摘要

版权信息

书名:Redis实战

ISBN:978-7-115-40284-4

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Josiah L. Carlson

  译    黄健宏

  责任编辑 杨海玲

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Original English language edition, entitled Redis in Action by Josiah L. Carlson published by Manning Publications Co., 209 Bruce Park Avenue, Greenwich, CT 06830. Copyright © 2013 by Manning Publications Co.

Simplified Chinese-language edition copyright ©2015 by Posts & Telecom Press. All rights reserved.

本书中文简体字版由Manning Publications Co.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。


本书深入浅出地介绍了Redis的5种数据类型,并通过多个实用示例展示了Redis的用法。除此之外,书中还讲述了Redis的优化方法以及扩展方法,是一本对于学习和使用Redis来说不可多得的参考书籍。

本书一共由三个部分组成。第一部分对Redis进行了介绍,说明了Redis的基本使用方法、它拥有的5种数据结构以及操作这5种数据结构的命令,并讲解了如何使用Redis去构建文章聚合网站、cookie、购物车、网页缓存、数据库行缓存等一系列程序。第二部分对Redis命令进行了更详细的介绍,并展示了如何使用Redis去构建更为复杂的辅助工具和应用程序,并在最后展示了如何使用Redis去构建一个简单的社交网站。第三部分对Redis用户经常会遇到的一些问题进行了介绍,讲解了降低Redis内存占用的方法、扩展Redis性能的方法以及使用Lua语言进行脚本编程的方法。

本书既涵盖了命令用法等入门主题,也包含了复制、集群、性能扩展等深入主题,所以无论是Redis新手还是有一定经验的Redis使用者,应该都能从本书中获益。本书面向具有基本数据库概念的读者,读者无须预先了解任何NoSQL知识,也不必具备任何Redis使用经验。


把这本书献给我亲爱的妻子See luan,以及我的宝贝女儿Mikela。


大家好,我是本书的译者黄健宏(huangz)。

本书是《Redis in Action》一书的中文翻译版,该书是一本广受欢迎的Redis著作,因为书中内容贴近实战而受到了不少赞许,是学习和深入了解Redis不可不读的一本书。

承蒙出版社和编辑的厚爱,我有幸担任本书的译者一职。为了不辜负出版社、编辑以及读者们的期待,我把大量心思和时间都投入到了本书的翻译工作当中,希望能够尽我所能地把最好的译作带给大家,而您正在阅读的这本书就是这一努力的成果。

尽管我已经努力地给大家呈现一个高质量的译本,但是因为本人的翻译水平和Redis水平都还有很多不足的方面,所以本书肯定也会有许多不尽如人意的地方,如果读者能够联系我并把您认为做得不够好的地方告诉我,我将不胜感激。

我的联系方式可以在huangz.me上面找到,欢迎读者就本书给我提供意见、建议或是问题反馈,非常感谢大家对本书的支持!

我为本书创建了支持网站redisinaction.com,读者可以在这个网站上面看到本书的购买链接、试读章样、内容简介、作者介绍、译者介绍等信息,也可以通过网站附带的留言系统进行留言。

为了方便读者学习书中展示的程序源码,我还把这些源码中的注释从英文翻译成了中文,这些带有中文注释的源码也可以在支持网站上面下载到。

感谢杨海玲编辑在本书的翻译过程中对我的支持和信任,如果没有她的帮助,我是绝对没办法完成这本书的翻译工作的。

感谢冯春丽细致入微地检查和修正工作,她发现了许多我没有注意到的错误,改正了许多我写下的乱糟糟的句子。

感谢fleuria和Juanito Fatas,他们最先阅读了本书的译文,并给了我很多反馈意见,让我获益良多。

最后,感谢我的家人和朋友,以及各个社交网站上面一直关注本书翻译进度的读者们,他们的支持和鼓励帮助我顺利地完成了这本译作。


黄健宏(huangz),男,1990 年出生,目前是程序员、技术图书作者和译者。著有《Redis设计与实现》,翻译了《Redis命令参考》《Disque 使用教程》等技术文档。想要了解更多关于黄健宏的信息,请访问他的个人网站huangz.me。


Redis 是我在大约3年前为了解决一个实际问题而创造出来的:简单来说,当时我在尝试做一件使用硬盘存储关系数据库(on-disk SQL database)无法完成的事情——在一台我能够支付得起的小虚拟机上面处理大量写入负载。

我要解决的问题在概念上并不复杂:多个网站会通过一个小型的JavaScript追踪器(tracker)连续不断地向我的服务器发送页面访问记录(page view),而我的服务器需要为每个网站保存一定数量的最新页面访问记录,并通过网页将这些记录实时地展示给用户观看。

在最大负载达到每秒数千条页面记录的情况下,无论我使用什么样的数据库模式(schema),无论我如何进行优化,我所使用的关系数据库都没办法在这个小虚拟机上处理如此大的负载。因为囊中羞涩,我没办法对虚拟机进行升级,并且我觉得应该有更简单的方法来处理一个由推入值组成的列表。最终,我决定自己写一个实验性质的内存数据库原型(prototype),这个数据库使用列表作为基本数据类型,并且能够对列表的两端执行常数时间复杂度的弹出(pop)和推入(push)操作。长话短说吧,这个内存数据库的想法的确奏效了,于是我用C语言重写了最初的数据库原型,并给它加上了基于子进程实现的持久化特性,Redis就这样诞生了。

在Redis诞生数年之后的今天,这个项目已经发生了显著的变化:我们现在拥有了一个更为健壮的系统,并且随着Redis 2.6的发布,开发的重点已经转移到实现集群以及高可用特性上面,Redis正在进入它的成熟期。在我看来,Redis生态系统中进步最为明显的一个地方,就是redis.io网站以及Redis Google Group这些由用户和贡献者组成的社区。数以千计的人通过GitHub的问题反馈系统参与到了这个项目里面,他们为Redis编写客户端库、提交补丁并帮助其他遇到麻烦的用户。

时至今日,Redis仍然是一个BSD授权的社区项目,它没有那些需要付钱才能使用的闭源插件或者功能增强版。Redis的参考文档非常详细和准确,在遇到问题时也很容易就可以找到Redis开发者或者专家来为你排忧解难。

Redis始于实用主义——它是一个程序员因为找不到合适的工具来解决手头上的问题而发明的,这是我认为理论性书籍无法很好地介绍Redis的原因,这也是我喜欢《Redis实战》(Redis in Action)的原因:这本书是为那些想要解决问题的人而写的,它没有乏味地介绍API,而是通过一系列引人入胜的例子深入地探究了Redis的各项特性以及数据类型。

值得一提的是,《Redis实战》同样来源于Redis社区:本书的作者Josiah在出版这本书之前,已经在很多不同的方面帮助了数以百计的Redis用户——从模式设计到硬件延迟问题,他的建议和贡献在Redis Group界面随处可见。

本书另一个非常好的地方在于它介绍了服务器运维方面的主题:实际上大部分人在开发应用程序的同时也需要自己部署服务器,而理解服务器运维操作、了解正在使用的硬件和服务器软件的基本限制,有助于写出最大限度地利用硬件和服务器软件的应用程序。

综上所述,《Redis实战》将是一本把读者带入Redis世界、向读者指明正确方向从而避免常见陷阱的书。我认为《Redis实战》对于Redis的生态系统非常有帮助,Redis的用户应该都会喜欢这本书。

——Salvatore Sanfilippo,“Redis之父”


Chris Testa是我在圣莫尼卡Google分部工作时认识的一个朋友,我从2010年3月开始和他一起在加利福尼亚州贝弗利山的一间小创业公司工作,Chris是公司的领头和主管,而我则受聘于他成为公司研究部门的架构师。

在对某个不相关的问题进行了一个下午的讨论之后,Chris向我推荐了Redis,他认为我这个理论计算机科学专业毕业的人应该会对这个数据库感兴趣。在使用Redis并按照自己的想法对Redis打补丁几个星期之后,我开始参与邮件列表里面的讨论,并向其他人提供建议或者补丁。

随着时间的推移,我将Redis广泛应用到了我们公司的各个项目里面:搜索、广告定向引擎、Twitter分析引擎以及一些将架构中的各个不同部分连接起来的小工具,所有这些项目都要求我学习更多关于Redis的知识。每当有其他Redis使用者在邮件列表里面提问的时候,我总会情不自禁地给出我的建议(我最喜欢回答的是与职位搜索有关的问题,本书的7.4节对此进行了介绍),并因此成为Redis邮件列表里面发言最积极的用户之一。

2011年9月下旬,当时我正在巴黎度蜜月,Manning出版社的策划编辑Michael Stephens给我打来了电话,但因为我的手机只能在美国使用,所以我未能接到Michael打来的电话。之后又由于手机固件bug的缘故,直到10月的第2周,我才收到Michael发给我的短信。

当我终于收到短信并与Michael联系上的时候,我才知道Manning出版社打算出版一本《Redis实战》。在阅读了相关的邮件列表并且向人们咨询应该由谁来写这本书的时候,我的名字出现了。幸运的是,在我回电话的时候,Manning出版社仍在接受关于《Redis实战》一书的提案。

在对本书的提案进行了几个星期的讨论和数次修改之后(提案的内容主要来源于我平时在Redis邮件列表发表的帖子),Manning出版社接受了我的提案,然后我开始了本书的写作工作。转眼之间,现在已经是我和Michael首次交谈之后的第17个月了,《Redis实战》一书已经基本完成,只剩下一些收尾的工作了。我花费了一整年的所有夜晚和假日,通过编写这本书来帮助其他人理解和使用我认为最有趣的技术——它比我在20年前的圣诞节第一次坐在电脑前面以来所知道的大部分技术都要有趣。

虽然自己未能有足够的远见来亲自发明Redis是有点儿遗憾,不过至少现在我有机会为它写一本书了。


我要感谢我的编辑,Manning的Beth Lexleigh,感谢她对我的整个写作过程给予的帮助:你的耐心指导和悉心教诲让我获益良多。

我还要感谢我的开发编辑Bert Bates:感谢你指出我需要为读者改变自己的写作风格,你对我写作风格的影响遍及全书,极大地改善了本书的可读性。

谢谢你Salvatore Sanfilippo:没有你,就没有Redis,更没有这本书,非常感谢你能为本书作序。

谢谢你Pieter Noordhuis:除了感谢你对Redis的贡献之外,我还要感谢你在RedisConf 2012大会期间,与我开怀畅饮并听取我关于Redis数据结构设计的想法,尽管这些想法未能变为现实,但能够与你交流关于Redis内部实现的知识,我仍深感荣幸。

感谢我的技术校对团队(以名字的首字母排序):James Phillips、Kevin Chang和Nicholas Lindgren,多亏了你们的帮助,本书的质量才能更上一层楼。

感谢我的朋友兼同事Eric Van Dewoestine:谢谢你不辞劳苦地为本书编写了Java版本的示例代码,这些代码可以在这个GitHub页面找到:https://github.com/ josiahcarlson/redis-in-action。

感谢包括Amit Nandi、Bennett Andrews、Bobby Abraham、Brian Forester、Brian Gyss、Brian McNamara、Daniel Sundman、David Miller、Felipe Gutierrez、Filippo Pacini、Gerard O’Sullivan、JC Pretorius、Jonathan Crawley、Joshua White、Leo Cassarani、Mark Wigmans、Richard Clayton、Scott Lyons、Thomas O’Rourke和Todd Fiala在内的参与本书一审、二审、三审以及最终评审的所有审稿人,我已经尽可能地将你们的宝贵意见采纳到本书当中了。

感谢所有在Manning的《Redis实战》作者在线论坛上发表反馈的读者,你们的火眼金睛让错误无处可逃。

我要特别感谢我的妻子See Luan,她宽宏大量地允许我在一年多的时间里,将数不清的夜晚和假日都花在写作上面,而她却独自忍受着怀孕带来的辛苦与不适;直到最近,在我完成本书最终定稿的这段时间里,她又开始独自照顾我们刚出生的女儿。

最后,感谢我的家人和朋友,谢谢他们一直忍受因为写书而无暇他顾的我。


本书将对Redis的使用方法进行说明。Redis是一个内存数据库(或者说内存数据结构)服务器,最初由Salvatore Sanfilippo创建,现在是一个开源软件。本书不要求读者有任何使用Redis的经验,不过因为本书的绝大部分示例都使用了Python编程语言来与Redis进行交互,所以读者需要对Python有一定程度的认识才能更好地理解本书的内容。

如果读者不熟悉Python的话,那么可以去看看Python 2.7.x版本的Python语言教程(Python language tutorial),并在本书提到某种Python语法结构的时候,查找并阅读相应语法结构的文档。虽然本书展示的Python代码在将来可能会被翻译成Java代码、JavaScript代码或者Ruby代码,但这些翻译代码的清晰性和简洁性可能会比不上现有的Python代码,并且在读者阅读本书的时候,将Python代码翻译成其他代码的工作可能尚未完成。

如果读者没有任何使用Redis的经验,那么就应该先阅读本书的第1章和第2章,然后再阅读本书的其他章节(介绍Redis安装方法和Python安装方法的附录A是一个例外,它可以在阅读第1章和第2章之前阅读)。第1章和第2章介绍了Redis是什么,它能做什么,以及读者可能会想要使用它的理由。之后的第3章介绍了Redis提供的各种结构,说明了这些结构的作用和总体概念。第4章介绍了Redis的管理操作,以及实现数据持久化的方法。

如果读者已经有使用Redis的经验,那么可以考虑跳过第1章和第3章——这两章介绍的入门内容都是为那些没有使用过Redis的读者准备的。另外,虽然第2章也属于入门内容,但即使是有Redis使用经验的读者也不应该跳过这一章,因为它展示了本书解决问题时的风格:首先展示问题,然后解决问题,之后回顾问题并改善已有的解决方案,最后,如果读者还想继续深究下去的话,本书还会指出比已有的解决方案更好的新方案。

本书在回顾一个主题的时候,通常会说明第一次讨论这个主题的章节。并非所有主题都要求读者先阅读之前的相关章节,但如果书本确实这么要求的话,那么读者最好还是照书本所说的去做,因为这有助于读者更好地了解整个主题的来龙去脉。

本书很少会给出某个特定问题的最佳解法,更多的是通过展示例子来让读者思考如何去解决某一类问题,并从直觉和非直觉两个方面为这些问题构建解答,所以如果读者在阅读某个主题的时候,发现了比本书列出的解法更好、更快或者更简单的解法,那将是一件非常棒的事情。

本书每一章对应的源代码都包含了一个测试运行器(test runner),测试运行器提供了那一章定义的绝大部分函数或者方法的使用示例,如果读者在理解某一章的示例时遇到了困难,或者想不明白示例是怎样运作的,那么可以去看看那一章对应的源代码。除此之外,每章对应的源代码还给出了那一章大部分练习的答案。

本书总共分为3个部分:第一部分对Redis进行了基本介绍,并展示了一些Redis的使用示例;第二部分对Redis的多个命令进行了详细的介绍,之后还介绍了Redis的管理操作以及使用Redis构建更复杂的应用程序的方法;最后,第三部分介绍了如何通过内存优化、水平分片以及Lua脚本这3种技术来扩展Redis。

第1章对Redis进行了基本介绍,列举了Redis提供的5种数据结构,对比了Redis与其他数据库之间的相同之处和不同之处,实现了一个可以对文章进行投票的简单文章聚合网站。

第2章介绍了如何使用Redis来提升应用程序的性能以及如何使用Redis来实现基本的网络分析。不太了解Redis的读者应该会从第2章开始逐渐明白Redis在最近几年变得越来越流行的原因——因为它简单易用,而且性能强劲。

第3章基本上是一个命令文档,它陆续介绍了Redis的常用命令、基本事务命令、排序命令和过期时间命令,并给出了这些命令的使用示例。

第4章介绍了数据持久化、性能测试、故障恢复以及防止数据丢失等概念。这一章前几节介绍的内容都是和Redis管理有关的,而之后的4.4节和4.5节则深入地讨论了Redis事务和流水线命令的性能。Redis新手和中级Redis用户都应该阅读4.4节和4.5节,因为本书在之后的章节里面会再次回顾这两节提到的问题。

第5章介绍了将Redis用作数据库,并使用它来实现日志、计数器、IP所属地查找程序和服务配置程序的方法。

第6章介绍了一些对于规模日益增长的应用程序非常有用的组件,比如自动补全、加锁、任务队列、消息传递以及文件分发。

第7章深入研究了一系列与搜索有关的问题和解决方案,它们可能会改变读者对于数据查询和数据过滤的看法。

第8章详细地说明了如何构建一个类似Twitter的社交网站,并给出了包括流API在内的整个网站后端实现。

第9章讨论了扩展Redis时会用到的内存优化技术,其中包括结构分片方法以及短结构的使用方法。

第 10 章介绍了对 Redis 进行水平分片和主从复制的方法。当一台服务器不足以满足需求的时候,这两项特性可以提供更强劲的性能以及更多的可用内存。

第11章介绍了如何通过Lua脚本编程在服务器端对Redis的功能进行扩展,并在某些场景下把Lua脚本用作提升性能的方法。

附录A介绍了如何在Linux、OS X和Windows这3种不同的平台上安装Redis、Python以及Python的Redis客户端。

附录B是一个参考手册,它列出了各种在使用Redis时可能会有用的资源,比如本书用到的Python语法结构的文档,一些Redis使用案例,用于完成各种任务的第三方Redis库,诸如此类。

为了与一般文本区别开来,本书在代码清单和正文中使用fixed-width font like this这样的字体来显示代码。重要的代码都带有相应的注释,有些代码还会带有编号,以便在之后的内容中对被编号的代码进行说明。

本书列出的所有代码清单的源代码都可以在Manning网站下载到:www.manning.com/RedisinAction。1如果读者想要查看被翻译成其他语言的源代码,或者想要在线阅览用Python语言编写的源代码,那么可以访问这个GitHub地址:github.com/josiahcarlson/redis-in-action。

1读者也可在异步社区(http://www.epubit.com)本书页面下载本书源代码

Manning出版社为本书创建了相应的专属论坛,读者可以通过这个论坛来发表关于本书的评论,询问技术问题,或者寻求作者或其他读者的帮助。www.manning.com/RedisinAction记载了访问本书专属论坛的方法,部分功能(如发帖)可能需要在注册或者登录之后才能使用。

Manning出版社承诺为本书提供论坛以供读者和作者使用,但并不对作者的参与度做任何保证:作者对该论坛的所有贡献都是自愿的,并且是无偿的,因此,读者应该尽可能地询问一些有挑战性的问题,从而尽量激发作者的积极性。

在本书正常销售期间,这个作者在线论坛会一直对外开放。

在大学毕业之后,Josiah Carlson博士继续在加州大学欧文分校学习理论计算机科学。在学习之余,Josiah断断续续地做过一些助教工作,偶尔还会承接一些编程方面的工作。在Josiah快要研究生毕业的时候,他发现教职方面的工作机会并不多,于是他加入了Networks in Motion公司,开始了自己的职业生涯。在Networks in Motion公司任职期间,Josiah负责开发实时GPS导航软件,以及交通事故通知系统。

在离开Networks in Motion公司之后,Josiah加入了Google公司,之后他又跳槽到了Adly公司工作,开始学习和使用Redis来构建内容定向广告系统和Twitter分析平台。几个月之后,Josiah加入了Redis邮件列表,并在那里回答了数百个关于使用和配置Redis的问题。在离开Adly公司并成为ChowNow公司的首席架构师兼联合创始人之后不久,Josiah开始创作这本《Redis实战》。


本书封面插图的标题为“一介草民”(A Man of the People),这幅插图取自 19世纪法国再版的地区服饰风俗四卷汇编(four-volume compendium of regional dress customs),作者是Sylvain Maréchal。书中所有插图都是手工精心绘制并上色的。Maréchal书中丰富多样的服饰生动地描述了200多年前世界上不同城镇和地区的文化差异,人们相互隔绝,说着不同的方言和语言,仅仅从穿着就可以判断他们是住在城镇还是乡间,知悉他们的工作和身份。

随着时间的流逝,人们的着装规范已经发生了变化,曾经丰富多彩的地区多样性也已经逐渐消失不见——现在仅仅通过穿着已经很难区分不同大洲的居民,更别说是不同城镇和地区了。也许我们已经舍弃了对文化多样性的追求,转为拥抱更丰富多彩的个人生活以及更多样和快节奏的技术生活去了。

同样地,在这个难以分辨不同计算机书籍的时代,Manning出版社希望通过Maréchal的作品,将两个世纪前丰富多彩的地区生活融入本书封面,以此来赞美计算机行业不断创新和敢为人先的精神。


本书最开始的两章将对Redis进行介绍,并展示Redis的一些基本用法。读完这两章之后,读者应该能够用Redis对自己的项目进行一些简单的优化。


本章主要内容

  • Redis与其他软件的相同之处和不同之处
  • Redis的用法
  • 使用Python示例代码与Redis进行简单的互动
  • 使用Redis解决实际问题

Redis是一个远程内存数据库,它不仅性能强劲,而且还具有复制特性以及为解决问题而生的独一无二的数据模型。Redis提供了5种不同类型的数据结构,各式各样的问题都可以很自然地映射到这些数据结构上:Redis的数据结构致力于帮助用户解决问题,而不会像其他数据库那样,要求用户扭曲问题来适应数据库。除此之外,通过复制、持久化(persistence)和客户端分片(client-side sharding)等特性,用户可以很方便地将Redis扩展成一个能够包含数百GB数据、每秒处理上百万次请求的系统。

笔者第一次使用Redis是在一家公司里面,这家公司需要对一个保存了6万个客户联系方式的关系数据库进行搜索,搜索可以根据名字、邮件地址、所在地和电话号码来进行,每次搜索需要花费10~15秒的时间。在花了一周时间学习Redis的基础知识之后,我使用Redis重写了一个新的搜索引擎,然后又花费了数周时间来仔细测试这个新系统,使它达到生产级别,最终这个新的搜索系统不仅可以根据名字、邮件地址、所在地和电话号码等信息来过滤和排序客户联系方式,并且每次操作都可以在50毫秒之内完成,这比原来的搜索系统足足快了 200 倍。阅读本书可以让你学到很多小技巧、小窍门以及使用Redis解决某些常见问题的方法。

本章将介绍Redis的适用范围,以及在不同环境中使用Redis的方法(比如怎样跟不同的组件和编程语言进行通信等);而之后的章节则会展示各式各样的问题,以及使用Redis来解决这些问题的方法。

现在你已经知道我是怎样开始使用Redis的了,也知道了这本书大概要讲些什么内容了,是时候更详细地介绍一下Redis,并说明为什么应该使用Redis了。

安装Redis和Python 附录A介绍了快速安装Redis和Python的方法。

在其他编程语言里面使用Redis 本书只展示了使用Python语言编写的示例代码,使用Ruby、Java和JavaScript(Node.js)编写的示例代码可以在这里找到:https://github.com/josiahcarlson/redis-in-action。使用Spring框架的读者可以通过查看http://www.springsource.org/spring-data/redis来学习如何在Spring框架中使用Redis。

前面对于Redis数据库的描述只说出了一部分真相。Redis是一个速度非常快的非关系数据库(non-relational database),它可以存储键(key)与5种不同类型的值(value)之间的映射(mapping),可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,还可以使用客户端分片来扩展写性能,接下来的几节将分别介绍Redis的这几个特性。

如果你熟悉关系数据库,那么你肯定写过用来关联两个表的数据的SQL查询。而Redis则属于人们常说的NoSQL数据库或者非关系数据库:Redis不使用表,它的数据库也不会预定义或者强制去要求用户对Redis存储的不同数据进行关联。

高性能键值缓存服务器memcached也经常被拿来与Redis进行比较:这两者都可用于存储键值映射,彼此的性能也相差无几,但是Redis能够自动以两种不同的方式将数据写入硬盘,并且Redis除了能存储普通的字符串键之外,还可以存储其他4种数据结构,而memcached只能存储普通的字符串键。这些不同之处使得Redis可以用于解决更为广泛的问题,并且既可以用作主数据库(primary database)使用,又可以作为其他存储系统的辅助数据库(auxiliary database)使用。

本书的后续章节会分别介绍将Redis用作主存储(primary storage)和二级存储(secondary storage)时的用法和查询模式。一般来说,许多用户只会在Redis的性能或者功能是必要的情况下,才会将数据存储到Redis里面:如果程序对性能的要求不高,又或者因为费用原因而没办法将大量数据存储到内存里面,那么用户可能会选择使用关系数据库,或者其他非关系数据库。在实际中,读者应该根据自己的需求来决定是否使用Redis,并考虑是将Redis用作主存储还是辅助存储,以及如何通过复制、持久化和事务等手段保证数据的完整性。

表1-1展示了一部分在功能上与Redis有重叠的数据库服务器和缓存服务器,从这个表可以看出Redis与这些数据库及软件之间的区别。

表1-1 一些数据库和缓存服务器的特性与功能

名称

类型

数据存储选项

查询类型

附加功能

Redis

使用内存存储(in-memory)的非关系数据库

字符串、列表、集合、散列表、有序集合

每种数据类型都有自己的专属命令,另外还有批量操作(bulk operation)和不完全(partial)的事务支持

发布与订阅,主从复制(master/slave replication),持久化,脚本(存储过程,stored procedure)

memcached

使用内存存储的键值缓存

键值之间的映射

创建命令、读取命令、更新命令、删除命令以及其他几个命令

为提升性能而设的多线程服务器

MySQL

关系数据库

每个数据库可以包含多个表,每个表可以包含多个行;可以处理多个表的视图(view);支持空间(spatial)和第三方扩展

SELECTINSERTUPDATEDELETE、函数、存储过程

支持ACID性质(需要使用InnoDB),主从复制和主主复制 (master/master replication)

PostgreSQL

关系数据库

每个数据库可以包含多个表,每个表可以包含多个行;可以处理多个表的视图;支持空间和第三方扩展;支持可定制类型

SELECTINSERTUPDATEDELETE、内置函数、自定义的存储过程

支持ACID性质,主从复制,由第三方支持的多主复制(multi-master replication)

MongoDB

使用硬盘存储(on-disk)的非关系文档存储

每个数据库可以包含多个表,每个表可以包含多个无schema(schema-less)的BSON文档

创建命令、读取命令、更新命令、删除命令、条件查询命令等

支持map-reduce操作,主从复制,分片,空间索引(spatial index)

在使用类似Redis这样的内存数据库时,一个首先要考虑的问题就是“当服务器被关闭时,服务器存储的数据将何去何从呢?”Redis拥有两种不同形式的持久化方法,它们都可以用小而紧凑的格式将存储在内存中的数据写入硬盘:第一种持久化方法为时间点转储(point-in-time dump),转储操作既可以在“指定时间段内有指定数量的写操作执行”这一条件被满足时执行,又可以通过调用两条转储到硬盘(dump-to-disk)命令中的任何一条来执行;第二种持久化方法将所有修改了数据库的命令都写入一个只追加(append-only)文件里面,用户可以根据数据的重要程度,将只追加写入设置为从不同步(sync)、每秒同步一次或者每写入一个命令就同步一次。我们将在第4章中更加深入地讨论这些持久化选项。

另外,尽管Redis的性能很好,但受限于Redis的内存存储设计,有时候只使用一台Redis服务器可能没有办法处理所有请求。因此,为了扩展Redis的读性能,并为Redis提供故障转移(failover)支持,Redis实现了主从复制特性:执行复制的从服务器会连接上主服务器,接收主服务器发送的整个数据库的初始副本(copy);之后主服务器执行的写命令,都会被发送给所有连接着的从服务器去执行,从而实时地更新从服务器的数据集。因为从服务器包含的数据会不断地进行更新,所以客户端可以向任意一个从服务器发送读请求,以此来避免对主服务器进行集中式的访问。我们将在第4章中更加深入地讨论Redis从服务器。

有memcached使用经验的读者可能知道,用户只能用APPEND命令将数据添加到已有字符串的末尾。memcached的文档中声明,可以用APPEND命令来管理元素列表。这很好!用户可以将元素追加到一个字符串的末尾,并将那个字符串当作列表来使用。但随后如何删除这些元素呢?memcached采用的办法是通过黑名单(blacklist)来隐藏列表里面的元素,从而避免对元素执行读取、更新、写入(包括在一次数据库查询之后执行的memcached写入)等操作。相反地,Redis的LISTSET允许用户直接添加或者删除元素。

使用Redis而不是memcached来解决问题,不仅可以让代码变得更简短、更易懂、更易维护,而且还可以使代码的运行速度更快(因为用户不需要通过读取数据库来更新数据)。除此之外,在其他许多情况下,Redis的效率和易用性也比关系数据库要好得多。

数据库的一个常见用法是存储长期的报告数据,并将这些报告数据用作固定时间范围内的聚合数据(aggregates)。收集聚合数据的常见做法是:先将各个行插入一个报告表里面,之后再通过扫描这些行来收集聚合数据,并根据收集到的聚合数据来更新聚合表中已有的那些行。之所以使用插入行的方式来存储,是因为对于大部分数据库来说,插入行操作的执行速度非常快(插入行只会在硬盘文件末尾进行写入)。不过,对表里面的行进行更新却是一个速度相当慢的操作,因为这种更新除了会引起一次随机读(random read)之外,还可能会引起一次随机写(random write)。而在Redis里面,用户可以直接使用原子的(atomic)INCR命令及其变种来计算聚合数据,并且因为Redis将数据存储在内存里面,而且发送给Redis的命令请求并不需要经过典型的查询分析器(parser)或者查询优化器(optimizer)进行处理,所以对Redis存储的数据执行随机写的速度总是非常迅速的。

使用 Redis 而不是关系数据库或者其他硬盘存储数据库,可以避免写入不必要的临时数据,也免去了对临时数据进行扫描或者删除的麻烦,并最终改善程序的性能。虽然上面列举的都是一些简单的例子,但它们很好地证明了“工具会极大地改变人们解决问题的方式”这一点。

除了第6章提到的任务队列(task queue)之外,本书的大部分内容都致力于实时地解决问题。本书通过展示各种技术并提供可工作的代码来帮助读者消灭瓶颈、简化代码、收集数据、分发(distribute)数据、构建实用程序(utility),并最终帮助读者更轻松地完成构建软件的任务。只要正确地使用书中介绍的技术,读者的软件就可以扩展至令那些所谓的“Web扩展技术(web-sacle technology)”相形见绌的地步。

在了解了Redis是什么、它能做什么以及我们为什么要使用它之后,是时候来实际地使用一下它了。接下来的一节将对Redis提供的数据结构进行介绍,说明这些数据结构的作用,并展示操作这些数据结构的其中一部分命令。

正如之前的表1-1所示,Redis可以存储键与5种不同数据结构类型之间的映射,这5种数据结构类型分别为STRING(字符串)、LIST(列表)、SET(集合)、HASH(散列)和ZSET(有序集合)。有一部分Redis命令对于这5种结构都是通用的,如DELTYPERENAME等;但也有一部分Redis命令只能对特定的一种或者两种结构使用,第3章将对Redis提供的命令进行更深入的介绍。

大部分程序员应该都不会对Redis的STRINGLISTHASH这3种结构感到陌生,因为它们和很多编程语言内建的字符串、列表和散列等结构在实现和语义(semantics)方面都非常相似。有些编程语言还有集合数据结构,在实现和语义上类似于Redis的SETZSET在某种程度上是一种Redis特有的结构,但是当你熟悉了它之后,就会发现它也是一种非常有用的结构。表1-2对比了Redis提供的5种结构,说明了这些结构存储的值,并简单介绍了它们的语义。

表1-2 Redis提供的5种结构

结构类型

结构存储的值

结构的读写能力

STRING

可以是字符串、整数或者浮点数

对整个字符串或者字符串的其中一部分执行操作;对整数和浮点数执行自增(increment)或者自减(decrement)操作

LIST

一个链表,链表上的每个节点都包含了一个字符串

从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值查找或者移除元素

SET

包含字符串的无序收集器(unordered collection),并且被包含的每个字符串都是独一无二、各不相同的

添加、获取、移除单个元素;检查一个元素是否存在于集合中;计算交集、并集、差集;从集合里面随机获取元素

HASH

包含键值对的无序散列表

添加、获取、移除单个键值对;获取所有键值对

ZSET(有序集合)

字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定

添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素

命令列表 本节在介绍每个数据类型的时候,都会在一个表格里面展示一小部分处理这些数据结构的命令,之后的第 3 章会展示一个更详细(但仍不完整)的命令列表,完整的 Redis 命令列表可以在http://redis.io/commands找到。

这一节将介绍如何表示Redis的这5种结构,并且还会介绍Redis命令的使用方法,从而为本书的后续内容打好基础。本书展示的所有示例代码都是用Python写的,如果读者已经按照附录A里面描述的方法安装好了Redis,那么应该也已经安装好了Python,以及在Python里面使用Redis所需的客户端库。只要读者在电脑里面安装了Redis、Python和redis-py库,就可以在阅读本书的同时,尝试执行书中展示的示例代码了。

请安装Redis和Python 在阅读后续内容之前,请读者先按照附录A中介绍的方法安装Redis和Python。如果读者觉得附录A描述的安装方法过于复杂,那么这里有一个更简单的方法,但这个方法只能用于Debian系统(或者该系统的衍生系统):从http://redis.io/download下载Redis的压缩包,解压压缩包,执行make && sudo make install,之后再执行sudo python -m easy_install redis hiredis(hiredis是可选的,它是一个使用C语言编写的高性能Redis客户端)。

如果读者熟悉过程式编程语言或者面向对象编程语言,那么即使没有使用过Python,应该也可以看懂Python代码。另一方面,如果读者决定使用其他编程语言来操作Redis,那么就需要自己来将本书的Python代码翻译成正在使用的语言的代码。

使用其他语言编写的示例代码 尽管没有包含在书中,但本书展示的Python示例代码已经被翻译成了Ruby代码、Java代码和JavaScript代码,这些翻译代码可以在https://github.com/ josiahcarlson/redis-in-action下载到。跟Python编写的示例代码一样,这些翻译代码也包含相应的注释,方便读者参考。

为了让示例代码尽可能地简单,本书会尽量避免使用Python的高级特性,并使用函数而不是类或者其他东西来执行Redis操作,以此来将焦点放在使用Redis解决问题上面,而不必过多地关注Python的语法。本节将使用redis-cli控制台与Redis进行互动。首先,让我们来了解一下Redis中最简单的结构:STRING

Redis的STRING和其他编程语言或者其他键值存储提供的字符串非常相似。本书在使用图片表示键和值的时候,通常会将键名(key name)和值的类型放在方框的顶部,并将值放在方框的里面。图1-1以键为hello、值为worldSTRING为例,分别标记了方框的各个部分。

图1-1 一个STRING示例,键为hello,值为world

STRING拥有一些和其他键值存储相似的命令,比如GET(获取值)、SET(设置值)和DEL(删除值)。如果读者已经按照附录A中给出的方法安装了Redis,那么可以根据代码清单1-1展示的例子,尝试使用redis-cli执行SETGETDEL,表1-3描述了这3个命令的基本用法。

表1-3 字符串命令

命令

行为

GET

获取存储在给定键中的值

SET

设置存储在给定键中的值

DEL

删除存储在给定键中的值(这个命令可以用于所有类型)

代码清单1-1 SETGETDEL的使用示例

使用 redis-cli 为了让读者在一开始就能便捷地与 Redis 进行交互,本章将使用redis-cli这个交互式客户端来介绍Redis命令。

除了能够GETSETDEL字符串值之外,Redis还提供了一些可以对字符串的其中一部分内容进行读取和写入的命令,以及一些能对字符串存储的数值执行自增或者自减操作的命令。第3章将对这些命令进行介绍,但是在此之前,我们还有许多基础知识需要了解,下面来看一下Redis的列表及其功能。

Redis对链表(linked-list)结构的支持使得它在键值存储的世界中独树一帜。一个列表结构可以有序地存储多个字符串,和表示字符串时使用的方法一样,本节使用带有标签的方框来表示列表,并将列表包含的元素放在方框里面。图1-2展示了一个这样的示例。

图1-2 list-key是一个包含3个元素的列表键,注意列表里面的元素是可以重复的

Redis列表可执行的操作和很多编程语言里面的列表操作非常相似:LPUSH命令和RPUSH命令分别用于将元素推入列表的左端(left end)和右端(right end);LPOP命令和RPOP命令分别用于从列表的左端和右端弹出元素;LINDEX命令用于获取列表在给定位置上的一个元素;LRANGE命令用于获取列表在给定范围上的所有元素。代码清单1-2展示了一些列表命令的使用示例,表1-4简单介绍了示例中用到的各个命令。

表1-4 列表命令

命令

行为

RPUSH

将给定值推入列表的右端

LRANGE

获取列表在给定范围上的所有值

LINDEX

获取列表在给定位置上的单个元素

LPOP

从列表的左端弹出一个值,并返回被弹出的值

代码清单1-2 RPUSHLRANGELINDEXLPOP的使用示例

即使Redis的列表只支持以上提到的几个命令,它也已经可以用来解决很多问题了,但Redis并没有就此止步——除了上面提到的命令之外,Redis列表还拥有从列表里面移除元素的命令、将元素插入列表中间的命令、将列表修剪至指定长度(相当于从列表的其中一端或者两端移除元素)的命令,以及其他一些命令。第3章将介绍许多列表命令,但是在此之前,让我们先来了解一下Redis的集合。

Redis 的集合和列表都可以存储多个字符串,它们之间的不同在于,列表可以存储多个相同的字符串,而集合则通过使用散列表来保证自己存储的每个字符串都是各不相同的(这些散列表只有键,但没有与键相关联的值)。本书表示集合的方法和表示列表的方法基本相同,图1-3展示了一个包含3个元素的示例集合。

图1-3 set-key是一个包含3个元素的集合键

因为Redis的集合使用无序(unordered)方式存储元素,所以用户不能像使用列表那样,将元素推入集合的某一端,或者从集合的某一端弹出元素。不过用户可以使用SADD命令将元素添加到集合,或者使用SRAM命令从集合里面移除元素。另外还可以通过SISMEMBER命令快速地检查一个元素是否已经存在于集合中,或者使用SMEMBERS命令获取集合包含的所有元素(如果集合包含的元素非常多,那么SMEMBERS命令的执行速度可能会很慢,所以请谨慎地使用这个命令)。代码清单1-3展示了一些集合命令的使用示例,表1-5简单介绍了代码清单里面用到的各个命令。

代码清单1-3 SADDSMEMBERSSISMEMBERSREM的使用示例

表1-5 集合命令

命令

行为

SADD

将给定元素添加到集合

SMEMBERS

返回集合包含的所有元素

SISMEMBER

检查给定元素是否存在于集合中

SREM

如果给定的元素存在于集合中,那么移除这个元素

跟字符串和列表一样,集合除了基本的添加操作和移除操作之外,还支持很多其他操作,比如SINTERSUNIONSDIFF这3个命令就可以分别执行常见的交集计算、并集计算和差集计算。第3章将对集合的相关命令进行更详细的介绍,另外第7章还会展示如何使用集合来解决多个问题。不过别心急,因为在Redis提供的5种数据结构中,还有两种我们尚未了解,让我们先来看看Redis的散列。

Redis的散列可以存储多个键值对之间的映射。和字符串一样,散列存储的值既可以是字符串又可以是数字值,并且用户同样可以对散列存储的数字值执行自增操作或者自减操作。图1-4展示了一个包含两个键值对的散列。

图1-4 hash-key是一个包含两个键值对的散列键

散列在很多方面就像是一个微缩版的Redis,不少字符串命令都有相应的散列版本。代码清单1-4展示了怎样对散列执行插入元素、获取元素和移除元素等操作,表1-6简单介绍了代码清单里面用到的各个命令。

代码清单1-4 HSETHGETHGETALLHDEL的使用示例

表1-6 散列命令

命令

行为

HSET

在散列里面关联起给定的键值对

HGET

获取指定散列键的值

HGETALL

获取散列包含的所有键值对

HDEL

如果给定键存在于散列里面,那么移除这个键

熟悉文档数据库的读者可以将Redis的散列看作是文档数据库里面的文档,而熟悉关系数据库的读者则可以将Redis的散列看作是关系数据库里面的行,因为散列、文档和行这三者都允许用户同时访问或者修改一个或多个域(field)。最后,让我们来了解一下Redis的5种数据结构中的最后一种:有序集合。

有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是独一无二的;而有序集合的值则被称为分值(score),分值必须为浮点数。有序集合是Redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序来访问元素的结构。图1-5展示了一个包含两个元素的有序集合示例。

图1-5 zset-key是一个包含两个元素的有序集合键

和Redis的其他结构一样,用户可以对有序集合执行添加、移除和获取等操作,代码清单1-5展示了这些操作的执行示例,表1-7简单介绍了代码清单里面用到的各个命令。

代码清单1-5 ZADDZRANGEZRANGEBYSCOREZREM的使用示例

表1-7 有序集合命令

命令

行为

ZADD

将一个带有给定分值的成员添加到有序集合里面

ZRANGE

根据元素在有序排列中所处的位置,从有序集合里面获取多个元素

ZRANGEBYSCORE

获取有序集合在给定分值范围内的所有元素

ZREM

如果给定成员存在于有序集合,那么移除这个成员

现在读者应该已经知道有序集合是什么和它能干什么了,到目前为止,我们基本了解了Redis提供的5种结构。接下来的一节将展示如何通过结合散列的数据存储能力和有序集合内建的排序能力来解决一个常见的问题。

在对Redis提供的5种结构有了基本的了解之后,现在是时候来学习一下怎样使用这些结构来解决实际问题了。最近几年,越来越多的网站开始提供对网页链接、文章或者问题进行投票的功能,其中包括图1-6展示的reddit以及图1-7展示的Stack Overflow。这些网站会根据文章的发布时间和文章获得的投票数量计算出一个评分,然后按照这个评分来决定如何排序和展示文章。本节将展示如何使用Redis来构建一个简单的文章投票网站的后端。

图1-6 Reddit是一个可以对文章进行投票的网站

图1-7 StackOverflow是一个可以对问题进行投票的网站

要构建一个文章投票网站,我们首先要做的就是为了这个网站设置一些数值和限制条件:如果一篇文章获得了至少200张支持票(up vote),那么网站就认为这篇文章是一篇有趣的文章;假如这个网站每天发布1000篇文章,而其中的50篇符合网站对有趣文章的要求,那么网站要做的就是把这50篇文章放到文章列表前100位至少一天;另外,这个网站暂时不提供投反对票(down vote)的功能。

为了产生一个能够随着时间流逝而不断减少的评分,程序需要根据文章的发布时间和当前时间来计算文章的评分,具体的计算方法为:将文章得到的支持票数量乘以一个常数,然后加上文章的发布时间,得出的结果就是文章的评分。

我们使用从UTC时区1970年1月1日到现在为止经过的秒数来计算文章的评分,这个值通常被称为Unix时间。之所以选择使用Unix时间,是因为在所有能够运行Redis的平台上面,使用编程语言获取这个值都是一件非常简单的事情。另外,计算评分时与支持票数量相乘的常量为432,这个常量是通过将一天的秒数(86 400)除以文章展示一天所需的支持票数量(200)得出的:文章每获得一张支持票,程序就需要将文章的评分增加432分。

构建文章投票网站除了需要计算文章评分之外,还需要使用Redis结构存储网站上的各种信息。对于网站里的每篇文章,程序都使用一个散列来存储文章的标题、指向文章的网址、发布文章的用户、文章的发布时间、文章得到的投票数量等信息,图1-8展示了一个使用散列来存储文章信息的例子。

图1-8 一个使用散列存储文章信息的例子

使用冒号作为分隔符 本书使用冒号(:)来分隔名字的不同部分:比如图 1-8 里面的键名article:92617就使用了冒号来分隔单词article和文章的ID号92617,以此来构建命名空间(namespace)。使用:作为分隔符只是我的个人喜好,不过大部分Redis用户也都是这么做的,另外还有一些常见的分隔符,如句号(.)、斜线(/),有些人甚至还会使用管道符号(|)。无论使用哪个符号来做分隔符,都要保持分隔符的一致性。同时,请读者注意观察和学习本书使用冒号创建嵌套命名空间的方法。

我们的文章投票网站将使用两个有序集合来有序地存储文章:第一个有序集合的成员为文章 ID,分值为文章的发布时间;第二个有序集合的成员同样为文章 ID,而分值则为文章的评分。通过这两个有序集合,网站既可以根据文章发布的先后顺序来展示文章,又可以根据文章评分的高低来展示文章,图1-9展示了这两个有序集合的一个示例。

图1-9 两个有序集合分别记录了根据发布时间排序的文章和根据评分排序的文章

为了防止用户对同一篇文章进行多次投票,网站需要为每篇文章记录一个已投票用户名单。为此,程序将为每篇文章创建一个集合,并使用这个集合来存储所有已投票用户的ID,图1-10展示了一个这样的集合示例。

图1-10 为100408号文章投过票的一部分用户

为了尽量节约内存,我们规定当一篇文章发布期满一周之后,用户将不能再对它进行投票,文章的评分将被固定下来,而记录文章已投票用户名单的集合也会被删除。

在实现投票功能之前,让我们来看看图 1-11:这幅图展示了当115423号用户给100408号文章投票的时候,数据结构发生的变化。

图1-11 当115423号用户给100408号文章投票的时候,数据结构发生的变化

既然我们已经知道了网站计算文章评分的方法,也知道了网站存储数据所需的数据结构,那么现在是时候实际地实现这个投票功能了!当用户尝试对一篇文章进行投票时,程序需要使用ZSCORE命令检查记录文章发布时间的有序集合,判断文章的发布时间是否未超过一周。如果文章仍然处于可以投票的时间范围之内,那么程序将使用SADD命令,尝试将用户添加到记录文章已投票用户名单的集合里面。如果添加操作执行成功的话,那么说明用户是第一次对这篇文章进行投票,程序将使用ZINCRBY命令为文章的评分增加432分(ZINCRBY命令用于对有序集合成员的分值执行自增操作),并使用HINCRBY命令对散列记录的文章投票数量进行更新(HINCRBY命令用于对散列存储的值执行自增操作),代码清单1-6展示了投票功能的实现代码。

代码清单1-6 article_vote()函数

Redis事务 从技术上来讲,要正确地实现投票功能,我们需要将代码清单1-6里面的SADDZINCRBYHINCRBY这3个命令放到一个事务里面执行,不过因为本书要等到第4章才介绍Redis事务,所以我们暂时忽略这个问题。

这个投票功能还是很不错的,对吧?那么发布文章的功能要怎么实现呢?

发布一篇新文章首先需要创建一个新的文章ID,这项工作可以通过对一个计数器(counter)执行INCR命令来完成。接着程序需要使用SADD将文章发布者的ID添加到记录文章已投票用户名单的集合里面,并使用EXPIRE命令为这个集合设置一个过期时间,让Redis在文章发布期满一周之后自动删除这个集合。之后,程序会使用HMSET命令来存储文章的相关信息,并执行两个ZADD命令,将文章的初始评分(initial score)和发布时间分别添加到两个相应的有序集合里面。代码清单1-7展示了发布新文章功能的实现代码。

代码清单1-7 post_article()函数

好了,我们已经陆续实现了文章投票功能和文章发布功能,接下来要考虑的就是如何取出评分最高的文章以及如何取出最新发布的文章了。为了实现这两个功能,程序需要先使用ZREVRANGE命令取出多个文章ID,然后再对每个文章ID执行一次HGETALL命令来取出文章的详细信息,这个方法既可以用于取出评分最高的文章,又可以用于取出最新发布的文章。这里特别要注意的一点是,因为有序集合会根据成员的分值从小到大地排列元素,所以使用ZREVRANGE命令,以“分值从大到小”的排列顺序取出文章ID才是正确的做法,代码清单1-8展示了文章获取功能的实现函数。

代码清单1-8 get_articles()函数

Python的默认值参数和关键字参数 代码清单1-8中的get_articles()函数为order参数设置了默认值score:。Python语言的初学者可能会对“默认值参数”以及“根据名字(而不是位置)来传入参数”的一些细节感到陌生。如果读者在理解函数定义或者参数传递方面有困难,可以考虑去看看《Python语言教程》,教程里面对这两个方面进行了很好的介绍,点击以下短链接就可以直接访问教程的相关章节:http://mng.bz/KM5x

虽然我们构建的网站现在已经可以展示最新发布的文章和评分最高的文章了,但它还不具备目前很多投票网站都支持的群组(group)功能:这个功能可以让用户只看见与特定话题有关的文章,比如与“可爱的动物”有关的文章、与“政治”有关的文章、与“Java编程”有关的文章或者介绍“Redis用法”的文章等等。接下来的一节将向我们展示为文章投票网站添加群组功能的方法。

群组功能由两个部分组成,一个部分负责记录文章属于哪个群组,另一个部分负责取出群组里面的文章。为了记录各个群组都保存了哪些文章,网站需要为每个群组创建一个集合,并将所有同属一个群组的文章ID都记录到那个集合里面。代码清单1-9展示了将文章添加到群组里面的方法,以及从群组里面移除文章的方法。

代码清单1-9 add_remove_groups()函数

初看上去,可能会有读者觉得使用集合来记录群组文章并没有多大用处。到目前为止,读者只看到了集合结构检查某个元素是否存在的能力,但实际上Redis不仅可以对多个集合执行操作,甚至在一些情况下,还可以在集合和有序集合之间执行操作。

为了能够根据评分对群组文章进行排序和分页(paging),网站需要将同一个群组里面的所有文章都按照评分有序地存储到一个有序集合里面。Redis的ZINTERSTORE命令可以接受多个集合和多个有序集合作为输入,找出所有同时存在于集合和有序集合的成员,并以几种不同的方式来组合(combine)这些成员的分值(所有集合成员的分值都会被视为是1)。对于我们的文章投票网站来说,程序需要使用ZINTERSTORE命令选出相同成员中最大的那个分值来作为交集成员的分值:取决于所使用的排序选项,这些分值既可以是文章的评分,也可以是文章的发布时间。

图 1-12 展示了对一个包含少量文章的群组集合和一个包含大量文章及评分的有序集合执行ZINTERSTORE命令的过程,注意观察那些同时出现在集合和有序集合里面的文章是怎样被添加到结果有序集合里面的。

图1-12 对集合groups:programming和有序集合score:进行交集计算得出了新的有序集合score:programming,它包含了所有同时存在于集合groups:programming和有序集合score:的成员。因为集合groups:programming的所有成员的分值都被视为是1,而有序集合score:的所有成员的分值都大于1,并且这次交集计算挑选的分值为相同成员中的最大分值,所以有序集合score:programming的成员的分值实际上是由有序集合score:的成员的分值来决定的

通过对存储群组文章的集合和存储文章评分的有序集合执行ZINTERSTORE命令,程序可以得到按照文章评分排序的群组文章;而通过对存储群组文章的集合和存储文章发布时间的有序集合执行ZINTERSTORE命令,程序则可以得到按照文章发布时间排序的群组文章。如果群组包含的文章非常多,那么执行ZINTERSTORE命令就会比较花时间,为了尽量减少Redis的工作量,程序会将这个命令的计算结果缓存60秒。另外,我们还重用了已有的get_articles()函数来分页并获取群组文章,代码清单1-10展示了网站从群组里面获取一整页文章的方法。

代码清单1-10 get_group_articles()函数

有些网站只允许用户将文章放在一个或者两个群组里面(其中一个是“所有文章”群组,另一个是最适合文章的群组)。在这种情况下,最好直接将文章所在的群组记录到存储文章信息的散列里面,并在article_vote()函数的末尾增加一个ZINCRBY命令调用,用于更新文章在群组中的评分。但是在这个示例里面,我们构建的文章投票网站允许一篇文章同时属于多个群组(比如一篇文章可以同时属于“编程”和“算法”两个群组),所以对于一篇同时属于多个群组的文章来说,更新文章的评分意味着程序需要对文章所属的全部群组执行自增操作。在这种情况下,如果一篇文章同时属于很多个群组,那么更新文章评分这一操作可能会变得相当耗时,因此,我们在get_group_articles()函数里面对ZINTERSTORE命令的执行结果进行了缓存处理,以此来尽量减少ZINTERSTORE命令的执行次数。开发者在灵活性或限制条件之间的取舍将改变程序存储和更新数据的方式,这一点对于任何数据库都是适用的,Redis也不例外。

练习:实现投反对票的功能

我们的示例目前只实现了投支持票的功能,但是在很多实际的网站里面,反对票也能给用户提供有用的反馈信息。因此,请读者能想办法在article_vote()函数和post_article()函数里面添加投反对票的功能。除此之外,读者还可以尝试为用户提供对调投票的功能:比如将支持票转换成反对票,或者将反对票转换成支持票。提示:如果读者在实现对调投票功能时出现了困难,可以参考一下第3章介绍的SMOVE命令。

好的,现在我们已经成功地构建起了一个展示最受欢迎文章的网站后端,这个网站可以获取文章、发布文章、对文章进行投票甚至还可以对文章进行分组。如果你觉得前面展示的内容不好理解,或者弄不懂这些示例,又或者没办法运行本书提供的源代码,那么请阅读下一节来了解如何获取帮助。

当你遇到与Redis有关的问题时,不要害怕求助于别人,因为其他人可能也遇到过类似的问题。首先,你可以根据错误信息在搜索引擎里面进行查找,看是否有所发现。

如果搜索一无所获,又或者你遇到的问题与本书的示例代码有关,那么你可以到Manning出版社提供的本书论坛里面发问(http://www.manning-sandbox.com/forum.jspa?forumID=809),我和其他熟悉本书的人将为你提供帮助。

如果你遇到的问题与Redis本身有关,又或者你正在解决的问题在这本书里面没有出现过,那么你可以到Redis的邮件列表里面发问(https://groups.google.com/d/forum/redis-db/),同样地,我和其他熟悉Redis的人将为你提供帮助。

最后,如果你遇到的问题与某个函数库或者某种编程语言有关,那么比起在Redis邮件列表里面发帖提问,更好的方法是直接到你正在使用的那个函数库或者那种编程语言的邮件列表或论坛里面寻求帮助。

本章对Redis进行了初步的介绍,说明了Redis与其他数据库的相同之处和不同之处,以及一些读者可能会使用Redis的理由。在阅读本书的后续章节之前,请记住本书的目标并不是构建一个完整的应用或者工具,而是展示各式各样的问题,并给出使用Redis来解决这些问题的办法。

本章希望向读者传达这样一个概念:Redis是一个可以用来解决问题的工具,它既拥有其他数据库不具备的数据结构,又拥有内存存储(这使得Redis的速度非常快)、远程(这使得Redis可以与多个客户端和服务器进行连接)、持久化(这使得服务器可以在重启之后仍然保持重启之前的数据)和可扩展(通过主从复制和分片)等多个特性,这使得用户可以以熟悉的方式为各种不同的问题构建解决方案。

在阅读本书的后续章节时,请读者注意自己解决问题的方式发生了什么变化:你也许会惊讶地发现,自己思考数据问题的方式已经从原来的“怎样将我的想法塞进数据库的表和行里面”,变成了“使用哪种Redis数据结构来解决这个问题比较好呢?”。

接下来的第2章将介绍使用Redis构建Web应用的方法,阅读这一章将帮助你更好地了解Redis的用法和用途。

 分片是一种将数据划分为多个部分的方法,对数据的划分可以基于键包含的ID、基于键的散列值,或者基于以上两者的某种组合。通过对数据进行分片,用户可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。

 客观来讲,memcached也能用在这个简单的场景里,但使用Redis存储聚合数据有以下3个好处:首先,使用Redis可以将彼此相关的聚合数据放在同一个结构里面,这样访问聚合数据就会变得更为容易;其次,使用Redis可以将聚合数据放到有序集合里面,构建出一个实时的排行榜;最后,Redis的聚合数据可以是整数或者浮点数,而memcached的聚合数据只能是整数。


本章主要内容

  • 登录cookie
  • 购物车cookie
  • 缓存生成的网页
  • 缓存数据库行
  • 分析网页访问记录

前面的第1章对Redis的特性和功能做了简单的介绍,本章将紧接上一章的步伐,通过几个示例,对一些典型的Web应用进行介绍。尽管本章展示的问题比起实际情况要简单得多,但这里给出的网络应用实际上只需要进行少量修改就可以直接应用到真实的程序里面。本章的主要任务是作为一个实用指南,告诉你可以使用Redis来做些什么事情,而之后的第3章将对Redis命令进行更详细的介绍。

从高层次的角度来看,Web应用就是通过HTTP协议对网页浏览器发送的请求进行响应的服务器或者服务(service)。一个Web服务器对请求进行响应的典型步骤如下。

(1)服务器对客户端发来的请求(request)进行解析。

(2)请求被转发给一个预定义的处理器(handler)。

(3)处理器可能会从数据库中取出数据。

(4)处理器根据取出的数据对模板(template)进行渲染(render)。

(5)处理器向客户端返回渲染后的内容作为对请求的响应(response)。

以上列举的5个步骤从高层次的角度展示了典型Web服务器的运作方式,这种情况下的Web请求被认为是无状态的(stateless),也就是说,服务器本身不会记录与过往请求有关的任何信息,这使得失效(fail)的服务器可以很容易地被替换掉。有不少书籍专门介绍了如何优化响应过程的各个步骤,本书要做的事情也和它们类似,不同之处在于,本书讲解的是如何使用更快的Redis查询来代替传统的关系数据库查询,以及如何使用Redis来完成一些使用关系数据库没办法高效完成的任务。

本章的所有内容都是围绕着发现并解决Fake Web Retailer这个虚构的大型网上商店来展开的,这个商店每天都会有大约500万名不同的用户,这些用户会给网站带来1亿次点击,并从网站购买超过10万件商品。我们之所以将Fake Web Retailer的几个数据量设置得特别大,是考虑到如果可以在大数据量背景下顺利地解决问题,那么解决小数据量和中等数据量引发的问题就更不在话下了。另外,尽管本章展示的解决方案都是为了解决Fake Web Retailer这个大型网店所遇到的问题而给出的,但除了其中一个解决方案之外,其他所有解决方案都可以在一个只有几GB内存的Redis服务器上面使用,并且这些解决方案的目标都在于提高系统响应实时请求的性能。

本章列举的所有解决方案(以及它们的一些变种)都在生产环境中实际使用过。说得更具体一点,通过将传统数据库的一部分数据处理任务以及存储任务转交给Redis来完成,可以提升网页的载入速度,并降低资源的占用量。

我们要解决的第一个问题就是使用Redis来管理用户登录会话(session)。

每当我们登录互联网服务(比如银行账户或者电子邮件)的时候,这些服务都会使用cookie来记录我们的身份。cookie由少量数据组成,网站会要求我们的浏览器存储这些数据,并且在每次服务发出请求时再将这些数据传回给服务。对于用来登录的cookie,有两种常见的方法可以将登录信息存储在cookie里面:一种是签名(signed)cookie,另一种是令牌(token)cookie。

签名cookie通常会存储用户名,可能还有用户ID、用户最后一次成功登录的时间,以及网站觉得有用的其他任何信息。除了用户的相关信息之外,签名cookie还包含一个签名,服务器可以使用这个签名来验证浏览器发送的信息是否未经改动(比如将cookie中的登录用户名改成另一个用户)。

令牌 cookie会在cookie里面存储一串随机字节作为令牌,服务器可以根据令牌在数据库中查找令牌的拥有者。随着时间的推移,旧令牌会被新令牌取代。表2-1展示了签名cookie和令牌cookie的优点与缺点。

表2-1 签名cookie和令牌cookie的优点与缺点

cookie类型

优点

缺点

签名cookie

验证cookie所需的一切信息都存储在cookie里面。cookie可以包含额外的信息(additional infomation),并且对这些信息进行签名也很容易

正确地处理签名很难。很容易忘记对数据进行签名,或者忘记验证数据的签名,从而造成安全漏洞

令牌cookie

添加信息非常容易。cookie的体积非常小,因此移动终端和速度较慢的客户端可以更快地发送请求

需要在服务器中存储更多信息。如果使用的是关系数据库,那么载入和存储cookie的代价可能会很高

因为Fake Web Retailer没有实现签名cookie的需求,所以我们选择了使用令牌cookie来引用关系数据库表中负责存储用户登录信息的条目(entry)。除了用户登录信息之外,Fake Web Retailer还可以将用户的访问时长和已浏览商品的数量等信息存储到数据库里面,这样便于将来通过分析这些信息来学习如何更好地向用户推销商品。

一般来说,用户在决定购买某个或某些商品之前,通常都会先浏览多个不同的商品,而记录用户浏览过的所有商品以及用户最后一次访问页面的时间等信息,通常会导致大量的数据库写入。从长远来看,用户的这些浏览数据的确非常有用,但问题在于,即使经过优化,大多数关系数据库在每台数据库服务器上面每秒也只能插入、更新或者删除200~2000 个数据库行。尽管批量插入、批量更新和批量删除等操作可以以更快的速度执行,但因为客户端每次浏览网页都只更新少数几个行,所以高速的批量插入在这里并不适用。

因为Fake Web Retailer目前一天的负载量相对比较大——平均情况下每秒大约1200次写入,高峰时期每秒接近6000次写入,所以它必须部署10台关系数据库服务器才能应对高峰时期的负载量。而我们要做的就是使用Redis重新实现登录cookie功能,取代目前由关系数据库实现的登录cookie功能。

首先,我们将使用一个散列来存储登录cookie令牌与已登录用户之间的映射。要检查一个用户是否已经登录,需要根据给定的令牌来查找与之对应的用户,并在用户已经登录的情况下,返回该用户的ID。代码清单2-1展示了检查登录cookie的方法。

代码清单2-1 check_token()函数

对令牌进行检查并不困难,因为大部分复杂的工作都是在更新令牌时完成的:用户每次浏览页面的时候,程序都会对用户存储在登录散列里面的信息进行更新,并将用户的令牌和当前时间戳添加到记录最近登录用户的有序集合里面;如果用户正在浏览的是一个商品页面,那么程序还会将这个商品添加到记录这个用户最近浏览过的商品的有序集合里面,并在被记录商品的数量超过25个时,对这个有序集合进行修剪。代码清单2-2展示了程序更新令牌的方法。

代码清单2-2 update_token()函数

通过update_token()函数,我们可以记录用户最后一次浏览商品的时间以及用户最近浏览了哪些商品。在一台最近几年生产的服务器上面,使用update_token()函数每秒至少可以记录20 000件商品,这比Fake Web Retailer高峰时期所需的6000次写入要高3倍有余。不仅如此,通过后面介绍的一些方法,我们还可以进一步优化update_token()函数的运行速度。但即使是现在这个版本的update_token()函数,比起原来的关系数据库,性能也已经提升了10~100倍。

因为存储会话数据所需的内存会随着时间的推移而不断增加,所以我们需要定期清理旧的会话数据。为了限制会话数据的数量,我们决定只保存最新的1000万个会话。清理旧会话的程序由一个循环构成,这个循环每次执行的时候,都会检查存储最近登录令牌的有序集合的大小,如果有序集合的大小超过了限制,那么程序就会从有序集合里面移除最多100个最旧的令牌,并从记录用户登录信息的散列里面,移除被删除令牌对应的用户的信息,并对存储了这些用户最近浏览商品记录的有序集合进行清理。与此相反,如果令牌的数量未超过限制,那么程序会先休眠1秒,之后再重新进行检查。代码清单2-3展示了清理旧会话程序的具体代码。

代码清单2-3 clean_sessions()函数

让我们通过计算来了解一下,这段简单的代码为什么能够妥善地处理每天500万人次的访问:假设网站每天有500万用户访问,并且每天的用户都和之前的不一样,那么只需要两天,令牌的数量就会达到1000万个的上限,并将网站的内存空间消耗殆尽。因为一天有24×3600=86 400秒,而网站平均每秒产生5 000 000/86 400<58个新会话,如果清理函数和我们之前在代码里面定义的一样,以每秒一次的频率运行的话,那么它每秒需要清理将近60个令牌,才能防止令牌数量过多的问题发生。但是实际上,我们定义的令牌清理函数在通过网络来运行时,每秒能够清理10 000多个令牌,在本地运行时,每秒能够清理60 000多个令牌,这比所需的清理速度快了150~1000倍,所以因为旧令牌过多而导致网站空间耗尽的问题不会出现。

在哪里执行清理函数? 本书会包含一些类似代码清单2-3的清理函数,它们可能会像代码清单2-3那样,以守护进程的方式来运行,也可能会作为定期作业(cron job)每隔一段时间运行一次,甚至在每次执行某个操作时运行一次(例如,6.3节就在一个获取锁操作里面包含了一个清理操作)。一般来说,本书中包含while not QUIT:代码的函数都应该作为守护进程来执行,不过如果有需要的话,也可以把它们改成周期性地运行。

Python传递和接收可变数量参数的语法 代码清单2-3用到了3次类似conn.delete (*vtokens)这样的语法。简单来说,这种语法可以直接将一连串的多个参数传入函数里面,而不必先对这些参数进行解包(unpack)。要了解关于这一语法的更多信息,请通过以下短链接访问《Python语言教程》的相关章节:http://mng.bz/8I7W

Redis 的过期数据处理 随着对Redis的了解逐渐加深,读者应该会慢慢发现本书展示的一些解决方案有时候并不是问题的唯一解决办法。比如对于这个登录cookie例子来说,我们可以直接将登录用户和令牌的信息存储到字符串键值对里面,然后使用Redis的EXPIRE命令,为这个字符串和记录用户商品浏览记录的有序集合设置过期时间,让Redis在一段时间之后自动删除它们,这样就不需要再使用有序集合来记录最近出现的令牌了。但是这样一来,我们就没有办法将会话的数量限制在1000万之内了,并且在将来有需要的时候,我们也没办法在会话过期之后对被废弃的购物车进行分析了。

熟悉多线程编程或者并发编程的读者可能会发现代码清单2-3展示的清理函数实际上包含一个竞争条件(race condition):如果清理函数正在删除某个用户的信息,而这个用户又在同一时间访问网站的话,那么竞争条件就会导致用户的信息被错误地删除。目前来看,这个竞争条件除了会使得用户需要重新登录一次之外,并不会对程序记录的数据产生明显的影响,所以我们暂时先搁置这个问题,之后的第3章和第4章会说明怎样防止类似的竞争条件发生,并进一步加快清理函数的执行速度。

通过使用Redis来记录用户信息,我们成功地将每天要对数据库执行的行写入操作减少了数百万次。虽然这非常的了不起,但这只是我们使用Redis构建Web应用程序的第一步,接下来的一节将向读者们展示如何使用Redis来处理另一种类型的cookie。

网景(Netscape)公司在20世纪90年代中期最先在网络中使用了cookie,这些cookie最终变成了我们在上一节讨论的登录会话cookie。cookie最初的意图在于为网络零售商(web retailer)提供一种购物车,让用户可以收集他们想要购买的商品。在cookie之前,有过几种不同的购物车解决方案,但这些方案全都不太好用。

使用cookie实现购物车——也就是将整个购物车都存储到cookie里面的做法非常常见,这种做法的一大优点是无须对数据库进行写入就可以实现购物车功能,而缺点则是程序需要重新解析和验证(validate)cookie,确保cookie的格式正确,并且包含的商品都是真正可购买的商品。cookie购物车还有一个缺点:因为浏览器每次发送请求都会连cookie一起发送,所以如果购物车cookie的体积比较大,那么请求发送和处理的速度可能会有所降低。

因为我们在前面已经使用Redis实现了会话cookie和记录用户最近浏览过的商品这两个特性,所以我们决定将购物车的信息也存储到Redis里面,并且使用与用户会话cookie相同的cookie ID来引用购物车。

购物车的定义非常简单:每个用户的购物车都是一个散列,这个散列存储了商品ID与商品订购数量之间的映射。对商品数量进行验证的工作由Web应用程序负责,我们要做的则是在商品的订购数量出现变化时,对购物车进行更新:如果用户订购某件商品的数量大于0,那么程序会将这件商品的ID以及用户订购该商品的数量添加到散列里面,如果用户购买的商品已经存在于散列里面,那么新的订购数量会覆盖已有的订购数量;相反地,如果用户订购某件商品的数量不大于0,那么程序将从散列里面移除该条目。代码清单2-4的add_to_cart()函数展示了程序是如何更新购物车的。

代码清单2-4 add_to_cart()函数

接着,我们需要对之前的会话清理函数进行更新,让它在清理旧会话的同时,将旧会话对应用户的购物车也一并删除,更新后的函数如代码清单2-5所示。

代码清单2-5 clean_full_sessions()函数

我们现在将会话和购物车都存储到了Redis里面,这种做法除了可以减少请求的体积之外,还使得我们可以根据用户浏览过的商品、用户放入购物车的商品以及用户最终购买的商品进行统计计算,并构建起很多大型网络零售商都在提供的“在查看过这件商品的用户当中,有X%的用户最终购买了这件商品”“购买了这件商品的用户也购买了某某其他商品”等功能,这些功能可以帮助用户查找其他相关的商品,并最终提升网站的销售业绩。

通过将会话cookie和购物车cookie存储在Redis里面,我们得到了进行数据分析所需的两个重要的数据来源,接下来的一节将展示如何使用缓存来减少数据库和Web前端的负载。

在动态生成网页的时候,通常会使用模板语言(templating language)来简化网页的生成操作。需要手写每个页面的日子已经一去不复返——现在的Web页面通常由包含首部、尾部、侧栏菜单、工具条、内容域的模板生成,有时候模板还用于生成JavaScript。

尽管Fake Web Retailer也能够动态地生成内容,但这个网站上的多数页面实际上并不会经常发生大的变化:虽然会向分类中添加新商品、移除旧商品、有时有特价促销、有时甚至还有“热卖商品”页面,但是在一般情况下,网站只有账号设置、以往订单、购物车(结账信息)以及其他少数几个页面才包含需要每次载入都要动态生成的内容。

通过对浏览数据进行分析,Fake Web Retailer发现自己所处理的95%的Web页面每天最多只会改变一次,这些页面的内容实际上并不需要动态地生成,而我们的工作就是想办法不再生成这些页面。减少网站在动态生成内容上面所花的时间,可以降低网站处理相同负载所需的服务器数量,并让网站的速度变得更快。(研究表明,减少用户等待页面载入的时间,可以增加用户使用网站的欲望,并改善用户对网站的印象。)

所有标准的Python应用框架都提供了在处理请求之前或者之后添加层(layer)的能力,这些层通常被称为中间件(middleware)或者插件(plugin)。我们将创建一个这样的层来调用Redis缓存函数:对于一个不能被缓存的请求,函数将直接生成并返回页面;而对于可以被缓存的请求,函数首先会尝试从缓存里面取出并返回被缓存的页面,如果缓存页面不存在,那么函数会生成页面并将其缓存在Redis里面5分钟,最后再将页面返回给函数调用者。代码清单2-6展示了这个缓存函数。

代码清单2-6 cache_request()函数

对于Fake Web Retailer网站上面95%的可被缓存并且频繁被载入的内容来说,代码清单2-6展示的缓存函数可以让网站在5分钟之内无须再为它们动态地生成视图页面。取决于网页的内容有多复杂,这一改动可以将包含大量数据的页面的延迟值从20~50毫秒降低至查询一次Redis所需的时间:查询本地Redis的延迟值通常低于1毫秒,而查询位于同一个数据中心的Redis的延迟值通常低于5毫秒。对于那些需要访问数据库的页面来说,这个缓存函数对于减少页面载入时间和降低数据库负载的作用会更加显著。

在这一节中,我们学习了如何使用Redis来减少载入不常改变页面所需的时间,那么对于那些经常发生变化的页面,我们是否也能够使用Redis来减少它们的载入时间呢?答案是肯定的,接下来的一节将介绍实现这一目标的具体做法。

到目前为止,我们已经将原本由关系数据库和网页浏览器实现的登录和访客会话转移到了Redis上面实现;将原本由关系数据库实现的购物车也放到了Redis上面实现;还将所有页面缓存到了Redis里面。这一系列工作提升了网站的性能,降低了关系数据库的负载并减少了网站成本。

Fake Web Retailer的商品页面通常只会从数据库里面载入一两行数据,包括已登录用户的用户信息(这些信息可以通过AJAX动态地载入,所以不会对页面缓存造成影响)和商品本身的信息。即使是那些无法被整个缓存起来的页面——比如用户账号页面、记录用户以往购买商品的页面等等,程序也可以通过缓存页面载入时所需的数据库行来减少载入页面所需的时间。

为了展示数据行缓存的作用,我们假设Fake Web Retailer为了清空旧库存和吸引客户消费,决定开始新一轮的促销活动:这个活动每天都会推出一些特价商品供用户抢购,所有特价商品的数量都是限定的,卖完即止。在这种情况下,网站是不能对整个促销页面进行缓存的,因为这可能会导致用户看到错误的特价商品剩余数量,但是每次载入页面都从数据库里面取出特价商品的剩余数量的话,又会给数据库带来巨大的压力,并导致我们需要花费额外的成本来扩展数据库。

为了应对促销活动带来的大量负载,我们需要对数据行进行缓存,具体的做法是:编写一个持续运行的守护进程函数,让这个函数将指定的数据行缓存到Redis里面,并不定期地对这些缓存进行更新。缓存函数会将数据行编码(encode)为JSON字典并存储在Redis的字符串里面,其中,数据列(column)的名字会被映射为JSON字典的键,而数据行的值则会被映射为JSON字典的值,图 2-1展示了一个被缓存的数据行示例。

图2-1 一个被缓存的数据行,这个数据行包含了在线售卖商品的信息

程序使用了两个有序集合来记录应该在何时对缓存进行更新:第一个有序集合为调度(schedule)有序集合,它的成员为数据行的行ID,而分值则是一个时间戳,这个时间戳记录了应该在何时将指定的数据行缓存到Redis里面;第二个有序集合为延时(delay)有序集合,它的成员也是数据行的行ID,而分值则记录了指定数据行的缓存需要每隔多少秒更新一次。

使用JSON而不是其他格式 因为JSON简明易懂,并且据我们所知,目前所有拥有Redis客户端的编程语言都带有能够高效地编码和解码JSON格式的函数库,所以这里的缓存函数使用了JSON格式来表示数据行,而没有使用XML、Google的protocol buffer、Thrift、BSON、MessagePack或者其他序列化格式。在实际应用中,读者可以根据自己的需求和喜好来选择编码数据行的格式。

嵌套多个结构 使用过其他非关系数据库的用户可能会期望Redis也拥有嵌套多个结构的能力,比如说,一个刚开始使用Redis的用户可能会期盼着散列能够包含有序集合值或者列表值。尽管嵌套结构这个特性在概念上并无不妥,但这个特性很快就会引起类似以下这样的问题:“对于一个位于嵌套第5层的散列,我们如何才能对它的值执行自增操作呢?”为了保证命令语法的简单性,Redis并不支持嵌套结构特性。如果有需要的话,读者可以通过使用键名来模拟嵌套结构特性:比如使用键user:123表示存储用户信息的散列,并使用键user:123:posts表示存储用户最近发表文章的有序集合;又或者直接将嵌套结构存储到JSON或者其他序列化格式里面(第11章将介绍使用Lua脚本在服务器端直接以JSON格式或者MessagePack格式对数据进行编码的方法)。

为了让缓存函数定期地缓存数据行,程序首先需要将行ID和给定的延迟值添加到延迟有序集合里面,然后再将行ID和当前时间的时间戳添加到调度有序集合里面。实际执行缓存操作的函数需要用到数据行的延迟值,如果某个数据行的延迟值不存在,那么这个调度商品将会被移除。如果我们想要移除某个数据行已有的缓存,并且让缓存函数不再缓存那个数据行,那么只需要把那个数据行的延迟值设置为小于或等于0就可以了。代码清单2-7展示了负责调度缓存和终止缓存的函数。

代码清单2-7 schedule_row_cache()函数

现在我们已经完成了调度部分,那么接下来该如何对数据行进行缓存呢?负责缓存数据行的函数会尝试读取调度有序集合的第一个元素以及该元素的分值,如果调度有序集合没有包含任何元素,或者分值存储的时间戳所指定的时间尚未来临,那么函数会先休眠50毫秒,然后再重新进行检查。当缓存函数发现一个需要立即进行更新的数据行时,缓存函数会检查这个数据行的延迟值:如果数据行的延迟值小于或者等于0,那么缓存函数会从延迟有序集合和调度有序集合里面移除这个数据行的ID,并从缓存里面删除这个数据行已有的缓存,然后再重新进行检查;对于延迟值大于0的数据行来说,缓存函数会从数据库里面取出这些行,将它们编码为JSON格式并存储到Redis里面,然后更新这些行的调度时间。执行以上工作的缓存函数如代码清单2-8所示。

代码清单2-8 守护进程函数cache_rows()

通过组合使用调度函数和持续运行缓存函数,我们实现了一种重复进行调度的自动缓存机制,并且可以随心所欲地控制数据行缓存的更新频率:如果数据行记录的是特价促销商品的剩余数量,并且参与促销活动的用户非常多的话,那么我们最好每隔几秒更新一次数据行缓存;另一方面,如果数据并不经常改变,或者商品缺货是可以接受的,那么我们可以每分钟更新一次缓存。

在这一节中,我们学习了如何将数据行缓存到Redis里面,在接下来的一节中,我们将通过只缓存一部分页面来减少实现页面缓存所需的内存数量。

网站可以从用户的访问、交互和购买行为中收集到有价值的信息。例如,如果我们只想关注那些浏览量最高的页面,那么我们可以尝试修改页面的格局、配色甚至是页面上展示的其他链接。每一个修改尝试都能改变用户对一个页面或者后续页面的体验,或好或坏,甚至还能影响用户的购买行为。

前面的2.1节和2.2节中介绍了如何记录用户浏览过的商品或者用户添加到购物车中的商品,2.3节中则介绍了如何通过缓存Web页面来减少页面载入时间并提升页面的响应速度。不过遗憾的是,我们对Fake Web Retailer采取的缓存措施做得过了火:Fake Web Retailer总共包含100 000件商品,而贸然地缓存所有商品页面将耗尽整个网站的全部内存!经过一番调研之后,我们决定只对其中10 000件商品的页面进行缓存。

前面的2.1节中曾经介绍过,每个用户都有一个相应的记录用户浏览商品历史的有序集合,尽管使用这些有序集合可以计算出用户最经常浏览的商品,但进行这种计算却需要耗费大量的时间。为了解决这个问题,我们决定在update_token()函数里面添加一行代码,如代码清单2-9所示。

代码清单2-9 修改后的update_token()函数

新添加的代码记录了所有商品的浏览次数,并根据浏览次数对商品进行了排序,被浏览得最多的商品将被放到有序集合的索引0位置上,并且具有整个有序集合最少的分值。随着时间的流逝,商品的浏览次数会呈现两极分化的状态,一些商品的浏览次数会越来越多,而另一些商品的浏览次数则会越来越少。除了缓存最常被浏览的商品之外,程序还需要发现那些变得越来越流行的新商品,并在合适的时候缓存它们。

为了让商品浏览次数排行榜能够保持最新,我们需要定期修剪有序集合的长度并调整已有元素的分值,从而使得新流行的商品也可以在排行榜里面占据一席之地。之前的2.1节中已经介绍过从有序集合里面移除元素的方法,而调整元素分值的动作则可以通过ZINTERSTORE命令来完成。ZINTERSTORE命令可以组合起一个或多个有序集合,并将有序集合包含的每个分值都乘以一个给定的数值(用户可以为每个有序集合分别指定不同的相乘数值)。每隔5分钟,代码清单2-10展示的函数就会删除所有排名在20 000名之后的商品,并将删除之后剩余的所有商品的浏览次数减半。

代码清单2-10 守护进程函数rescale_viewed()

通过记录商品的浏览次数,并定期对记录浏览次数的有序集合进行修剪和分值调整,我们为Fake Web Retailer建立起了一个持续更新的最常浏览商品排行榜。接下来要做的就是修改之前介绍过的can_cache()函数,让它使用新的方法来判断页面是否需要被缓存,如代码清单 2-11所示。

代码清单2-11 can_cache()函数

通过使用前面介绍的几个函数,Fake Web Retailer现在可以统计商品被浏览的次数,并以此来缓存用户最经常浏览的10 000个商品页面。如果我们想以最少的代价来存储更多页面,那么可以考虑先对页面进行压缩,然后再缓存到Redis里面;或者使用Edge Side Includes技术移除页面中的部分内容;又或者对模板进行提前优化(pre-optimize),移除所有非必要的空格字符。这些技术能够减少内存消耗并增加Redis能够缓存的页面数量,为访问量不断增长的网站带来额外的性能提升。

本章介绍了几种用于降低Fake Web Retailer的数据库负载和Web服务器负载的方法,这些例子里面介绍的都是真实的Web应用程序当今正在使用的思路和方法。

本章希望向读者传达这样一个概念:在为应用程序创建新构件时,不要害怕回过头去重构已有的构件,因为就像本章展示的购物车cookie的例子和基于登录会话cookie实现网页分析的例子一样,已有的构件有时候需要进行一些细微的修改才能真正满足你的需求。本书之后的章节也会继续引入新的主题,并且偶尔会回过头去审视之前介绍过的主题,对它们的功能或者性能进行改进,又或者重用之前已经介绍过的思路。

本章向读者介绍了怎样使用Redis来构建真实的应用程序组件,下一章将向读者介绍Redis提供的各种命令:通过更深入地了解Redis提供的各种结构以及这些结构的作用,读者将掌握到构建更复杂也更有用的组件所需的知识。不要犹豫,赶快阅读下一章吧!

 因为Fake Web Retailer这个示例假设的是生产环境,所以保存会话的数量会设置得比较高,在测试或者开发这个程序的时候,读者可以按照自己的需要调低这个值。


相关图书

Redis入门指南 第3版
Redis入门指南 第3版
Redis入门指南(第2版)
Redis入门指南(第2版)
Redis入门指南
Redis入门指南

相关文章

相关课程