题外话:如何为字符串获取唯一标识
在标签的例子里,我们用到了标签ID,却没有提到ID从何而来。基本上你得为每个加入系统的标签分配一个唯一标识。你也希望在多个客户端同时试着添加同样的标签时不要出现竞争的情况。此外,如果标签已存在,你希望返回他的ID,否则创建一个新的唯一标识并将其与此标签关联。
Redis 1.4将增加Hash类型。有了它,字符串和唯一ID关联的事儿将不值一提,但如今我们如何用现有Redis命令可靠的解决它呢?
我们首先的尝试(以失败告终)可能如下。假设想为标签“redis”获取一个唯一ID:
1.为了让算法是二进制安全的(意即不考虑字符串的编码或空格等等,只将注意力放在标签上)我们对标签做SHA1签名。SHA1(redis)=b840fc02d524045429941cc15f59e41cb7be6c52。
2.检查这个标签是否已与一个唯一ID关联,
用命令GET tag:b840fc02d524045429941cc15f59e41cb7be6c52:id
3.如果上面的GET操作返回一个ID,则将其返回给用户。标签已经存在了。
4.否则… 用INCR next.tag.id命令生成一个新的唯一ID(假定它返回123456)。
5.最后关联标签和新的ID,
SET tag:b840fc02d524045429941cc15f59e41cb7be6c52:id 123456
并将新ID返回给调用者。
多美妙,或许更好…等等!当两个客户端同时使用这组指令尝试为标签“redis”获取唯一ID时会发生什么呢?如果时间凑巧,他们俩都会从GET操作获得nil,都将对next.tag.id key做自增操作,这个key会被自增两次。其中一个客户端会将错误的ID返回给调用者。幸运的是修复这个算法并不难,这是明智的版本:
为了让算法是二进制安全的(意即不考虑字符串的编码或空格等等,只将注意力放在标签上)我们对标签做SHA1签名。SHA1(redis)=b840fc02d524045429941cc15f59e41cb7be6c52。
检查这个标签是否已与一个唯一ID关联,
用命令GET tag:b840fc02d524045429941cc15f59e41cb7be6c52:id
如果上面的GET操作返回一个ID,则将其返回给用户。标签已经存在了。
否则… 用INCR next.tag.id命令生成一个新的唯一ID(假定它返回123456)。
下面关联标签和新的ID,(注意用到一个新的命令)
SETNX tag:b840fc02d524045429941cc15f59e41cb7be6c52:id 123456。如果另一个客户端比当前客户端更快,SETNX将不会设置key。而且,当key被成功设置时SETNX返回1,否则返回0。那么…让我们再做最后一步运算。
如果SETNX返回1(key设置成功)则将123456返回给调用者,这就是我们的标签ID,否则执行GET tag:b840fc02d524045429941cc15f59e41cb7be6c52:id 并将其结果返回给调用者。
有序集合
集合是使用频率很高的数据类型,但是…对许多问题来说他们也有点儿太不讲顺序了;)因此Redis1.2引入了有序集合。他和集合非常相似,也是二进制安全的字符串集合,但是这次带有关联的score,以及一个类似LRANGE的操作可以返回有序元素,此操作只能作用于有序集合,它就是,ZRANGE 命令。
从某种程度上说,有序集合是SQL世界的索引在Redis中的等价物。例如在刚才的reddit.com例子中,并没有提到如何根据用户投票和时间因素将新闻组合生成首页。下面我们会用有序集合解决这个问题,我们先从最简单的事情开始,阐明这个高级数据类型是如何工作的。让我们添加几个黑客,并将他们的生日作为“score”。
(integer) 1
$ redis-cli zadd hackers 1953 "Richard Stallman"
(integer) 1
$ redis-cli zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
$ redis-cli zadd hackers 1916 "Claude Shannon"
(integer) 1
$ redis-cli zadd hackers 1969 "Linus Torvalds"
(integer) 1
$ redis-cli zadd hackers 1912 "Alan Turing"
(integer) 1
对有序集合来说,按生日排序返回这些黑客易如反掌,因为他们已经是有序的。有序集合是通过一个dual-ported 数据结构实现的,它包含一个精简的有序列表和一个hash table,因此添加一个元素的时间复杂度是O(log(N))。这还行,但当我们需要访问有序的元素时,Redis不必再做任何事情,它已经是有序的了:
1. Alan Turing
2. Claude Shannon
3. Alan Kay
4. Richard Stallman
5. Yukihiro Matsumoto
6. Linus Torvalds
你知道Linus比Yukihiro年轻吗 ;)
无论如何,我想反向对这些元素排序,这次就用 ZREVRANGE 代替 ZRANGE 吧:
1. Linus Torvalds
2. Yukihiro Matsumoto
3. Richard Stallman
4. Alan Kay
5. Claude Shannon
6. Alan Turing
一个非常重要的小贴士,ZSets只是有一个“默认的”顺序,但你仍然可以用 SORT 命令对有序集合做不同的排序(但这次服务器要耗费CPU了)。要想得到多种排序,一种可选方案是同时将每个元素加入多个有序集合。
区间操作
有序集合之能不止于此,他能在区间上操作。例如获取所有1950年之前出生的人。我们用 ZRANGEBYSCORE 命令来做:
1. Alan Turing
2. Claude Shannon
3. Alan Kay
我们请求Redis返回score介于负无穷到1950年之间的元素(两个极值也包含了)。
也可以删除区间内的元素。例如从有序集合中删除生日介于1940到1960年之间的黑客。
(integer) 2
ZREMRANGEBYSCORE 这个名字虽然不算好,但他却非常有用,还会返回已删除的元素数量。
回到Reddit的例子
最后,回到 Reddit的例子。现在我们有个基于有序集合的像样方案来生成首页。用一个有序集合来包含最近几天的新闻(用 ZREMRANGEBYSCORE 不时的删除旧新闻)。用一个后台任务从有序集合中获取所有元素,根据用户投票和新闻时间计算score,然后用新闻IDs和scores关联生成 reddit.home.page 有序集合。要显示首页,我们只需闪电般的调用 ZRANGE。
不时的从 reddit.home.page 有序集合中删除过旧的新闻也是为了让我们的系统总是工作在有限的新闻集合之上。
更新有序集合的scores
结束这篇指南之前还有最后一个小贴士。有序集合scores可以在任何时候更新。只要用 ZADD 对有序集合内的元素操作就会更新它的score(和位置),时间复杂度是O(log(N)),因此即使大量更新,有序集合也是合适的。
这篇指南远未尽言,这只是从Redis开始的基础,欲深入之请读命令参考文档。
谢谢阅读。Salvatore。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Redis是一种面向“键/值”对类型数据的分布式NoSQL数据库系统,特点是高性能,持久存储,适应高并发的应用场景。它起步较晚,发展迅速,目前已被许多大型机构采用,比如Github,看看谁在用它。
本文翻译自Redis的一篇官方文档:http://redis.io/topics/data-types-intro
方便感兴趣的朋友,快速介绍Redis的数据类型。