Redis 数据类型和简单命令实用介绍

       Redis并不是一个纯粹的key-value存储,而是一台支持多种values类型的数据结构服务器。也就是说,传统的key-value存储只能将string类型的key和string类型的value关联起来,而Redis的value不局限在string类型,而是支持更多复杂的数据类型。这些复杂的数据结构包括:

      1、二进制安全的string;

      2、Lists:根据插入顺序排序的string元素的集合。就像基本的linked List;

      3、Sets:不重复的、无序的string元素的集合;

      4、Sorted sets:与Sets类似,但是它的每个string元素与跟一个浮点数关联起来,称之为score。score是Sorted sets排序的依据。因此,不同于Sets的是,Sorted sets可以检索一定范围内的string元素,如top10、bottom10问题等。

      5、Hashs,将一个域field映射到一个value中,所有的filed和value均是string类型的,key标注了在Redis中存储。这在在Python和Ruby中的Hash类似,Java中就是一个Object,如用一个json串表示就是:

{

      id:"user:001",

      {

         name:"filed:name",

         age:19

     }

}

       6、Bit arrays(或者说是bitmaps):它可以用特殊的命令像处理bits的数组一样处理String。这就是说,你可以set或clear个别位、count所有值为1的位的数量、找到第一个或者最后一个set或unset位,等等。

       7、HyperLogLogs:这是一种基于概率统计的数据结构,可以用来评估一个set的势(或者叫做基数,在有限集里面描述的是集合中不重复元素的个数,是一种抽象概念,无限集里面比较复杂,下文会简单介绍,更深入的介绍请参考我的另外一篇文章《就论HyperLogLogs》)。

       进一步深入学习这些数据类型如何工作以及可以用来解决什么问题,可以参考命令引用。所以本文只是一个较为概括性的关于数据类型和常见的使用场景的介绍。

       本文所涉及的例子都使用redis-cli这个命令行工具实现。关于redis-cli的使用和详细介绍,请参考我的另外一篇文章《Redis命令行接口—redis-cli》。redis-cli是官方自带的命令行工具,可以与Redis服务器通信并操控之。

       以下是详细介绍:

       Redis Keys:Redis Keys是二进制安全的,这就意味着可以使用任何的二进制序列都可以表示一个key,从一个“foo”的字符串到一个JPEG的图片内容,都可以,即使是空字符串""也是有效的(但是最好别这么做)。

       关于Redis的Keys有如下几条规定:

       1)keys不宜过长。如1024字节的key是非常糟糕的,不仅仅因为会占用更多的内存,还因为在数据库中查询这样一个key会花费更多的更多的时间进行key的比较。即使你的任务只是检测它大的value的存在性,对它的key使用SHA1等方法hashing也是一个更好的办法。尤其是存内存和带宽的角度来看。

       2)keys也不宜过短。如用"user:1000:followers"代替"u1000flw"显然更好一些。这是因为前者更易理解并且相比于key本身和value所占用的空间,这样的改变增加的空间占用显得微不足道。所以,在实际设计的时候,我们需要在易读性和空间占用中间找到一个平衡点。

      3)坚持使用一种命名模式:如"Object-type:id"就是一个好的方法,具体来说"user:001"。冒号或者破折号常用来连接多个单词组成的filed,再如:"comment:123:reply-to"。

      4)Redis中key最大允许是512MB。

      Redis Strings

      Redis 中string 类型是最简单的value的数据类型。在传统的key-value存储中,如Memcached中是唯一的value类型。所以新的key-value存储的选择总是会优先选择Redis。

      因为Redis的key是strings。我们选用string作为value的时候,仅仅是将两个string关联系来了。如缓存一个html文件。

     如:

127.0.0.1:6379> set mykey some:value
OK
127.0.0.1:6379> get mykey
"some:value"
     如你所见,使用set和get命令是可以设置和检索一个string value。使用set时需要注意的是,如果key已经存在,则用新的value替换原来的value。即使value是空字符串。

     Value可以是任何类型的strings。如一个JPEG图像,但是其大小不能超过512MB。

     Set命令有几个有趣的选项。如,我可以设置如果keys存在令set命令执行失败或者只有key存在才执行成功。例子如下:

127.0.0.1:6379> set mykey new:value nx
(nil)
127.0.0.1:6379> set mykey new:value xx
OK
127.0.0.1:6379> get mykey
"new:value"

        在Redis中,incr 和incrby是有趣的原子操作,如下:

127.0.0.1:6379> set counter 100
OK
127.0.0.1:6379> incr counter
(integer) 101
127.0.0.1:6379> incrby counter 100
(integer) 201
127.0.0.1:6379> 

         其中,incr将counter转换成Integer类型并且递增1,而incrby则会指定增大的大小数值。需要注意的是,counter必须是可以转换成整数的,否则将会报错类似的命令还有decr 、decrby等。

        getset命令会查找指定key的value,并将其值设置为新值,若是key不存在,则会创建新的key并将设置其value。

127.0.0.1:6379> get counter
"gs"
127.0.0.1:6379> getset id 100
(nil)
127.0.0.1:6379> get id
"100"

        mset在给key赋值的时候会非常方便,mget则在检索的时候非常有用,它将返回一个value的数组。

127.0.0.1:6379> mset a 10 b 20 c 30
OK
127.0.0.1:6379> mget a b c 
1) "10"
2) "20"
3) "30"
127.0.0.1:6379> mget a d 
1) "10"
2) (nil)

         key空间的变更和查询。

127.0.0.1:6379> set mykey hello
OK
127.0.0.1:6379> exists mykey
(integer) 1
127.0.0.1:6379> del mykey
(integer) 1
127.0.0.1:6379> exists mykeys
(integer) 0

        如你所见,exists查询数据库中是否存在key,返回数量(interger不是Redis内置的数据类型,此处返回值是redis-cli做的转换);del则是删除指定的key。key space(key空间)相关的命令有许多,但是以上两个和type始最重要的三个。

127.0.0.1:6379> set mykey x
OK
127.0.0.1:6379> type mykey
string
127.0.0.1:6379> del mykey
(integer) 1
127.0.0.1:6379> type mykey
none

        Redis Expire: keys的存活时间限制。

        在继续学习其他的数据类型之前,我们需要讨论一下另外一个重要的特性与value的数据类型无关的特性。也就是Redis Expires。最简单的你可以设置一个过期时间,这个时间是这个key存在于数据库中的时间。随着过期时间的流逝,key将会自动的从数据库中删除而无需用户参与,正如用户使用del命令删除了一样。

       关于Redis expires有如下2个简洁的说明:

       1)可以用秒(seconds)或者毫秒(milliseconds)作为单位。

       2)关于expire的设置信息被复制并持久化到磁盘上,所以即使Redis服务器重启,该设置依然有效。

127.0.0.1:6379> expire key 10
(integer) 1
127.0.0.1:6379> get key (10s之前)
"some-value"
127.0.0.1:6379> get key (10s后)
(nil)

       可以在set命令中设置Redis Expires,ttl命令测试剩余存活时间,persist命令持久化key:

127.0.0.1:6379> set key 100 ex 100
OK
127.0.0.1:6379> ttl key
(integer) 93
127.0.0.1:6379> persist key
(integer) 1
127.0.0.1:6379> ttl key
(integer) -1

        当单位是毫秒时,对应的以上命令分别是:pexpire、pttl:

127.0.0.1:6379> set key 100000
OK
127.0.0.1:6379> pexpire key 100000
(integer) 1
127.0.0.1:6379> ttl key
(integer) 96
127.0.0.1:6379> pttl key
(integer) 92818

        Redis Lists:

        为了更好的解释List数据类型,有必要讲一点理论知识。因为List经常被IT人士错误的使用。例如Python里边的list不是List,而是数组。

        通常来讲,Lists是有序(保持插入有序)的元素的集合,如10,20,1,2,3。但是数组实现的Lists和链表实现的List是大不相同的。这就是说,即使一个list中有成千上万个元素,在list的头部和尾部插入一个新的元素可以在常数时间内完成。所以说,理论上讲,Java中的ArryList并不能称之为List,而LinkedList才是List。命令lpush将一个新元素插入到只有十个元素的List中和将其插入到一个10亿个元素的List所需的时间是一样的。

         然而,List的缺点显而易见:根据索引访问一个List中的元素,对于基于数组实现的List而言更快一些。它所需要的时间成本与List中的元素数量成正比。

        Redis中的数组是基于链表的,因为对于一个数据库而言,最重要的是能在更快地内将新的元素插入到非常大的List中。而索引的访问速度而言,反而显得是次要的了。在接下来的瞬间,我们将会看到这一点。当快速访问重要的时候,我们可以有另外的一种数据结构选择,就是稍后介绍的sorted sets。

       Redis Lists的第一步使用

       lpush将一个新的元素添加到List的头部或者左边,rpush则添加到右侧或者尾部,lrange用来提取lists中指定范围的元素,如下:

127.0.0.1:6379> rpush mylist a b c
(integer) 3
127.0.0.1:6379> lpush 3 2 1
(integer) 2
127.0.0.1:6379> del 3
(integer) 1
127.0.0.1:6379> lpush mylist 3 2 1
(integer) 6
127.0.0.1:6379> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
4) "a"
5) "b"
6) "c"
        需要注意的是,lrange需要两个索引参数,需要返回的范围中的第一个元素的索引和最后一个元素的索引。索引参数可以是负数,如果是,则Redis会从List的尾部进行计数,如-1表示最后一个元素,-2表示倒数第2个元素等等。

       上面也展示了 lpush和rpush均是可变参数的命令,也就是说,当需要向一个List中添加多个元素时,不需要敲多个命令,而只需要一次即可。

       pop是定义在Lists上的重要的操作,它可以将数组中的元素从数组中删除并返回该元素。lpop是对Lists的头部或者左侧部分进行的操作,而rpop则是对Lists的尾部或者右侧元素的操作:

127.0.0.1:6379> lpop mylist
"1"
127.0.0.1:6379> rpop mylist
"c"
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "3"
3) "a"
4) "b"
       如果要在一个空的List中执行pop操作,Redis则会返回一个null。
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> rpop mylist
(nil)

        Lists的常见用例

        对很多任务而言,Lists是非常有用的,如下是两个最具代表性的:

       1)社交网络中用户发出的最新的更新。

       2)进程之间的通信中,使用生产者-消费者模式:生产者对产品List执行push,消费者执行pop得到产品并执行后续的操作。Redis有特殊的命令完成高效可靠地这个任务(psubscribe)。

       举个例子,Ruby类库(软件包)resque和sidekiq在其的引擎中均采用Redis的Lists完成后台作业。

       最著名的Twitter社交网络采用Redis Lists保存用户的最新推文。

       为了一步步地描述通用的用例,想象你的主页上展示了一个图片共享社交网络中最新的图片并且你非常想快速得到它:

       1)每当用户发表一个新的图片时,我们将它的ID使用lpush添加到Redis List中;

       2)每当用户访问他的主页时,我们只需要执行命令lrange list-name 0 9就可以得到该用户的最新十幅图像的ID,借此可以拿到其对应的10幅图片。

       Capped lists(限制列表):

       在许多情况下,我们仅仅需要使用list存储最新的条目:社交网络的更新、日志、其他任何事物。

       Redis允许我们将lists用作限制列表,仅仅保留它的最新的N个元素而使用ltrim 命令丢弃它的其余元素。

       ltrim命令跟lrange有点像,但不同的是,lrange只是显示限制范围内的元素,而ltrim则用限制范围内的元素替代原来的Lists的values,范围之外的元素均会被移除。

127.0.0.1:6379> rpush mylist 1 2 3 4 5
(integer) 5
127.0.0.1:6379> ltrim mylist 0 2
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"

        以上的ltrim命令高速Redis截取mylist中索引为0-2的元素,其余的均被丢弃。这非常简单但是提供给了我们一个通用的限制列表的实现模式那就是一个push操作和一个ltrim操作结合实现稳定数量的元素列表:

       lpush list-name <some-items>

       ltrim  list-name  <start-index>  <end-index>

       可以使用lrange访问lists的元素,但是需要注意:lrange的时间复杂度是O(n),它的时间消耗与元素数量的正比,但是朝head或者tail访问可以在你常数时间完成。

       Lists上的阻塞操作

       Lists因其特殊的属性,使得非常适合实现队列,通常而言,可以构建阻塞队列。

       假设你想通过一个进程将某种元素push到List中,而用另外的进程获取它实现别的功能。这就是典型的生产者-消费者模型,可以使用如下的步骤实现它:

      1)生产者调用lpush命令,将元素push进list。

      2)消费者调用rpop命令,从list中提取元素。

      或者也可以使用rpush和lpop实现之。

      然而一定的时候,list一定是空的,所以pop操作将会返回一个null,这将导致消费者等待一定时间并且反复重试。这个过程叫polling(轮询),这不是一个好的方式,因为以下的缺点:

      1)强迫Redis或者客户端处理无用的命令(list为空时,一直返回null,这并没有用)。

      2)push延迟:一旦Redis返回一个null,总是会等待一定的时间,即使list已经有了元素。而这种延迟是可以缩小的。

      鉴于此,Redis实现了pop的阻塞操作,brpop和blpop,如果lists为空,Redis将会阻塞:直到lists中有了新的元素或者用户指定的延时达到:

127.0.0.1:6379> brpop tasks 5
(nil)
(5.01s)
127.0.0.1:6379> lpush tasks b a
(integer) 2
127.0.0.1:6379> brpop tasks 5
1) "tasks"
2) "b"
127.0.0.1:6379> brpop tasks 5
1) "tasks"
2) "a"
127.0.0.1:6379> lrange tasks 0 -1
(empty list or set)

        这意味着,如果tasks中没有元素,等待5秒钟,如果时间到了还没有则返回null。特别地,当延时设置为0时,意味着一直等待直到list中有了一个元素。

       关于brpop的几点说明:

       1)阻塞操作是以一种有序的方式进行的:若同时有多个客户端调用brpop请求一个空的list时,第一个被添加的元素将会返回给第一个阻塞的客户端。

       2)brpop和rpop的返回值不一样:brpop返回一个两个元素组成的数组,其中第一个是list的key,这是因为brpop和blpop可以同时阻塞等待多个lists。

       另外的两个有用的命令是:rpoplpush和brpoplpush:

127.0.0.1:6379> lrange tasks 0 -1
1) "a"
2) "b"
127.0.0.1:6379> lrange new-tasks 0 -1
(empty list or set)
127.0.0.1:6379> rpoplpush tasks new-tasks
"b"
127.0.0.1:6379> rpoplpush tasks new-tasks
"a"
127.0.0.1:6379> lrange tasks 0 -1
(empty list or set)
127.0.0.1:6379> lrange new-tasks 0 -1
1) "a"
2) "b"

       这给我们一个启示,可以用rpoplpush实现一个循环队列:

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> rpush tasks a b c
(integer) 3
127.0.0.1:6379> lrange tasks 0 -1
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> rpoplpush tasks tasks
"c"
127.0.0.1:6379> lrange tasks 0 -1
1) "c"
2) "a"
3) "b"

        brpoplpush是rpoplpush的阻塞实现。

        keys的自动创建特性和删除:

        截止到目前为止,我们在push之前并未创建一个空的list或者list为空的时候删除list,因为这是Redis自动的行为。这不仅对lists数据类型有效,也同样适用于sets、sorted sets、hashes等。

        基本上我们可以总结出来三条规则:

        1)当我添加元素到一个不存在的聚合数据类型时,Redis首先会创建一个空的集合然后将其添加到里面。

        2)一旦聚合数据类型为空,Redis将自动删除之。

        3)调用一个只读命令如llen和调用一个写命令如lpop操作一个空的聚合数据类型其产生的结果是一样的,该聚合数据类型仍然保持为空。

127.0.0.1:6379> del mylist
(integer) 1
127.0.0.1:6379> lpush mylist 3 2 1
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> rpop mylist
"3"
127.0.0.1:6379> lpop mylist
"1"
127.0.0.1:6379> lpop mylist
"2"
127.0.0.1:6379> exists mylist
(integer) 0
127.0.0.1:6379> del mylist
(integer) 0
127.0.0.1:6379> llen mylist
(integer) 0
127.0.0.1:6379> lpop mylist
(nil)

        但是,同一个非空的key,只能同时保持一种数据类型:

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> lpush foo 3 2 1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> type foo
string

        可以看到,foo是一个string类型的,如用lpush操作,则会报类型错误的错。但是非常奇怪的是,可以用set命令操纵集合如list,并且该操作更改了value的数据类型为string,此问题无官方说明,但可以猜想是因为聚合数据类型都是string的聚合导致的吧:

127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> lpush mylist 3 2
(integer) 2
127.0.0.1:6379> type mylist
list
127.0.0.1:6379> set mylist string-type
OK
127.0.0.1:6379> type mylist
string

        Redis Hashes

        Redis hash正如其名一样,是由filed-value对组成的:

127.0.0.1:6379> hmset user:1000 username antirez birthday 1997 verify 1
OK
127.0.0.1:6379> hget user:1000 username
"antirez"
127.0.0.1:6379> hmget user:1000 username birthday
1) "antirez"
2) "1997"
127.0.0.1:6379> hgetall user:1000
1) "username"
2) "antirez"
3) "birthday"
4) "1997"
5) "verify"
6) "1"

        Redis 中的hashes表示对象非常方便,并且其filed-value对可以无限制,当然实在不考虑内存的情况下。所以在实际中,hashes是用的非常多的,尤其是在面向对象编程技术替代面向过程成为编程主流之后。

       正如上述例子所展示的,hmset命令用来设置多个的filed-value对,hmget命令则是用来检索多个filed对应的value值。当然在实际中,可能这样检索一个对象很麻烦,可以使用hgetall检索hashes中所有的filed-value对。hset和hget命令针对单个filed-value对的操作:

127.0.0.1:6379> hlen user:1000
(integer) 3
127.0.0.1:6379> hset user:1000 married no
(integer) 1
127.0.0.1:6379> hget user:1000 married
"no"

        更多的关于hashes的命令参考官方手册,或者在redis-cli中使用help @hash或者直接使用命令command查看所有的支持命令。

        值得注意的是,用特殊方式编码的hash在内存中是非常搞笑的。

        Redis Sets

        Redis sets是无序的strings集合。sadd命令可以将一个新的元素添加到sets中。sets也支持很多的其他操作,如测试一个sets是否存在指定的元素、在不同的set中执行交、并、差集操作等等。

        

127.0.0.1:6379> sadd myset 1 2 3
(integer) 3
127.0.0.1:6379> sismember myset 1
(integer) 1
127.0.0.1:6379> sismember myset 20
(integer) 0
127.0.0.1:6379> sadd otherset 1 a b
(integer) 3
127.0.0.1:6379> sinter myset otherset
1) "1"
127.0.0.1:6379> sunion myset otherset
1) "2"
2) "1"
3) "b"
4) "3"
5) "a"
127.0.0.1:6379> sdiff myset otherset
1) "2"
2) "3"
127.0.0.1:6379> sdiff otherset myset
1) "b"
2) "a"

       spop命令是非常重要的命令,它可以从中随机提取出一个元素。举个例子,为了实现一个基于web的扑克牌游戏,可以用set构造整副牌。简单起见我们使用首字母表示四种花色的牌:C(lubs)、D(iamonds)、H(earts)、S(pades):

127.0.0.1:6379> sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3 H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 SJ SQ SK
(integer) 52

        现在我们想给每个玩家随机分配五张牌,spop 是这一操作的完美选择。但是如果我们直接从整副牌中取,下次玩的时候又需要再取一次,所以我们考虑调用spop从deck中取5张牌制作一个副本:game:1:deck,这个操作已经有了成熟的实现:

127.0.0.1:6379> sunionstore game:1:deck deck
(integer) 52

        副本制作完成,接下来给第一个玩家分配5张牌:

127.0.0.1:6379> spop game:1:deck
"C10"
127.0.0.1:6379> spop game:1:deck
"H4"
127.0.0.1:6379> spop game:1:deck
"DK"
127.0.0.1:6379> spop game:1:deck
"S2"
127.0.0.1:6379> spop game:1:deck
"CK"

        按照现在的知识,我们应该使用smembers game:1:deck来得到剩余牌的数量。但是这样显得大材小用了,所以我们找到了一个更好的命令scard来计算set中的元素数量(在关于set的理论中,通常叫做基数或者势,即cardinality):

127.0.0.1:6379> scard game:1:deck
(integer) 47

        当我们只需要得到有个随机元素而不需要在sets中移除它的时候,srandmember是一个很适合的命令。

127.0.0.1:6379> srandmember game:1:deck
"CJ"
127.0.0.1:6379> scard game:1:deck
(integer) 47

         Redis Sorted sets

        Sorted sets在Redis中很像hashes和sets的混合物:跟sets 一样是不重复的、无序的元素;跟hashes一样,Sorted sets中的每个元素都与一个浮点数相关联,叫做score,这点跟hashes是类似的。如此一来,Sorted sets中的元素可以按照元素对应的score保持有序,此时的排序遵循如下规则:

        1)如果A和B的score值不相等,那么若A.score>B.score,则A>B。

        2)如果A和B的score值严格相等,则A和B按照字典顺序排序。

        接下来我们举个简单的小例子,选用一些黑客的名字添加到hackers中,并且以他们的出生日期作为score排序:

127.0.0.1:6379> zadd hackers 1940 "Alan Kay"
(integer) 1
127.0.0.1:6379> zadd hackers 1957 "Sophie Wilson"
(integer) 1
127.0.0.1:6379> zadd hackers 1953 "Richard Stallman"
(integer) 1
127.0.0.1:6379> zadd hackers 1949 "Anita Borg"
(integer) 1
127.0.0.1:6379> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
127.0.0.1:6379> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
127.0.0.1:6379> zadd hackers 1916 "Claude Shannon"
(integer) 1
127.0.0.1:6379> zadd hackers 1969 "Linus Torvalds"
(integer) 1
127.0.0.1:6379> zadd hackers 1912 "Alan Turing"
(integer) 1

        或者可以直接使用zadd hackers 1912  "Alan Turing"  1916  "Linus Torvalds"...一次性天安家多个score-value对。计算Sorted sets中的元素个数使用类似于lrange的命令zrange:

127.0.0.1:6379> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"

        该返回结果已经在添加的时候按照score排过序了。

        因为Sorted lists的实现基于list和哈希表的特性,zadd命令添加新的元素的时间复杂度是O(logN)。

        当我们需要按照降序访问元素时,用zrevrange代替zrange就可以了:

127.0.0.1:6379> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"

      通过比较,排序已经发生了翻转。同样滴,我们可以得到score带score的结果,只需要添加withscores参数即可:

127.0.0.1:6379> zrange hackers 0 -1 withscores
 1) "Alan Turing"
 2) "1912"
 3) "Hedy Lamarr"
 4) "1914"
 5) "Claude Shannon"
 6) "1916"
 7) "Alan Kay"
 8) "1940"
 9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"

       Sorted sets上的范围操作

       Sorted sets的威力不仅如此,我们可以对其进行范围上的操作,如使用zrangebyscore检索hackers中出生于1950年及其以下的hackers:

127.0.0.1:6379> zrangebyscore hackers -inf 1950 withscores
 1) "Alan Turing"
 2) "1912"
 3) "Hedy Lamarr"
 4) "1914"
 5) "Claude Shannon"
 6) "1916"
 7) "Alan Kay"
 8) "1940"
 9) "Anita Borg"
10) "1949"

        zrevrangebyscore命令也同样可以使用,其中inf代表infinity。

        我们同样可以使用zremrangebyscore移除指定score范围内的元素:

127.0.0.1:6379> zremrangebyscore hackers 1950 1960
(integer) 2

        另外一个很棒的命令是zrank,他可以获取指定元素在sorted set中的排名,通俗地讲就是第N小;同样地,zrevrank得到的是降序排名,通俗的将就是第N大:

127.0.0.1:6379> zrank hackers "Anita Borg"
(integer) 4
127.0.0.1:6379> zrevrank hackers "Anita Borg"
(integer) 2

       score字典排序

       前面讲过,当我们的sorted sets中的元素的score比较结果是一致的时,按照以往的规则,他们实际上是无需的。这个问题在2.8版本上提供了系列的命令解决这个问题:zrangebylex、zrevrangebylex、zremrangebylex以及zlexcount:

127.0.0.1:6379> flushall
485:M 26 Mar 23:38:38.786 * DB saved on disk
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> zadd lexset 0 a 0 bb 0 b 0  cc
(integer) 4
127.0.0.1:6379> zrevrangebylex lexset [c [b
1) "bb"
2) "b"
127.0.0.1:6379> zrangebylex lexset [b [c
1) "b"
2) "bb"
127.0.0.1:6379> zlexcount lexset [b [c
(integer) 2

        更新score:积分榜

        sorted sets最后需要注意的是:在任何时候都可以调用zadd更新一个已存在的元素的score,该操作的时间复杂度是O(logN)。正因如此,sorted sets非常适用于有大量元素更新的场景。

       一个常见的用例是积分排名,如Facebook使用get-rank操作计算用户在游戏中的排名。

      关于更多的命令请在redis-cli中调用help @sorted_set获取。

       Bitmaps

       bitmaps并不是真是的数据类型,而是定义在string类型上的面向位的操作。因为strings是二进制安全的大对象,其最大可达到512MB,所以可以表示2^32中不同的位组合。

       位操作鲁豫区分成两类:常数时间的单一操作,如设置某位为0或者1、获取它们的value;一组位上的操作,如在给定范围的位中计算数量。

      bitmaps 最大的优势是在保存信息时能最大程度的节省空间。例如,在一个系统中用两个不同的id表示40亿用户中的两位最大也只需要512MB的空间。

      位操作符常见的有setbit和getbit。

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set str ab
OK
127.0.0.1:6379> setbit str 2 0
(integer) 1
127.0.0.1:6379> get str
"Ab"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> setbit str 10 1
(integer) 0
127.0.0.1:6379> getbit str 10
(integer) 1
127.0.0.1:6379> getbit str 11
(integer) 0

        如果strings的位数不够setbit要操作的位数,这个命令会自动扩充当前strings的长度,getbit则返回0。如下是help setbit的说明。

127.0.0.1:6379> help setbit

  SETBIT key offset value
  summary: Sets or clears the bit at offset in the string value stored at key
  since: 2.2.0
  group: string

127.0.0.1:6379> help getbit

  GETBIT key offset
  summary: Returns the bit value at offset in the string value stored at key
  since: 2.2.0
  group: string

       由此可以看出,bitmaps实际上是strings的操作。但是由于其特殊性所以单独拿出来讨论。

       bitop、bitcount、bitpos是重要的关于bitmaps的命令,bitop是位操作符,支持and、or、xor、not运算:

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set a 100
OK
127.0.0.1:6379> set b 110
OK
127.0.0.1:6379> bitop and a b
(integer) 3
127.0.0.1:6379> get a
"110"
127.0.0.1:6379> get b
"110"

        bitop被用来进行strings的按位运算,bitcount则用来统计统计用bit数组表示strings后其中的1的数量,也叫做势或基数。

127.0.0.1:6379> help bitcount

  BITCOUNT key [start end]
  summary: Count set bits in a string
  since: 2.6.0
  group: string

        start表示bitmaps中统计起始的索引位置,end不用多说,返回值是bitmaps中1的数量。

127.0.0.1:6379> help bitpos

  BITPOS key bit [start] [end]
  summary: Find first bit set or clear in a string
  since: 2.8.7
  group: string

        bitpos则在key中找到第一个被设置为bit(0或者1)的索引位置,start和end如上所述,两者省略时表示在整个bitmaps中寻找:

127.0.0.1:6379> setbit key 0 1
(integer) 0
127.0.0.1:6379> setbit key 100 1
(integer) 0
127.0.0.1:6379> bitcount key
(integer) 2
127.0.0.1:6379> bitcount key 0 10
(integer) 1
127.0.0.1:6379> bitcount key 0 100
(integer) 2
127.0.0.1:6379> bitpos key 1 0 10
(integer) 0
127.0.0.1:6379> bitpos key 1 0 100
(integer) 0
127.0.0.1:6379> bitpos key 1 0 101
(integer) 0
127.0.0.1:6379> bitpos key 1
(integer) 0
127.0.0.1:6379> bitpos key 1 10 100
(integer) 100
127.0.0.1:6379> bitcount key 
(integer) 2
127.0.0.1:6379> bitcount key 10 100
(integer) 1
127.0.0.1:6379> bitpos key 0 
(integer) 1

        bitmaps的通常的使用场景是:

       1)全种类的实时分析

       2)高效存储对象关联性逻辑信息

       假设你想知道你的网站上每天访问时间最长的用户记录。在你将网站放在公网上的同时开始从0计数,每次客户访问时用setbit命令记录,可以简单地将当前时间的绝对秒作为索引,这样减去初始索引,然后用3600*24除之,而使用userID和时间结合作为key。

       这样每个用户每天都会有一个相关联的较小的strings保存他的网站访问信息。这样的时候,简单地使用bitcount就可以得到该用户每天的访问次数,通过几次bitpos的调用,就可以计算出最长的访问时间记录。

       bitmaps通常被琐碎的分割成多个keys。如为了共享数据集、避免操作更大的keys。划分策略是:每个key存储M bits,key的名字命名为bit-number/M,N MOD M就是第N个bit在bitmaps中的索引。

       HyperLogLogs

       HyperLogloLogs被用来统计具有唯一属性的事物的量,如Redis中的sets、sorted sets;Java中的set collection等。通常情况下,统计唯一元素的量缩耗费的内存跟集合的大小成比例,因为我们需要记住我们已经统计过得元素避免重复统计。但是有很多的方法可以降低精度换取内存小号的减少,最终可以在一个错误率可以接受的范围内得到一个结果。在Redis中,这个比例少于1%。如果集合元素更少的话,精度会更高,相应的错误率会降低不少。这个魔法效果的秘诀就是不在使用成比例的内存消耗,而是使用固定数量的内存,12kb足够了!

       在Redis中,HHLs从技术实现上将,都会编码成strings。所以可以使用set序列化一个HHL,而使用get反序列化之。

       从概念上讲,HHL API就像是再用Redis Sets在做同样的事情。可以使用sadd将看到的元素添加一个set并使用scard计算sets中的元素数量。

       但是使用HHLs,你不需要真的将元素添加到HHL中,因为这个数据结构并没有将元素保存而是只保存了一个状态。实现逻辑是:

       1)每次看到一个新的元素,使用pfadd命令计算

       2)每次你想知道目前有多少个元素被加进去时,pfcount可以完成这个任务。

127.0.0.1:6379> pfadd hhl1 a b c
(integer) 1
127.0.0.1:6379> pfadd hhl2 1 2 3
(integer) 1
127.0.0.1:6379> pfadd hhl3 A B C
(integer) 1
127.0.0.1:6379> pfcount hhl1
(integer) 3
127.0.0.1:6379> pfmerge hhl-merge hhl1 hhl2 hhl3
OK
127.0.0.1:6379> pfcount hhl-merge
(integer) 9

        Redis支持的三个重要的HHL操作使用如上所示,其中pfmerge是将N个HHLs合并生成一个HHL,是一个UNION操作,非常的好用。完整的使用方法在redis-cli中调用help @hyperloglog。 

       还有一些其他的重要API方法由于篇幅原因不能在此讨论,后期单独整理出来:

       1)增量的迭代key空间_

       2)  Pub-Sub 服务器_

       3) Lua脚本在Redis上的使用_

       总后总结一下,redis-server是Redis的服务器进程,redis-cli是命令行工具,可以操纵Redis Server,command命令可以列举可用的命令列表,help调看具体命令的用法。另外注意的是,Redis中的命令按照数据类型分组实现的,help @group-name可以查看某类数据的用法,目前分组有:string、list、hash、set、sorted_set、hyperloglog共6个组别,bitmaps实质上是一个string,所以归类在string组别。

猜你喜欢

转载自worldly.iteye.com/blog/2365059