Redis3官方文档(6)
——使用Redis实现Twitter(上)
——使用Redis实现Twitter(上)
本文讲述使用PHP以及Redis来设计和实现一个简单的微博。编程社区传统上认为,在开发web应用程序时,作为特殊目的的键值存储数据库不能用于替换关系型数据库。本文将向你展示Redis在键值层之上的数据结构是实现各种应用程序的有效数据模型。
在继续之前,你可以花点时间体验一下在线演示(http://retwis.redis.io,译者注),看看我们究竟要做什么。长话短说:这是个练手,但是已经足够复杂到让你学习如何创建一个更复杂的程序的基础。
注意:这篇文章的原始版本写于2009年Redis发布时。当时还不清楚Redis的数据模型适合整个程序。5年以后的今天,已经有许多应用程序使用Redis作为他们的主要存储,所以今天这篇文章的目的就是作为新学者的教程。你讲学习如何使用Redis设计一个简单的数据层,如何应用不同的数据结构。
我们的微博系统,叫做Retwis,结构简单,具有很高的性能,只需少许努力能够分布于任意数量的web和Redis服务器。你可以在这里找到源代。
我使用PHP来做这个例子,是因为每个人都能看懂。使用Ruby,Python,Erlang等等语言也能得到同样(或更好)的结果。也有一些其他的实现(但是不是所有的实现都使用和当前版本教程同样的数据层,所以请使用PHP官方实现会更好)。
- Retwis-RB是由Daniel Lucraft使用Ruby和Sinatra实现的版本!当然包含了全部的源代码,以及文章底部一个指向Git仓库的链接。本文剩下的部分定位为PHP,但是Ruby程序员也可以查看Redis-RB的代码,因为他们从概念上非常相似。
- Retwis-J是由Costin Leau使用Spring Data Framework和Java实现的版本。代码可以在GitHub上找到,springsource.org上有更全面的文档介绍。
此处省略一万字。。。
(原文此处是对Redis数据类型的介绍,可以参考本系列文章的第2篇和第3篇,译者注)
前提条件(Prerequisites)
如果你还没有下载Retwis源码请先下载。包含一些PHP文件和Predis的一份拷贝(例子中我们使用的客户端库)。
另外你想要做的一件事是一个运行的Redis服务器。下载源码,使用make构建,使用./redis-server运行,你就可以开始了。只是玩玩或者运行我们的Retwis的话,不需要配置。
数据设计(Layout)
当使用关系型数据库时,必须先设计数据库模式,这样我们先需要知道表,索引等数据库确定的东西。Redis没有表,那我们需要设计什么呢?我们需要确定需要什么键来表示我们的对象,以及这些键需要存储什么值。
让我们从用户开始。我们需要用户名、用户id,密码,用户粉丝(following),关注列表等等来表示用户。第一个问题是,我们如果标识一个用户?像在关系型数据库,一个好的解决方案是用不同的号码来标识不同的用户,所以我们可以关联一个唯一ID给每个用户。对这个用户的引用通过其ID。产生唯一ID非常简单,使用我们的原子INCR操作。当我们创建一个新用户我们就可以(假设用户名为antirez):
INCR next_user_id => 1000 HMSET user:1000 username antirez password p1pp0
注意:在真实程序中你应该使用哈希的密码,为了简化我们直接存储密码明文。
我们使用next_user_id键为每一位新用户提供唯一ID。然后我们使用唯一ID来命名存储用户数据的哈希结构的键。记住,这是使用键值存储的通用设计模式!除了字段已经被定义了以外,我们还需要更多东西来完整定义一个用户。例如,有时通过用户名获得用户ID,于是我们每次添加一个用户,我们也需要操作用户的键,使用用户名作为字段,用ID作为值的哈希。
HSET users antirez 1000
这一开始看起来有点奇怪,但是记住,我们只能采取直接访问数据的方式,而没有第二层索引。没法告诉Redis根据一个指定值返回其键。这也是我们的优势。强制我们使用按照主键来访问一切的新的范式来组织数据,此处主键是关系型数据库中的术语。
粉丝(followers),关注(following),和帖子(updates)
我们的系统还有一个核心需求。一个用户可能有很多关注他的用户,我们称他们为其粉丝。一个用户也可能会关注其他用户,我们称他们为其关注者。我们有一个为此量身打造的数据结构,就是集合。独一无二的集合元素,常量时间测试存在性,是两个非常有趣的特性。然而,记录一个用户开始关注另一个用户的时间怎么办?在我们加强版的微博系统里面。我们使用有序集合而不是一个简单的集合,用粉丝或者粉儿的用户ID作为元素,用用户关系创建时的unix时间作为分数。
让我们来定义我们的键:
followers:1000 => Sorted Set of uids of all the followers users following:1000 => Sorted Set of uids of all the following users
我们添加一个粉丝:
ZADD followers:1000 1401267618 1234 => Add user 1234 with time 1401267618
另外一件重要的事情我们需要一个用户首页的位置来展示用户的更新。我们需要按照时间顺序来访问这些数据,从最近的到最老的,为此最好的数据结构就是列表。基本上每一个更新都会被LPUSH到用户更新键,多亏了LRANGE,我们能实现分页等等。注意,我们可以互换地使用更新(updates)和帖子(posts)这两个词,因为某种意义上说,更新其实就是小型帖子。
posts:1000 => a List of post ids - every new post is LPUSHed here.
这个列表基本上就是用户的时间轴。我们会加入他自己帖子ID,以及其关注者创建的帖子。基本上我们实现了一个写分列。
身份验证(Authentication)
好了,我们或多或少已经有了关于用户的一切,除了身份验证。我们会用一种简单而又健壮的方式处理身份验证:我们不想使用PHP的会话机制,我们的系统要为轻松地分布式部署于很多web服务器上而准备,所以我们会保存全部状态到Redis数据库中。所有我们要做的就是要设置一个猜不出来的字符串作为认证用户的cookie,以及一个持有该字符串的客户端的用户ID的一个键。
我们需要两件事情来使得这个可以工作得健壮。第一,当前认证秘钥(不可猜测的字符串)是用户对象的一部分,所以当创建用户时,我们需要在哈希中设置一个认证字段:
HSET user:1000 auth fea5e81ac8ca77622bed1c2132a021f9
另外,我们需要映射认证秘钥到用户ID,所以我们也需要一个认证键,使用哈希来映射秘钥和用户ID。
HSET auths fea5e81ac8ca77622bed1c2132a021f9 1000
为了认证一个用户,我们只需要简单几步(请查看Retwis项目中的login.php源代码):
- 从登陆表单获取用户名和密码。
- 检查用户名是否存在于users哈希中。
- 如果存在,我们获取其ID(例如1000)。
- 检查user:1000的密码是否匹配,否则返回错误消息。
- 认证完毕,设置"fea5e81ac8ca77622bed1c2132a021f9"(user:1000的auth字段)作为认证cookie。
这是真实的代码:
include("retwis.php"); # Form sanity checks if (!gt("username") || !gt("password")) goback("You need to enter both username and password to login."); # The form is ok, check if the username is available $username = gt("username"); $password = gt("password"); $r = redisLink(); $userid = $r->hget("users",$username); if (!$userid) goback("Wrong username or password"); $realpassword = $r->hget("user:$userid","password"); if ($realpassword != $password) goback("Wrong useranme or password"); # Username / password OK, set the cookie and redirect to index.php $authsecret = $r->hget("user:$userid","auth"); setcookie("auth",$authsecret,time()+3600*24*365); header("Location: index.php");
这些发生在每次用户登录时,但是我们还需要一个isLoggedIn函数来检查用户是否已经通过身份认证。以下是isLoggedIn函数的逻辑步骤:
- 从用户获取auth cookie。如果没有cookie则用户没有登录。我们称这个cookie值为
。 - 检查
是否存在于auths哈希字段中,以及其值(即用户ID,本例中是1000)。 - 为了系统更加健壮,验证user:1000的auth字段是否匹配。
- 用户验证完成,我们从$User全局变量中加载一些信息。
代码也许比上面的描述更简单:
function isLoggedIn() { global $User, $_COOKIE; if (isset($User)) return true; if (isset($_COOKIE['auth'])) { $r = redisLink(); $authcookie = $_COOKIE['auth']; if ($userid = $r->hget("auths",$authcookie)) { if ($r->hget("user:$userid","auth") != $authcookie) return false; loadUserInfo($userid); return true; } } return false; } function loadUserInfo($userid) { global $User; $r = redisLink(); $User['id'] = $userid; $User['username'] = $r->hget("user:$userid","username"); return true; }
===============================================================================
大家好,我是阮威。华中科技大学,计算机软件专业硕士。毕业后加入腾讯,先后在腾讯电子商务部和无线游戏产品部工作,现供职于欢聚时代基础产品部。IT男,至今。对技术有强烈的热情,对技术以外的事物热情更高。
我的座右铭是“呼喊移动人的移动连接,践行自由人的自由联合”。欢迎大家收听我的公众账号。