NoteDeep
Redis通常被人们认为是一种持久化的存储器关键字-值型存储(in-memory persistent key-value store)。我认为这种对Redis的描述并不太准确。Redis的确是将所有的数据存放于存储器(更多是是按位存储),而且也确实通过将数据写入磁盘来实现持久化,但是Redis的实际意义比单纯的关键字-值型存储要来得深远。纠正脑海里的这种误解观点非常关键,否则你对于Redis之道以及其应用的洞察力就会变得越发狭义。
事实是,Redis引入了5种不同的数据结构,只有一个是典型的关键字-值型结构。理解Redis的关键就在于搞清楚这5种数据结构,其工作的原理都是如何,有什么关联方法以及你能怎样应用这些数据结构去构建模型。首先,让我们来弄明白这些数据结构的实际意义。


对于Redis而言,关键字就是一切,而值是没有任何意义。更通俗来看就是,Redis不允许你通过值来进行查询。

我们之前提及过,Redis是一种持久化的存储器内存储(in-memory persistent store)。对于持久化,默认情况下,Redis会根据已变更的关键字数量来进行判断,然后在磁盘里创建数据库的快照(snapshot)。你可以对此进行设置,如果X个关键字已变更,那么每隔Y秒存储数据库一次。默认情况下,如果1000个或更多的关键字已变更,Redis会每隔60秒存储数据库;而如果9个或更少的关键字已变更,Redis会每隔15分钟存储数据库。

数据结构

字符串(Strings)

在Redis里,字符串是最基本的数据结构。当你在思索着关键字-值对时,你就是在思索着字符串数据结构。不要被名字给搞混了,如之前说过的,你的值可以是任何东西。

散列(Hashes)

我们已经知道把Redis称为一种关键字-值型存储是不太准确的,散列数据结构是一个很好的例证。你会看到,在很多方面里,散列数据结构很像字符串数据结构。两者显著的区别在于,散列数据结构提供了一个额外的间接层:一个域(Field)。因此,散列数据结构中的setget是:
hset users:goku powerlevel 9000 hget users:goku powerlevel
如你所见,散列数据结构比普通的字符串数据结构具有更多的可操作性。我们可以使用一个散列数据结构去获得更精确的描述,是存储一个用户,而不是一个序列化对象。从而得到的好处是能够提取、更新和删除具体的数据片段,而不必去获取或写入整个值。

列表(Lists)

对于一个给定的关键字,列表数据结构让你可以存储和处理一组值。你可以添加一个值到列表里、获取列表的第一个值或最后一个值以及用给定的索引来处理值。列表数据结构维护了值的顺序,提供了基于索引的高效操作。为了跟踪在网站里注册的最新用户,我们可以维护一个newusers的列表:
lpush newusers goku ltrim newusers 0 50


集合(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

分类集合(Sorted Sets)

最后也是最强大的数据结构是分类集合数据结构。如果说散列数据结构类似于字符串数据结构,主要区分是域(field)的概念;那么分类集合数据结构就类似于集合数据结构,主要区分是标记(score)的概念。标记提供了排序(sorting)和秩划分(ranking)的功能。如果我们想要一个秩分类的朋友名单,可以这样做:
zadd friends:duncan 70 ghanima 95 paul 95 chani 75 jessica 1 vladimir

对于分类集合数据结构,最常见的应用案例是用来实现排行榜系统。事实上,对于一些基于整数排序,且能以标记(score)来进行有效操作的东西,使用分类集合数据结构来处理应该都是不错的选择。

仿多关键字查询(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)


我们已经看到,集合数据结构很常被用来实现这类索引:
sadd friends:leto ghanima paul chani jessica
这个集合里的每一个成员都是一个Redis字符串数据结构的引用,而每一个引用的值则包含着用户对象的具体信息。那么如果chani改变了她的名字,或者删除了她的帐号,应该如何处理?从整个朋友圈的关系结构来看可能会更好理解,我们知道,chani也有她的朋友:
sadd friends_of:chani leto paul


pipelined

Redis还支持流水线功能。通常情况下,当一个客户端发送请求到Redis后,在发送下一个请求之前必须等待Redis的答复。使用流水线功能,你可以发送多个请求,而不需要等待Redis响应。这不但减少了网络开销,还能获得性能上的显著提高。

事务(Transactions)

每一个Redis命令都具有原子性,包括那些一次处理多项事情的命令。此外,对于使用多个命令,Redis支持事务功能。
incr命令实际上就是一个get命令然后紧随一个set命令。
getset命令设置一个新的值然后返回原始值。
setnx命令首先测试关键字是否存在,只有当关键字不存在时才设置值
  • 事务中的命令将会按顺序地被执行
  • 事务中的命令将会如单个原子操作般被执行(没有其它的客户端命令会在中途被执行)
  • 事务中的命令要么全部被执行,要么不会执行
multi hincrby groups:1percent balance -9000000000 hincrby groups:99percent balance 9000000000 exec


关键字反模式(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把关键字删除掉。

发布和订阅(Publication and Subscriptions)

Redis的列表数据结构有blpopbrpop命令,能从列表里返回且删除第一个(或最后一个)元素,或者被堵塞,直到有一个元素可供操作。这可以用来实现一个简单的队列。
(译注:对于blpopbrpop命令,如果列表里没有关键字可供操作,连接将被堵塞,直到有另外的Redis客户端使用lpushrpush命令推入关键字为止。)
此外,Redis对于消息发布和频道订阅有着一流的支持。你可以打开第二个redis-cli窗口,去尝试一下这些功能。在第一个窗口里订阅一个频道(我们会称它为warnings):
subscribe warnings
其将会答复你订阅的信息。现在,在另一个窗口,发布一条消息到warnings频道:
publish warnings "it's over 9000!"
如果你回到第一个窗口,你应该已经接收到warnings频道发来的消息。
你可以订阅多个频道(subscribe channel1 channel2 ...),订阅一组基于模式的频道(psubscribe warnings:*),以及使用unsubscribepunsubscribe命令停止监听一个或多个频道,或一个频道模式。
最后,可以注意到publish命令的返回值是1,这指出了接收到消息的客户端数量。

监控和延迟日志(Monitor and Slow Log)

在实际生产环境里,你应该谨慎运行monitor命令,这真的仅仅就是一个很有用的调试和开发工具。除此之外,没有更多要说的了。
随同monitor命令一起,Redis拥有一个slowlog命令,这是一个优秀的性能剖析工具。其会记录执行时间超过一定数量微秒的命令。在下一章节,我们会简略地涉及如何配置Redis,现在你可以按下面的输入配置Redis去记录所有的命令:
config set slowlog-log-slower-than 0


排序(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, ....}"
所有的事情不仅变得更为容易管理,而且我们能通过severitypriority来进行排序,还可以告诉sort命令具体要检索出哪一个域的数据:
sort watch:leto by bug:*->priority get bug:*->details

对于太大的集合,sort命令的执行可能会变得很慢。好消息是,sort命令的输出可以被存储起来:
sort watch:leto by bug:*->priority get bug:*->details store watch_by_priority:leto

配置(Configuration)

当你第一次运行Redis的服务器,它会向你显示一个警告,指redis.conf文件没有被找到。这个文件可以被用来配置Redis的各个方面。一个充分定义(well-documented)的redis.conf文件对各个版本的Redis都有效。范例文件包含了默认的配置选项,因此,对于想要了解设置在干什么,或默认设置是什么,都会很有用。你可以在https://github.com/antirez/redis/raw/2.4.6/redis.conf找到这个文件。


验证(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的复制功能还没有提供自动故障恢复。


评论列表

    字符串(Strings)
    散列(Hashes)
    列表(Lists)
    集合(Sets)
    分类集合(Sorted Sets)
    仿多关键字查询(Pseudo Multi Key Queries)
    引用和索引(References and Indexes)
    pipelined
    事务(Transactions)
    关键字反模式(Keys Anti-Pattern)
    发布和订阅(Publication and Subscriptions)
    监控和延迟日志(Monitor and Slow Log)
    排序(Sort)
    配置(Configuration)
    验证(Authentication)
    大小限制(Size Limitations)
    复制(Replication)