Redis系统学习 三、使用数据结构

时间:2021-12-13 11:33:26
前言:上一章,简单介绍了5种数据结构,并给出了一些用例。现在是时候来看看一些高级的,但依然很常见的主题和设计模式
一、大O表示法(Big O Notation )
常用时间复杂度O(1)被认为是最快速的,无论我们是在处理5个元素还是5百万个元素,最终都能得到相同的性能。对于sismember命令,其作用是告诉我们一个值是否属于一个集合,时间复杂度为O(1)。sismember命令很强大,强大的一部分原因是其高效的性能特征。许多Redis命令都具有O(1)的时间复杂度
对数的时间复杂度 O(log(N)) 被认为是第二快速的,其通过使需三秒的区间不断皱缩来快速完成处理。使用这种“分而治之”的方式,大量的元素能在几个迭代过程里被快速分解完整。zadd 命令的时间复杂度就是O(log(N)),其中N是在分类集合中的元素数量
 
再下来就是线性时间复杂度 O(N),在一个表格的非索引列里进行查找就需要O(N)次操作。ltrim命令具有O(N)的时间复杂度,但是,在ltrim命令里,N不是列表所拥有的元素数量,而是被删除的元素数量。从一个具有百万元素的列表里用ltrim命令删除一个元素,要比从一个具有一千个元素列表用ltrim命令删除10个元素来的快速,
根据给定的最小和最大值的标记,zremrangebyscore 命令 会在一个分类集合里进行删除元素操作,其时间复杂度是O(log(N)+M)。这看起来似乎有点儿杂乱,通过阅读文档可以知道,这里的N指的是分类集合里的总元素数量,而M则是被删除的元素数量,可以看出,对于性能而言,被删除的元素数量很可能会比分类即合理的总元素数量更为重要。 (zremrangebyscore 命令的具体构成是 zremrangebyscore key max mix)

对于sort命令,其时间复杂度为O(N+M*log(M)),我们将会在下一章谈论更多的相关细节。从sort命令的性能特征来看,可以说这是Redis里最复杂的一个命令。

还存在其他的时间复杂度描述,包括O(N^2)和O(C^N)。随着N的增大,其性能将急速下降。在Redis里,没有任何一个命令具有这些类型的时间复杂度。

值得指出的一点是,在Redis里,当我们发现一些操作具有O(N)的时间复杂度时,我们可能可以找到更为好的方法去处理。

(译注:对于Big O Notation,相信大家都非常的熟悉,虽然原文仅仅是对该表示法进行简单的介绍,但限于个人的算法知识和文笔水平实在有限,此小节的翻译让我头痛颇久,最终成果也确实难以让人满意,望见谅。)

二、仿多关键字查询

时常,你会想通过不同的关键字去查询相同的值,例如 通过用户ID 获取用户信息,也希望通过用户名获取用户信息,有一种很不实效的解决方法,就是将用户对象分别放置到两个字符串值里去  set users:leto xxxx ; set users:9001 xxx
可以实现功能,但是内存会产生两倍的数量,并且今后维护管理也是个噩梦
Redis 其实已经提供了解决的方法:散列
使用散列数据结构,我们可以摆脱重复的缠绕:
set users:9001 "{id: 9001, email: leto@dune.gov, ...}"
hset users:lookup:email leto@dune.gov 9001
简单讲,就是通过散列,模拟搜索功能,进行关联
id = redis.hget('users:lookup:email', 'leto@dune.gov')  先通过散列 搜索出 ID
user = redis.get("users:{id}")  再通过ID获取数
三、引用和索引

我们已经看过几个关于值引用的用例,包括介绍列表数据结构时的用例,以及在上面使用散列数据结构来使查询更灵活一些。进行归纳后会发现,对于那些值与值间的索引和引用,我们都必须手动的去管理。诚实来讲,这确实会让人有点沮丧,尤其是当你想到那些引用相关的操作,如管理、更新和删除等,都必须手动的进行时。在Redis里,这个问题还没有很好的解决方法。

我们已经看到,集合数据结构很常被用来实现这类索引:

sadd friends:leto ghanima paul chani jessica

这个集合里的每个成员都是一个Redis字符串数据结构的引用,而每一个引用的值则包含着用户对象的具体信息。那么如果chani改变了她的名字,或者删除了她的账号,应该如何处理?从整个朋友圈关系结构来看可能会更好理解,我们知道,chni也有她的朋友

sadd friends_of:chani leto paul
如果你有什么待处理情况像上面那样,那在维护成本之外,还会有对于额外索引值的处理和存储空间的成本。这可能会令你感到有点退缩。在下一小节里,我们会谈论减少使用额外数据交互的性能成本的一些方法。
四、数据交互和流水线(Round Trips and Pipelining)
许多命令能接受一个或更多的参数,也有一种关联命令可以接受更多个参数。例如早前我们看到过mget命令接受多个关键字,然后返回值:
sadd friends:chani pater luch hni
Redsi还支持流水线功能。通常情况下,当一个客户端发送请求到Redis后,在发送下一个请求之前必须等待Redis的答复,使用流水线功能,你可以发送多个请求,而不需要等待Redis响应。不但减少了网络开销,还能获得性能上的显著提高。
值得一提的是,Redis会使用存储器去排列命令,因此批量执行命令是一个好主意
五、事务
每一个Redis命令都具有原子性,包括那些一次处理多项事情的命令。此外,对于使用多个命令,Redis支持事务功能。

你可能不知道,但Redis实际上是单线程运行的,这就是为什么每一个Redis命令都能够保证具有原子性。当一个命令在执行时,没有其他命令会运行(我们会在往后的章节里简略谈论一下Scaling)。在你考虑到一些命令去做多项事情时,这会特别的有用。例如:

incr命令实际上就是一个get命令然后紧随一个set命令。

getset命令设置一个新的值然后返回原始值。

setnx命令首先测试关键字是否存在,只有当关键字不存在时才设置值

虽然这些都很有用,但在实际开发时,往往会需要运行具有原子性的一组命令。若要这样做,首先要执行multi命令,紧随其后的是所有你想要执行的命令(作为事务的一部分),最后执行exec命令去实际执行命令,或者使用discard命令放弃执行命令。Redis的事务功能保证了什么?

· 事务中的命令将会按顺序地被执行
· 事务中的命令将会如单个原子操作般被执行(没有其它的客户端命令会在中途被执行)
· 事务中的命令要么全部被执行,要么不会执行
最后,Redis能让你指定一个关键字(或多个关键字),当关键字有改变时,可以查看或者有条件地应用一个事务。这是用于当你需要获取值,且待运行的命令基于那些值时,所有都在一个事务里。对于上面展示的代码,我们不能去实现自己的incr命令,因为一旦exec命令被调用,他们会全部被执行在一块。我们不能这么做:
redis.multi()
current = redis.get('powerlevel')
redis.set('powerlevel', current + 1)
redis.exec()

(译注:虽然Redis是单线程运行的,但是我们可以同时运行多个Redis客户端进程,常见的并发问题还是会出现。像上面的代码,在get运行之后,set运行之前,powerlevel的值可能会被另一个Redis客户端给改变,从而造成错误。)

所以需要指定观察这个powerlevel关键字 如果发生变化,就事务直接失败回滚。
redis.watch('powerlevel')
current = redis.get('powerlevel')
redis.multi()
redis.set('powerlevel', current + 1)
redis.exec()

在我们调用watch后,如果另一个客户端改变了powerlevel的值,我们的事务将会运行失败。如果没有客户端改变powerlevel的值,那么事务会继续工作。我们可以在一个循环里运行这些代码,直到其能正常工作。 很实用

六、关键字反模式
在下一章中,我们将会讨论那些没有确切关联到数据结构的命令,其中的一些是管理或联调工具。然而有一个命令在调试或者追踪BUG的时候非常有用:keys 。这个命令需要一个模式,然后查找所有匹配的关键字,但是因为它是通过线性扫描所有的关键字来进行匹配。所以不能使用在产品代码里,太慢了,消耗也较大
查BUG,比如想查账户号 1233开头的账户  keys bug:1233*  (*号是通配符)
 
小结:
结合这一章以及前一章,希望能让你得到一些洞察力,了解如何使用Redis支持(Poswer) 实际项目。还有其他的模式可以让你去构建各种类型的东西,但真正的关键是要理解基本的数据结构。你将能领悟到,这些数据结构是如何能够实现你最初的视角之外的东西