Redis 是一个 key-value 存储器,但是它的 value 的类型并不局限于 string,还可以是其他复杂的类型,如下表所示:
类型 | 说明 | 相关命令 |
---|---|---|
Binary-safe strings | 字符串 | APPEND, BITCOUNT, BITFIELD, BITOP, BITPOS, DECR, DECRBY, GET, GETBIT, GETRANGE, GETSET, INCR, INCRBY, INCRBYFLOAT, MGET, MSET, MSETNX, PSETEX, SET, SETBIT, SETEX, SETNX, SETRANGE, STRLEN |
Lists | 按插入顺序排列的双向链表,元素类型为字符串 | BLPOP, BRPOP, BRPOPLPUSH, LINDEX, LINSERT, LLEN, LPOP, LPUSH, LPUSHX, LRANGE, LREM, LSET, LTRIM, RPOP, RPOPLPUSH, RPUSH, RPUSHX, |
Sets | 无序,无重复值的字符串集合 | SADD, SCARD, SDIFF, SDIFFSTORE, SINTER, SINTERSTORE, SISMEMBER, SMEMBERS, SMOVE, SPOP, SRANDMEMBER, SREM, SSCAN, SUNION, SUNIONSTORE, |
Sorted sets | 有序,无重复值的字符串集合,每个元素关联一个 float 值,作为 score,用以排序 | BZPOPMAX, BZPOPMIN, ZADD, ZCARD, ZCOUNT, ZINCRBY, ZINTERSTORE, ZLEXCOUNT, ZPOPMAX, ZPOPMIN, ZRANGE, ZRANGEBYLEX, ZRANGEBYSCORE, ZRANK, ZREM, ZREMRANGEBYLEX, ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREVRANGE, ZREVRANGEBYLEX, ZREVRANGEBYSCORE, ZREVRANK, ZSCAN, ZSCORE, ZUNIONSTORE, |
Hashes | 元素为 field-value 形式的数据集,field 和 value 都为 string 型 | HDEL, HEXISTS, HGET, HGETALL, HINCRBY, HINCRBYFLOAT, HKEYS, HLEN, HMGET, HMSET, HSCAN, HSET, HSETNX, HSTRLEN, HVALS, |
Bit arrays | bit 组,可看做位图,通过特定指令操作字符串对应的 bit 位 | APPEND, BITCOUNT, BITFIELD, BITOP, BITPOS, DECR, DECRBY, GET, GETBIT, GETRANGE, GETSET, INCR, INCRBY, CRBYFLOAT, MGET, MSET, MSETNX, PSETEX, SET, SETBIT, SETEX, SETNX, SETRANGE, STRLEN, |
HyperLogLogs | ||
Streams |
要想更好的理解 redis,并使用 redis 解决实际问题,必须先好好的了解 redis 的数据结构,它的概念?相关的命令?是怎么样的?等等。在学习 redis 时,可以多使用 redis-cli 工具,这是一个简单、方便、又强大的命令行工具。
.
redis 的键是二进制安全的,也就是说可以为二进制数据,比如 JPEG 文件,空字符串也是有效的 key。
键值不宜过长,比如 1024 字节的key,即不便于记忆,也不方便查询。但也不应太过简洁,例如 “u1000flw”, 显然不如 “user:1000:followers”,后者更易读。在实际应用中,需要在内存占用
和 可理解性
两者之间做权衡。
键值最好有一定的结构性,类似 “object-type:id” 形式的,如 “user:1000”,或者 “comment?reply.to”,不同属性使用 :
分割,相同属性的多个单词之间可以使用 .
或 _
分割。
key 最大不超过 512 MB.
字符串是 redis 中最简单的值。
实际工作中,需要缓存
类型数据的场景很多,比如缓存 HTML 页。
> set mykey somevalue
OK
> get mykey
“somevalue”
在 redis-cli 中,使用 set/get 命令来 设置/获取
键值对,当调用 set 时,如果 key 对应的 value 已存在,则用新值替换原值,不管原值为何种类型。
当 value 为 string 时,大小不能超过 512 MB。
SET 命令有些有意思的选项,在调用时作为附加参数,例如,当 key 存在时,使 set 失败;或者,只有当 key 存在时,set 才执行。
> set mykey newval nx
(nil)
> set mykey newval xx
OK
INCR, INCRBY, DECR and DECRBY,这些命令会将 string 解析为 integer,并原子性的增加或减少。
> set counter 100
OK
> incr counter
(integer) 101
incrby counter 50
(integer) 152
GETSET
命令在为 key 赋值的同时,返回 key 对应的旧值。这个命令有时很有用,比如,当有用户访问时,系统中的某个 counter 值就加 1,并且每过一个小时,获取 counter 当前值,并将 counter 重置为 0;如果不是 GETSET
,当访问量较大时,统计必然会出现遗漏。
MSET 和 MGET 可以处理多个 key 的情况:
> mset a 10 b 20 c 30
OK
> mget a b c
“10”
“20”
“30”
MGET 返回的结果为数组。
可以使用 TYPE 命令查看 key 对应的值的类型。
> type mykey
string
del mykey
(integer) 1
type mykey
none
redis 支持设置 key 的过期时间,也就是 TTL(limited time to live),当 ttl 结束时,对应的 key 会自动清除。
相关的命令有:
EXPIRE key seconds
EXPIREAT key timestamp
PEXPIRE key milliseconds
PEXPIREAT key milliseconds-timestamp
ttl 相关信息会序列化到磁盘中,也就是说,即使 redis 服务关闭了,ttl 时间也会继续减少。
> set key some-value
OK
> expire key 5
(integer) 1
> get key (immediately)
“some-value”
> get key (after some time)
(nil)
PERSIT 命令用来取消 expire,让对应的 key 一致存在。
也可以使用 SET 附加属性来设置过期时间:
> set key 100 ex 10
OK
> ttl key
(integer) 9
redis 中的 list 是通过链表来实现的,因此可以快速的插入到表头或表尾,所消耗的时间与 list 的长度无关,其缺点是,随机访问比 array 慢。
在需要较多随机访问的场景中,可以选择使用 sorted set,而不是 list。
> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1
“first”
“A”
“B”
rpush/lpush 用于往 list 中添加元素,lrange 用以获取子 list,获取列表右侧的子list,可以使用负数下标。
可以同时添加多个元素
> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
> lrange mylist 0 -1
“first”
“A”
“B”
“1”
“2”
“3”
“4”
“5”
“foo bar”
POP 命令用于获取并删除元素,当对空列表执行 POP 时,会返回 null:
> rpop mylist
(nil)
使用案例 :
大多数情况下,我们使用 redis list 来缓存最近的更新,不管是社交网络内容,日志,还是其他数据。使用 LTRIM 命令,可以让 list 始终保存最近的 N 个元素,删除旧的数据。
> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
“1”
“2”
“3”
注意:LRANGE 操作的时间复杂度为 O ( n ) O(n) O(n)
redis list 可以用于消息队列,或者任务队列。当 list 为空时,RPOP 操作会返回 NULL,这时,可以选择等待一段时间后,再次调用 RPOP,这种方式称为轮询;或者使用 BRPOP 或 BLPOP 命令,当 list 为空时,让命令阻塞等待,直到有新的元素加入到 list 中,或者超时时间已过。
> brpop tasks 5
“tasks”
“do_something”
当 timeout 值为 0 时,会一直等待。
Redis 对 BRPOP 命令的处理是有序的,第一个调用的 client 会首先得到返回值。另外,由于 BLPOP 与 BRPOP 允许操作多个 list,所以其返回值是 (key, value) 元组。
RPOPLPUSH srclist, dst list
删除 srclist 中的元素,并将其添加到 dstlist 中。
注意,Redis 会自动创建不存在的 list,并且删除那些元素为空的 list。
Redis hash 中存储的是 (field, value)
二元组。
> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
“antirez”
> hget user:1000 birthyear
“1977”
> hgetall user:1000
“username”
“antirez”
“birthyear”
“1977”
“verified”
“1”
hash 中 field 的数目没有大小限制。
> hincrby user:1000 birthyear 10
(integer) 1987
> hincrby user:1000 birthyear 10
(integer) 1997
Sets 是无序的字符串集合。sets 相关的操作有元素的添加删除(SADD),判断存在性(SISMEMBER),求多个集合的交集,并集,差集,等待。
sadd myset 1 2 3
(integer) 3
smembers myset
3
1
2
SADD 命令往 sets 中添加元素,由上例可见,smembers 获取的元素是无序的。
> sismember myset 3
(integer) 1
> sismember myset 30
(integer) 0
集合很适于用来表示对象之间的关系,比如,用 sets 来存储对象的标签集,每个需要标注的对象,都分配一个 set,set 中保存该对象所有标签的 id。
对象所有标签的集合:
> sadd news:1000:tags 1 2 5 77
(integer) 4
标签关联的对象集:
> sadd tag:1:news 1000
(integer) 1
> sadd tag:2:news 1000
(integer) 1
> sadd tag:5:news 1000
(integer) 1
> sadd tag:77:news 1000
(integer) 1
> smembers news:1000:tags
5
1
77
2
备注:上例中,还需要一个用来映射标签 id 与 标签名称的 hash。
有时,会需要一些复杂的集合操作,比如想获得标注了 1,2,10,27 这四个标签的所有对象的集合,可以使用 SINTER 命令:
> sinter tag:1:news tag:2:news tag:10:news tag:27:news
… results here …
除了交集,还可以求并集,差集等。
下例中,用 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
SPOP 会随机的获取集合中的一个元素,因此,可以完美的模拟随机发牌的场景。当下一局发牌时,还需要重新初始化 derk,可以使用 SUNIONSTORE 命令来备份 set,这样就不需要每次都重新初始化了。
> sunionstore game:1:deck deck
(integer) 52
> spop game:1:deck
“C6”
> spop game:1:deck
“CQ”
> spop game:1:deck
“D1”
> spop game:1:deck
“CJ”
> spop game:1:deck
“SJ”
SCARD 能返回集合中元素的个数,又称为集合的基数(cardinality)。
scard game:1:deck
(integer) 47
The math works: 52 - 5 = 47.
如果仅仅希望随机获取元素,但不删除,可以使用 SRANDMEMBER,该命令支持重复获取和非重复获取元素两种方式。
Sorted sets 像是 sets 与 hash 的混合体,首先,它像 sets 一样是非重复元素的集合,但是 sorted sets 中的元素会关联一个 float 值,称为 score,这跟 hash 有点像。
zadd key [NX|XX] [CH] [INCR] score member [score member ...]
> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer) 1
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1
ZADD 的使用方式跟 SADD 类似,但是多了个一个 score 参数。
> zrange hackers 0 -1
“Alan Turing”
“Hedy Lamarr”
“Claude Shannon”
“Alan Kay”
“Anita Borg”
“Richard Stallman”
“Sophie Wilson”
“Yukihiro Matsumoto”
“Linus Torvalds”
使用 ZREVRANGE 来获取反序的列表
> zrevrange hackers 0 -1
“Linus Torvalds”
“Yukihiro Matsumoto”
“Sophie Wilson”
“Richard Stallman”
“Anita Borg”
“Alan Kay”
“Claude Shannon”
“Hedy Lamarr”
“Alan Turing”
使用 WITHSCORES 参数,可以获取包含 score 的结果列表:
> zrange hackers 0 -1 withscores
“Alan Turing”
“1912”
“Hedy Lamarr”
“1914”
“Claude Shannon”
“1916”
“Alan Kay”
“1940”
“Anita Borg”
“1949”
“Richard Stallman”
“1953”
“Sophie Wilson”
“1957”
“Yukihiro Matsumoto”
“1965”
“Linus Torvalds”
“1969”
sorted sets 还支持很多强大的,复杂的操作,比如使用 ZRANGEBYSCORE 命令获取所有 1950 年之前出生的黑客,使用
> zrangebyscore hackers -inf 1950
“Alan Turing”
“Hedy Lamarr”
“Claude Shannon”
“Alan Kay”
“Anita Borg”
bitmaps 并不是实际的数据类型,而是一组定义在字符串上的 bit 操作集。redis 中字符串的最大长度为 512 MB,也就是说最大位数为 2 32 2^{32} 232。
比特操作分为两类:单点操作,组操作。
位图,又称为位掩码,比如,40亿人的性别信息,可以使用 512 MB大小的比特位来表示,而不是使用 id + boolean 的形式。
A HyperLogLog is a probabilistic data structure used in order to count unique things (technically this is referred to estimating the cardinality of a set). Usually counting unique items requires using an amount of memory proportional to the number of items you want to count, because you need to remember the elements you have already seen in the past in order to avoid counting them multiple times. However there is a set of algorithms that trade memory for precision: you end with an estimated measure with a standard error, which in the case of the Redis implementation is less than 1%. The magic of this algorithm is that you no longer need to use an amount of memory proportional to the number of items counted, and instead can use a constant amount of memory! 12k bytes in the worst case, or a lot less if your HyperLogLog (We’ll just call them HLL from now) has seen very few elements.
HLLs in Redis, while technically a different data structure, are encoded as a Redis string, so you can call GET to serialize a HLL, and SET to deserialize it back to the server.
Conceptually the HLL API is like using Sets to do the same task. You would SADD every observed element into a set, and would use SCARD to check the number of elements inside the set, which are unique since SADD will not re-add an existing element.
While you don’t really add items into an HLL, because the data structure only contains a state that does not include actual elements, the API is the same:
Every time you see a new element, you add it to the count with PFADD.
Every time you want to retrieve the current approximation of the unique elements added with PFADD so far, you use the PFCOUNT.
pfadd hll a b c d
(integer) 1
pfcount hll
(integer) 4
An example of use case for this data structure is counting unique queries performed by users in a search form every day.
Redis is also able to perform the union of HLLs, please check the full documentation for more information.