redis支持的数据结构
- Binary-safe strings
- Lists
- Sets
- Sorted sets
- Hashes
- Bit arrays
- HyperLogLogs
redis的key注意事项
- 不建议用太长的key, 不只因为浪费内存和宽带, 而且在数据库查找key时会做一些费时的key的比较工作
- 不建议用太短的key, 为确保key值的可读性
- 对key做一些规范化工作, 比如, 用”object-type: id”这种格式作为redis的key
- key所被允许最大的大小是512MB ( value也是如此 )
redis的strings类型
redis的key是string类型的, 而string也是可分配的最简单的value. 下面演示一个简单的例子:
> set mykey somevalue
OK
> get mykey
"somevalue"
如果需要更新某一个key的value, 并返回旧的值, 可以使用getset命令
> set mykey "Hello"
OK
> getset mykey "World"
"Hello"
> get mykey
"World"
如果需要批量保存key-value, 可以使用mset
命令, 相对应的mget
命令可以获取多个key的值
> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"
正如上面的演示, get和set分别是获取和保存的命令, 而需要注意的是, set命令将覆盖已经存在的key所对应的value.
set命令还可以附带一些有趣的选项参数以达到一些效果. 比如, 当key已经存在时就保存失败, 或者当key已经存在时才保存成功.(mykey已经存在)
> set mykey newvalue nx
(nil)
> set mykey newvalue xx
OK
除此之外, 还有一个原子子增长的命令
> set counter 100
OK
> incr counter
(integer) 101
> incrby counter 50
(integer) 151
与incr
相对应的还有原子性的递减, decr
和decrby
等命令.
修改和查询key空间
有很多命令与key空间相关, 虽然它们不是为特殊类型所定义, 但是很有用.
比如exists
, del
和type
命令, exists命令将返回1或0表示给出的key是否存在, 而del返回1或0表示给出的key是否被删除
> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0
type命令将返回key所对应的value的类型
> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none
redis过期时间
在我们学习复杂的数据结构之前, 我们有必要了解其他功能, 而不管value是什么类型, 它就是redis expires.
如果设置了过期时间, 那么这个key将在时间触发时自动销毁, 就好像对这个key调用了del命令.
关于redis expires的信息:
- 它能够用秒或者毫秒作为保存单位
- 过期时间的处理通常在1个毫秒
- 过期时间的相关信息被复制和保留在硬盘上, 如果redis服务器挂掉, 那么那些设置了过期时间的key也将被销毁.
对于过期时间, set命令还有ex和px选项参数
ex
: 以秒为单位设置过期时间px
: 以毫秒为单位设置过期时间
同时秒和毫秒都有对应的命令
expire
: 修改已有的key过期时间, 以秒为单位pexpire
: 修改已有的key过期时间, 以毫秒为单位ttl
: 返回剩余可存活的时间, 以秒为单位pttl
: 返回剩余可存活的时间, 以毫秒为单位
redis list数据结构
redis list是由linked list实现的. 这就意味着即使你有几百万个元素在list里面, 添加一个新元素到list的头部或者尾部所花费的时间都是固定的. 那么你用lpush
命令将一个新元素添加到已经有十个元素的list的头部所花费的时间, 和添加到已经有一千万个元素的list的头部所花费的时间是一样的.
如果array来实现list, 那么通过index来访问list就会很快. 而linked list实现的list则访问速度没那么快. 这就是redis list的弊端.
而redis list之所以没有这么做, 是因为它觉得能够以快速时间添加一个元素到一个很长的列表是很重要的事情. 另一个重要的因素是, redis list能够以常数时间获取常数长度.
redis list第一步
lpush
: 添加一个新元素到list的左边(头部)rpush
: 添加一个新元素到list的右边(尾部)lrange
: 从list获取一定范围的元素
rpush mylist A
(integer) 1
rpush mylist B
(integer) 2
lpush mylist first
(integer) 3
lrange mylist 0 -1(-1为倒数第一个, -2为倒数第二个, 以此类推)
1) “first”
2) “A”
3) “B”
上面的命令都是可变长度命令, 意味这你能够单个命令添加多个元素
> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "foo bar"
在redis list中有一个重要的操作, 即pop. pop元素既从list获取元素, 也会将此元素从list剔除. 同时, 你能够从左边(left)和右边(right)pop元素.
> rpush mykey a b c
(integer) 3
> rpop mylist
"c"
> lpop mylist
"a"
常见list用例
list有两个典型的用例, 如下:
- 记住用户发布到社交网络的最新更新
- 能够通过使用消费者-生产者模式在进程之间通信, 生产者将元素推送到list中, 而消费者获取元素并执行操作. redis有特殊的list命令使得操作这些用例更加灵活和高效.
让我们简单描述常用的用例, 可以想象你的主页展示了社交网络发布的最新相片, 你为了更快速的访问这些相片
- 每次用户发布一个新的相片, 我们就用lpush将它的ID添加到list中
- 当用户访问到主页时, 我们就使用lrange 0 9获取最新发布的10个相片
限制list
redis允许我们使用list作为一个限制集合, 通过ltrim命令只保留最新的N个元素, 而抛弃最老的元素.
ltrim
命令类似于lrange
, 但是并不显示特定范围的list元素. 它保存给出的范围元素作为list的新值, 而删除范围外的元素.
> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
list的阻塞操作
list有一个特殊的功能使它能够用来实现队列(queue).
想象你需要在一个进程添加元素到list中, 同时在不同的进程用list中的这些元素做各种类型的工作.
这就是通用的生产者/消费者模式, 我们可以用如下简单方式去实现它.
- 添加元素到list中, 生产者调用lpush命令
- 获取元素从list中, 消费者调用rpop命令
然而有可能当list是空或者没有东西处理的时候, 那么rpop将返回NULL. 这种情况下, 消费者不得不等待一段时间或者循环rpop直到获取到元素.
这并不是一种好的方式, 因为它有几个不好之处:
- redis和客户端不得不去执行这个没用的命令(只返回NULL也不会做任何有用的工作)
- 增加了延迟. 因为在处理器接收到一个NULL之后, 它会等待一段时间. 为了减少等待时间, 我们将更频繁地调用rpop, 而这又扩大了问题1的影响, 即更多的无用命令将被调用.
因此redis实现了rpop和lpop的阻塞命令brpop
和blpop
. 如果list为空: 只有当有新的元素添加到list或者到了自定义的超时时间, 它才会返回一个caller.
> brpop tasks 5
1) "tasks"
2) "do_something"
如上操作: 等待tasks列表的元素, 但是如果5秒后还没有可用的元素将返回.
注意: 你能够使用0作为超时时间, 那么将一直等待list元素. 而且你能够用brpop定义多个list, 这样的话, 多个list都将以相同的时间等待, 也将获取到通知当第一个list获取到一个元素时.
下面有些关于brpop主要要点:
- 客户端以有序的方式提供服务: 第一个客户端在阻塞等待一个list, 那么当有元素添加到list时, 第一个客户端先获取到这个元素, 接着是第二个等待的客户端, 以此类推.
- 比起rpop, brpop返回的值不用. 它将返回两个元素(key(即list名)和value)的数组. 因为brpop和lrpop可以阻塞等待多个list的元素, 所以需要识别返回的元素是哪个list的元素
- 如果过了超时时间, 那么返回NULL
还有两个命令, 也可以去了解
rpoplpush
命令能够建立安全的列表, 或者转动列表brpoplpush
命令是阻塞的命令
自动创建以及移除key
到目前为止, 我们不需要在添加元素之前手动创建一个空list, 也不需要在list为空的时候手动删除list. 这些工作都由redis帮我们完成了.
这些工作并不只发生在list中, redis对所有由多个元素组合而成的数据类型(集合)都自动做了此类工作—-set, sorted set和hash.
基本上, 我们可以对这些行为总结出三个规则.
- 当我们添加元素到一个集合数据类型时, 当key不存在, 则redis先创建一个空的集合类型
- 当我们从一个集合数据类型移除元素时, 如果value是空的, 则redis会删除这个key.
- 使用不存在的key去调用一个读命令例如llen, 或者调用一个写命令移除元素, 那么将返回的结果和调用空集合返回的结果一样.
redis hash数据结构
redis hash以field-value的方式保存数据, 因此hash更方便表现一个对象, 而且hash中的field数量并没有实际的限制(看redis所在服务器的内存有多大).
hmset
命令保存多个field-value, 相对应地, hmget
获取多个field对应的value数组, 而hget
获取单个field的value.
> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hmget user:1000 username birthyear verified
1) "antirez"
2) "1977"
3) "1"
> hgetall user:1000
1) "antirez"
2) "1977"
3) "1"
如果需要了解更多hash相关命令, 可以从 http://redis.io/commands#hash 中了解
这里特别需要说明: 小hash(即拥有很少的field以及value值很小)会被特殊编码, 使它们很高效..
redis set数据结构
redis set是无序的strign集合.
sadd命令添加元素到set集合中.
> sadd myset 1 2 3
OK
> smembers myset
1) "1"
2) "2"
3) "3"
redis有sismember
命令检查是否存在给出的元素
> sismember myset 3
(integer) 1
> sismember myset 30
(integer) 0
set能够表示对象之间的关系, 比如我们用set实现标签.
假如我们需要给一个对象添加标签, 那么set可以包含所有某个对象相关联的ID. 想象我们需要给news对象添加标签, 如果ID为1000的news对象被标签1,2,5和77打上标签, 那么我们就可以用news:1000:tags集合保存标签ID.
> sadd news:1000:tags 1 2 5 77
(integer) 4
然而,有时候我们也可以对这种关系进行反转存储:即通过给出的标签ID查询出所有与此有关联的news对象ID
> sadd tag:1:news 1000 2000
(integer) 2
> sadd tag:2:news 1000 3000
(integer) 2
我们也可以使用sinter命令查看覆盖了多个tag的news对象, sinter
将返回多个set集合的公共拥有的值
spop
能够从set中随机移除一个元素, 这个命令可以解决一些问题. 比如, 你需要实现一个扑克牌游戏, 那么就可以使用set去模拟一副牌. 我们用前缀C,D,H,S分别模拟梅花,方块,红心,黑桃.
> 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
现在我们需要给每个玩家发送5张牌, spop因为能够随机返回一个元素, 所以它能够完成这个工作. 如果我们直接从deck中获取牌, 那么玩第二轮的时候,牌就不齐了. 所以我们一开始就应该拷贝一份出来, 方便第二轮使用. 为了完成这个工作, 我们可以调用sunionstore
命令, 它能够将唯一的值拷贝一份到目的set集合中.
> sunionstore game:1:deck deck
(integer) 52
如果你需要从set中随机获取元素, 而又不删除它, 那么你可以使用srandmember
命令. 我们可以用scard
命令去检查set中的元素个数, 确定是否删除了元素.
> scard game:1:deck
(integer) 52
> srandmember game:1:deck 2
1) "D2"
2) "S10"
> scard game:1:deck
(integer) 52
redis sorted set数据结构
sorted set很想hash和set的结合体. sorted set也是由不重复的string元素构成.
在sorted set中的每个元素都会关联一个浮点值, 称之为分数(score)(这也是为什么sorted set类型很像hash, 因为每个元素都映射到一个值). sorted set中的元素是有序的, 而这个顺序依照一下规则:
- 如果有A元素和B元素, 满足A.score > B.score, 那么A > B.
如果有A元素和B元素, 满足A.score = B.score, 且比较A元素和B元素的字母顺序, 有A.value > B.value, 那么A > B (不会出现A = B的情况, 因为set确保唯一性)
zadd mysortset 1 b 1 a 4 e 3 m
(integer) 4
根据以上的排序规则, 我们可以知道mysortedset里面的元素排序是: a, b, m, e
> zrange mysortset 0 -1
1) "a"
2) "b"
3) "m"
4) "e"
如果我们需要倒序的元素数组,怎么办? 这时,只需要用zrevrange
命令就可以实现了
> zrevrange mysortset 0 -1
1) "e"
2) "m"
3) "b"
4) "a"
如果我们需要将score也一起返回,怎么办?使用withscores
选项参数就可以了
> zrange mysortset 0 -1 withscores
1) "a"
2) "1"
3) "b"
4) "1"
5) "m"
6) "3"
7) "e"
8) "4"
关于sorted sets的结构实现的注意点:
sorted set通过双端口的数据结构实现, 它包含了一个跳跃list和一个hash表, 所以每次我们添加元素, redis花费的时间复杂度将是O(log(N)). 但这也有好处就是当我们可以直接获取已经排序好的元素, 而不用再做任何工作.
操作ranges
sorted sets的ranges功能更加强大, 如果有个sorted set通过出生年作为score,那么我们如何获取1960年之前(包含1960)出生的人的列表, 很简单, 通过zrangebyscore
命令就能完成它.
> zrangebysocre mysortset -inf 3
1) "a"
2) "b"
3) "m"
器重-inf表示负无穷. 即获取mysortset的score在负无穷和3之间的值, 包括负无穷和3.
我们也可以删除一定范围的元素
> zremrangebyscore mysortset 1 3
( Integer ) 3
除了上面这些range命令外, 还有一个get-rank的操作命令, 它能够返回在sorted set中的元素为知
> zrank mysortset "m"
(integer) 2
字典score
在redis 2.8中有一个新功能引入, 它可以获取一定范围的字典顺序元素, 只需要在sorted set中添加相同的score. 主要命令有zrangebylex
, zremrangebylex
和zlexcount
.
> zadd hackers 0 "Alan kay" 0 "Sophie Wilson" 0 "Richard Stallman" 0 "Anita Borg" 0 "Yukihiro Matsumoto"
0 "Hedy Lamarr" 0 "Claude Shannon" 0 "Linus Torvalds" 0 "Alan Turing"
可以使用字典来检索数据
> zrangebylex hackers [B [P
1) "Claude Shannon"
2) "Hedy Lamarr"
3) "Linus Torvalds"
需要注意的是一些标识符的作用
+: 无限大
-: 无限小
[: 大于或等于
]: 小于或等于
(: 大于
): 小于
更新score
sorted set的score可以随时更新, 只需要在此zadd一次需要更新的元素, 它的时间复杂度是O(log(N)).
redis bitmap数据结构
bitmap不是一个真实的数据类型, 一系列的bit操作都定义在string类型, 因为string是二进制安全的, 而且它的最大长度是512mb. redis允许使用二进制的数据的key(binary key)和二进制数据的value(binary value). bitmap就是二进制数据的value.
redis的setbit(key, offset, value)操作对指定key的value的指定偏移(offset)量置1或者0, 时间复杂度为O(1). 为了统计今日登录的用户数, 我们建立一个bitmap, 每一位标识一个用户ID. 当某个用户访问我们的网页或者执行了某个操作, 就在bitmap中把标识此用户的bit置为1. 在redis中获取此bitmap的key值是通过用户执行操作的类型和时间戳获得的.
这个简单的例子中, 每次用户登录会执行一次redis.setbit(daily_active_users, user_id, 1). 将bitmap对应为知的bit置1. 统计bitmap结果显示今天有9个用户登录. bitmap的key是daily_active_users, 它的值为1011110100100101.
因为日活跃用户每天都变化, 所以需要每天创建一个新的bitmap. 我们简单地把日期添加到key后面, 实现这个功能. 例如, 要统计某一个天有多少用户至少听了一个因为app中给的一首歌曲, 可以把这个bitmap的redis key设计为play:yyyy-mm-dd-hh. 当用户听了一首歌曲,我们只是简单在bitmap中把表示这个用户的bit置1.