Redis应用实例Twitter Alike Example

来源:互联网 发布:记录地址的软件 编辑:程序博客网 时间:2024/06/05 03:14

1.1     A case study: Design and implementation of a simple Twitter clone using only the Redis key-value store as database and PHP

本章将阐述一个模仿Twitter应用的设计与实现,使用PHP并把Redis作为唯一的数据库。编程者社区常常将key-value存储是为一个特殊的数据库,不能被用来代替web应用开发中的关系数据库。本证将证明相反的结论。

我们的Twitter模仿,叫做Retwis,结构简单而性能优秀,可以很小成本发布到n个web server和M个Redis Server上。你可以从这里找到源码。

用PHP举例是因为php可能更迅捷,同样(或者更好)的结果对Ruby,Python,Erlang一样。

1.2     Key-value stores basics

Key-value存储的本质是存储数据的能力,是包含一个key的value。当我们知道存储时的key时,我们可以之后获取这个数据。没有办法用value来查询什么东西。比如我使用SET来将值bar存储在键key上:

SET foo bar

Redis将永久保存我们的数据,所以我们以后可以查询”键foo上存储的值是什么?”,而Redis将返回bar:

GET foo => bar

其他key-value存储常用的操作是用来删除指定key及相关values的DEL,SET-if-not-exists(在Redis上叫做SETNX)设置一个key当它不存在时,以及INCR可以原子增加指定key上存储的一个数值:

SET foo 10
INCR foo => 11
INCR foo => 12
INCR foo => 13

 

1.3     Atomic operations

到现在还很简单,但是在INCR上有点特殊。想象一下,为什么提供这样的一个操作而我们可以自己通过很少的指令来实现它?毕竟这很简单:

X = GET foo

X= x +1

SET foo x

问题是,这种方式在一个客户端的时候表现良好,x任何时候只有一个值。看看如果两台机器同时访问会怎样:

x = GET foo (yields 10)
y = GET foo (yields 10)
x = x + 1 (x is now 11)
y = y + 1 (y is now 11)
SET foo x (foo is now 11)
SET foo y (foo is now 11)

出问题了!我们两次增加了值,但应该从10变到12的key现在存的是11。这是因为这个INCR操作是通过GET/increment/SET完成的,不是一个原子操作。而Redis,Memechaed..提供的INCR是原子实现的,服务端会在get-increment-set操作在需要的整个时间内保护它,以避免同时访问。

令Redis与其他key-value存储不同的是它提供了更多类似INCR的操作,可以被结合起来完成复杂的问题。这就是为什么你可以用Redis实现整个web应用而不需要一个SQL数据库,也不会因此发疯。

1.4     Beyond key-value stores

本节我们会看到建立仿Twitter需要Reids的什么功能。第一点是要知道Redis的值可以不仅仅是String。Redis的值支持List和Set,并且有原子操作来控制这些更高级的值,因此即使是在同一个key上的多访问也是安全的。从list开始:

LPUSH mylist a (now mylist holds one element list 'a')
LPUSH mylist b (now mylist holds 'b,a')
LPUSH mylist c (now mylist holds 'c,b,a')

LPUSH意思是left push,就是向mylist所存储的list的左端(或者说头部)增加一个元素。如果mylist键不存在,就会在push之前自动创建一个空的list。你可以想象,RPUSH指令会把元素加在list的右端(尾部)。

对于我们的仿Twitter来说这非常有用。比如用户的更新可以放在一个存在username:updates的list上。当然有操作从这些lists获取数据或信息。比如LRANGE返回list的一段,或者整个list。

LRANGE mylist 0 1 => c, b

LRANGE使用0起始的索引,第一个元素索引是0,第二个是1,以此类推。指令的参数是LRANGE key first-index last-index,last index参数可以是负数,-1表示列表的最后一个元素,-2是倒数第二个,类推。所以我们可以这样获取整个list:

LRANGE mylist 0 -1 => c, b, a

另一个重要的指令是LLEN,返回list的长度,以及LTRIM,LTRIM比较象LRANGE但是不是返回指定范围而是缩减list,所以它就像”获取mysql的一段,然后设置为新的值”的原子操作。我们只使用了这些List的操作,但是请务必查看Redis文档来找到所有Redis支持的List操作。

 

1.5     The set data type

不仅仅是List,Redis也支持Set,无序的元素集合。可以增加,移除以及检测成员是否存在,以及执行不同集合之间的交集。当然也可以要求list或集合的元素数目。举些例子更清晰。记住SADD是增加到set的操作,SREM是从set移除的操作,sismember是检测是否为成员的操作,SINTER是执行交集操作,SCARD是获取集合的势(成员数),SMEMBER将返回set的所有成员。

SADD myset a
SADD myset b
SADD myset foo
SADD myset bar
SCARD myset => 4
SMEMBERS myset => bar,a,foo,b

注意SMEMBER的返回成员的顺序并不是我们加入的顺序,因为set是无序的元素集合。如果你想有序存储最好用list。一些对set的操作:

SADD mynewset b
SADD mynewset foo
SADD mynewset hello
SINTER myset mynewset => foo,b

SINTER可以返回集合的交集,但是不限于两个集合,你可以查询4,5个或10000个集合。最后我们看看SISMEMBER怎么工作的:

SISMEMBER myset foo => 1
SISMEMBER myset notamember => 0

Ok,我想我们可以开始coding了!

(代码太多,没什么写在这里的价值,删掉了)

1.1     Prerequisites

如果还没有下载源码,请先下载Retwis的源码。简单的tar.gz文件,里面是几个php文件。实现很简单。你可以从里面找到用来连接Redis server的php客户端库(redis.php)。这个库文件是Ludovico Magnocavallo写的,你可以在你的项目里随意使用,但是库文件的更新版本,请从Redis的发布下载。

另一个你可能需要的是一个工作的Redis server,获取源码,用make编译,用./redis-server运行就好了。随便在你机器上泡泡,完全不需要设置什么。

 

1.2     Data layout

如果用关系数据库,这一步是规划数据应该是怎样的表,索引,等等。我们没有表,该怎么设计?我们需要识别出要表示出我们的对象需要哪些key,以及这些key要保持的是怎样的value。

从user开始。我们当然需要表示user,通过用户名username,userid,password,followers以及following user,等等。第一个问题是,在我们系统内部通过什么标记用户?username是一个好主意因为他是唯一的,但是也是很大的,我们希望节省内存。所以就像我们的数据库是关系型的一样,我们可以管理一个唯一的id到每个用户。用户所有其他信息通过id引用。做起来很简单,因为我们有一个原子操作INCR!当我们创建一个新用户的时候,我们可以像下面这样,假如用户叫做”antirez”:

INCR global:nextUserId => 1000
SET uid:1000:username antirez
SET uid:1000:password p1pp0

我们用global:nextUserId键来为每个新用户获得始终唯一的ID。然后我们用这个唯一id联接其他用户数据。这是个key-value的设计模式!牢记这点。此外的字段已经定义了,我们需要一些更多的材料来完整定义一个用户。比如有时候是很有用的就是通过username获取id,所以我们也设置这个key:

SET username:antirez:uid 1000

乍看有点奇怪,但是记住我们只能通过key访问数据!不可能告诉Redis指定的value返回key。这也是我们的力量,这个新范例强制我们这样组织数据,用关系数据库的话来说,每个东西都是通过主键访问的。

1.3     Following, followers and updates

这是另一个我们系统需要的枢纽。每个用户都有followers用户和following用户。对此我们有一个完美的数据结构!就是…set。所以我们增加两个新字段到规划里:

uid:1000:followers => Set of uids of all the followers users
uid:1000:following => Set of uids of all the following users

另一个我们需要的重要东西是我们可以增加更新到用户主页的显示上。我们需要按照时间顺序从新到旧访问这个数据,所以这项工作最完美的值是List。基本上每个新更新会LPUSH到用户更新的key上,而通过LRANGE我们可以进行分页等工作。注意我们使用的”更新”和”发布”是可替换的,因为更新在某种程度上是”小型发布”:

uid:1000:posts => a List of post ids, every new post is LPUSHed here.

 

1.4     Authentication

Ok,我们有了用户或多或少的信息,但是除了鉴权。我们将鉴权控制的简单而健壮:我们不需要php的session或者其他类似的东西,我们的系统必须为在不同的server上分布做好准备,所以我们在Redis数据库上控制整个状态。我们需要做的是一个随机串作为一个已验证用户的cookie,以及一个key来告诉我们客户端的这个随机串对应的user id。我们需要两个key来实现这种方式:

SET uid:1000:auth fea5e81ac8ca77622bed1c2132a021f9
SET auth:fea5e81ac8ca77622bed1c2132a021f9 1000

为了校验一个用户,我们做了简单的事情(login.php):

  • Get the username and password via the login form
    • 通过登录表单获取用户名和密码
  • Check if the username:<username>:uid key actually exists
    • 检查 username:<username>:uid的键值是否存在
  • If it exists we have the user id, (i.e. 1000)
    • 如果存在,我们就有了用户id
  • Check if uid:1000:password matches, if not, error message
    • 检查uid:1000:password是否匹配,如果不匹配,报错
  • Ok authenticated! Set "fea5e81ac8ca77622bed1c2132a021f9" (the value of uid:1000:auth) as "auth" cookie
    • 校验通过!设置” fea5e81ac8ca77622bed1c2132a021f9”(uid:1000:auth)作为cookie”auth”

这是实际的代码:

{代码}

这在每次用户登录的时候执行,但我们还需要一个功能isLoggedIn,以检查一个用户是否已经登录了。这是实现isLoggedIn的逻辑步骤:

  • Get the "auth" cookie from the user. If there is no cookie, the user is not logged in, of course. Let's call the value of this cookie <authcookie>
    • 获取用户cookie”quth”,如果没有,用户未登录,当然我们称这个cookie值为<authcookie>
  • Check if auth:<authcookie> exists, and what the value (the user id) is (1000 in the exmple).
    • 检测auth:<authcookie>是否存在,以及他的值
  • In order to be sure check that uid:1000:auth matches.
    • 检查与uid:1000:auth是否匹配
  • Ok the user is authenticated, and we loaded a bit of information in the $User global variable.
    • Ok,用户校验过了,我们载入一点user的全局信息。

代码比描述的要简单,可能是这样:

{代码}

loadUserInfo是单独的方法,对我们的应用来说不好,但是对复杂应用来说这是个很好的模板。校验漏掉的唯一一件事是logout。我们怎么做logout?简单,改变uid:1000:auth的随机串值,移除旧的auth:<oldauthstring>然后放一个新的auth:<newauthstring>。

重要:logout过程说明了为什么在找到auth:<randomstring>后我们为什么不是单单校验用户,而是双重校验它是否匹配uid:1000:auth。真正的校验串是后一个,而auth:<randomstring>只是一个校验key,可能是易变的,或者如果程序有bug或脚本中断,我们会有多个auth:<something>的key指向同一个user id。Logout的代码如下:(logout.php)

{代码}
这就是刚才描述的,很容易理解。

1.5     Updates

更新,或者说发布,相当的简单。为了在数据库创建一个新的更新,我们做了下面的事情:

INCR global:nextPostId => 10343
SET post:10343 "$owner_id|$time|I'm having fun with Retwis"

你可以看到,发布的用户id和时间都存在这个串里了,在这个示例程序中我们不需要根据时间或user id查找,所以好的做法是把所有信息打包到发布串里。

在创建发布后,我们获得了发布的id。我们需要LPUSH这个id到每个following这个post的用户的user,当然还有作者的发布list。这是update.php,看看它怎么做的:

{代码}

功能的核心是foreach,我们用SMEMEBER获得当前用户所有的folloer,然后对每一个follower使用LPUSH这个post到uid:<userid>:posts去。

注意我们还维护了一个所有发布的时间线。这么做只要LPUSH这个post到global:timeline就可以了。看看它,你不认为通过SQL的ORDER BY去排序一个按时间顺序插入的数据很奇怪吗?我觉得很奇怪。


0 0
原创粉丝点击