环境:
- 操作系统:Ubuntu 20.04
- Redis:6.2.6
Redis并不是传统意义上简单的 key-value 存储,而是支持几种不同类型的数据结构:
String
List
Hash
Set
Sorted Set
先来看一下 key
。Redis的key是“binary safe”的,这就意味着可以使用任意的二进制序列作为key,比如一个简单的字符串 "foo"
或者一个JPEG图片。空字符串也是有效的key。
接下来,我们看一下Redis的数据类型,以及各种数据类型常用的命令。
注:本文中所有例子都是使用 redis-cli
命令行运行。
String
string
是最简单的数据类型,比如:
127.0.0.1:6379> set mykey1 myvalue1
OK
127.0.0.1:6379> get mykey1
"myvalue1"
127.0.0.1:6379>
虽然被称为 string
,但实际上值可以是任何类型,比如二进制数据,或者数值。
set
命令有一个变种 getset
,在设置新值的同时返回其旧值。相比于用两条命令先get再set,显然后者会有丢失更新的风险(两条命令之间的更新丢失了)。不过 getset
命令已经deprecate了,取代方法是给 set
命令加上 get
参数。
另外, set
命令还有 nx
和 xx
两个参数:
-
nx
:只有当key不存在时才赋值 -
xx
:只有当key已存在时才赋值
127.0.0.1:6379> set mykey1 hello nx
(nil)
127.0.0.1:6379> set mykey3 hello xx
(nil)
127.0.0.1:6379>
mykey1
已存在,而 mykey3
不存在,所以上面两条命令都失败了。
set
命令还可以通过 ex
参数指定过期时间:
127.0.0.1:6379> set mykey1 hello ex 5
OK
127.0.0.1:6379> get mykey1
"hello"
127.0.0.1:6379> get mykey1
(nil)
127.0.0.1:6379>
本例设置 mykey1
5秒钟过期,在5秒钟内查询,可以获取其值,超过5秒钟之后,就变成 nil
了。
也可以直接用 expire
命令来设置过期时间:
127.0.0.1:6379> set mykey1 hello
OK
127.0.0.1:6379> expire mykey1 5
(integer) 1
127.0.0.1:6379> get mykey1
"hello"
127.0.0.1:6379> get mykey1
(nil)
127.0.0.1:6379>
ttl
(time to live)命令用来查询生存时间:
127.0.0.1:6379> set mykey1 hello ex 5
OK
127.0.0.1:6379> ttl mykey1
(integer) 3
127.0.0.1:6379> ttl mykey1
(integer) 1
127.0.0.1:6379> ttl mykey1
(integer) -2
127.0.0.1:6379>
返回结果为 -2
,表示key值不存在。
对于数值类型的value,也可以使用 incr
或其变种 incrby
(相应的也有 decr
和 decrby
)改变其值。比如:
127.0.0.1:6379> set mykey2 100
OK
127.0.0.1:6379> incr mykey2
(integer) 101
127.0.0.1:6379> incrby mykey2 5
(integer) 106
127.0.0.1:6379>
注意, incr
操作是原子的,这就意味着在并发情况下不会产生冲突。
可以使用 type
命令查看数据类型:
127.0.0.1:6379> type mykey1
string
127.0.0.1:6379> type mykey2
string
可见,虽然 mykey2
实际上是数值类型,这里只是简单的把它当作 string
类型。实际上 type
命令的返回值只有 string
、 list
、 set
、 zset
、 hash
、 stream
和 none
(key值不存在)。
mset
/ mget
命令可以set/get多个key值,比如:
127.0.0.1:6379> mset x 111 y 222 z 333
OK
127.0.0.1:6379> mget x y z
1) "111"
2) "222"
3) "333"
exists
命令可以查询key值是否存在,而 del
命令可以删除key值。
127.0.0.1:6379> exists mykey1
(integer) 1
127.0.0.1:6379> del mykey1
(integer) 1
127.0.0.1:6379> exists mykey1
(integer) 0
127.0.0.1:6379>
List
常见的list有“Array List”和“Linked List”。Redis的 list
使用了“Linked List”。
常用命令有 lpush
, lpop
, lrange
(查询指定范围的元素), ltrim
(保留指定范围的元素),以及类似的 rpush
, rpop
, rrange
, rtrim
等。
127.0.0.1:6379> lpush mylist1 a b c d e f g
(integer) 7
127.0.0.1:6379> lpop mylist1
"g"
127.0.0.1:6379> lpop mylist1
"f"
127.0.0.1:6379> lrange mylist1 0 3
1) "e"
2) "d"
3) "c"
4) "b"
127.0.0.1:6379> lrange mylist1 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
5) "a"
127.0.0.1:6379> ltrim mylist1 1 2
OK
127.0.0.1:6379> lrange mylist1 0 -1
1) "d"
2) "c"
127.0.0.1:6379>
对于 lrange
和 ltrim
,需要指定范围,即开始下标和结束下标,下标值从 0
开始。注意, -1
表示最后一个下标, -2
表示倒数第二个下标。
lpop
和 rpop
命令有一个变种称为 blpop
和 brpop
(其中 b
表示blocking)。如果list为空,则命令会被阻塞,直到list有值,或者超时。例如:
127.0.0.1:6379> lrange mylist1 0 -1
1) "d"
2) "c"
127.0.0.1:6379> blpop mylist1 5
1) "mylist1"
2) "d"
127.0.0.1:6379> blpop mylist1 5
1) "mylist1"
2) "c"
127.0.0.1:6379> blpop mylist1 5
(nil)
(5.06s)
127.0.0.1:6379>
第一次运行 blpop mylist1 5
,弹出了 d
。
第二次运行 blpop mylist1 5
,弹出了 c
。
第三次运行 blpop mylist1 5
,因为list为空,所以命令被阻塞,5秒钟之后命令超时。
注意:
-
0
表示永远不超时; -
blpop
可以指定多个key,我们可以看到结果里面包含了每个key值; - 如果有多个
blpop
在等待数据,当数据到来时,会采用“先来先服务”的策略;
llen
命令返回list的长度:
127.0.0.1:6379> lrange mylist1 0 -1
1) "g"
2) "f"
3) "e"
4) "d"
5) "c"
6) "b"
7) "a"
127.0.0.1:6379> llen mylist1
(integer) 7
127.0.0.1:6379>
Hash
Redis的 hash
有点类似于Java的 Map
。
常用命令有 hset
, hmset
, hget
, hmget
等。
127.0.0.1:6379> hmset user1 name Tom age 30
OK
127.0.0.1:6379> hset user1 sex male
(integer) 1
127.0.0.1:6379> hget user1 name
"Tom"
127.0.0.1:6379> hmget user1 name age sex
1) "Tom"
2) "30"
3) "male"
127.0.0.1:6379>
incrby
命令在 hash
上对应的操作是 hincrby
:
127.0.0.1:6379> hincrby user1 age 1
(integer) 31
127.0.0.1:6379>
Set
set
是无序的元素集合。
常用命令有 sadd
(添加元素), smembers
(查询所有元素), sismember
(查询指定元素是否存在) spop
(随机弹出一个元素), srandmember
(随机获取一个元素,不弹出), scard
(查询set大小)等。
127.0.0.1:6379> sadd myset1 Tom Jerry John
(integer) 3
127.0.0.1:6379> smembers myset1
1) "Jerry"
2) "John"
3) "Tom"
127.0.0.1:6379> sismember myset1 Tom
(integer) 1
127.0.0.1:6379> sismember myset1 Kate
(integer) 0
127.0.0.1:6379> spop myset1
"Jerry"
127.0.0.1:6379> smembers myset1
1) "John"
2) "Tom"
127.0.0.1:6379> srandmember myset1
"Tom"
127.0.0.1:6379> smembers myset1
1) "John"
2) "Tom"
127.0.0.1:6379> scard myset1
(integer) 2
127.0.0.1:6379>
Sorted set
即有序集合,集合中的每个元素,都有 score
属性,用来排序。
- 如果
A.score
>B.score
,则A
>B
; - 如果
A.score
=B.score
,则直接比较A
和B
的字符串;
常用命令有 zadd
(添加元素), zrange
(顺序查询指定下标范围的元素), zrevrange
(逆序查询指定下标范围的元素), zrank
(顺序排名), zrevrank
(逆序排名), zrangebyscore
(顺序查询指定score范围的元素), zrevrangebyscore
(逆序查询指定score范围的元素), zremrangebyscore
(删除指定score范围的元素)等。
127.0.0.1:6379> zadd myzset 100 Tom 90 Jerry 110 John
(integer) 3
127.0.0.1:6379> zrange myzset 0 -1
1) "Jerry"
2) "Tom"
3) "John"
127.0.0.1:6379> zrange myzset 0 -1 withscores
1) "Jerry"
2) "90"
3) "Tom"
4) "100"
5) "John"
6) "110"
127.0.0.1:6379> zrevrange myzset 0 -1
1) "John"
2) "Tom"
3) "Jerry"
127.0.0.1:6379> zrank myzset Tom
(integer) 1
127.0.0.1:6379> zrevrank myzset Jerry
(integer) 2
127.0.0.1:6379> zrangebyscore myzset 95 110
1) "Tom"
2) "John"
127.0.0.1:6379> zrangebyscore myzset -inf 110
1) "Jerry"
2) "Tom"
3) "John"
127.0.0.1:6379> zrevrangebyscore myzset 110 95
1) "John"
2) "Tom"
127.0.0.1:6379> zremrangebyscore myzset 95 100
(integer) 1
127.0.0.1:6379> zrange myzset 0 -1
1) "Jerry"
2) "John"
127.0.0.1:6379>
注: -inf
表示负无穷大。
Bitmap
指的是 string
类型上的位运算。
常用的命令有 getbit
(查询指定位上的值), setbit
(设置指定位上的值), bitop
(位运算), bitcount
(统计 1
的个数), bitpos
(第一个 0
或 1
出现的位置)等。
例如:我们知道字符 h
的二进制编码是104,转换为二进制就是 01101000
:
127.0.0.1:6379> set mykey1 hello
OK
127.0.0.1:6379> getbit mykey1 0
(integer) 0
127.0.0.1:6379> getbit mykey1 1
(integer) 1
127.0.0.1:6379> getbit mykey1 2
(integer) 1
127.0.0.1:6379> getbit mykey1 3
(integer) 0
127.0.0.1:6379> getbit mykey1 4
(integer) 1
127.0.0.1:6379> getbit mykey1 5
(integer) 0
127.0.0.1:6379> getbit mykey1 6
(integer) 0
127.0.0.1:6379> getbit mykey1 7
(integer) 0
127.0.0.1:6379>
如果把第7位从 0
变成 1
,相当于把二进制编码变成105,也就是字符 i
:
127.0.0.1:6379> setbit mykey1 7 1
(integer) 0
127.0.0.1:6379> get mykey1
"iello"
127.0.0.1:6379>
把字符 m
( 01101101
)和字符 k
( 01101011
)做“按位与”运算,结果是 01101001
,即字符 i
:
127.0.0.1:6379> set mykey2 m
OK
127.0.0.1:6379> set mykey3 k
OK
127.0.0.1:6379> bitop and mykey4 mykey2 mykey3
(integer) 1
127.0.0.1:6379> get mykey4
"i"
127.0.0.1:6379>
分别统计 m
, k
, i
字符中 1
的个数:
127.0.0.1:6379> bitcount mykey2
(integer) 5
127.0.0.1:6379> bitcount mykey3
(integer) 5
127.0.0.1:6379> bitcount mykey4
(integer) 4
127.0.0.1:6379>
字符 m
( 01101101
)中第一个 0
的位置是0,第一个 1
的位置是1:
127.0.0.1:6379> set mykey2 m
OK
127.0.0.1:6379> bitpos mykey2 0
(integer) 0
127.0.0.1:6379> bitpos mykey2 1
(integer) 1
127.0.0.1:6379>
HyperLogLog
一开始,我看了半天也没理解这是干吗用的,后来仔细想了一下,大概明白它的意思了。这是为了估算集合里面的元素数量(cardinality)。
常用命令有 pfadd
, pfcount
, pfmerge
等。实际上,它跟 set
的操作非常类似:
127.0.0.1:6379> pfadd mykey5 a b c d e
(integer) 1
127.0.0.1:6379> pfcount mykey5
(integer) 5
此时如果添加 f
,则集合中元素数量变成6。但如果添加已有元素 c
,则集合中元素数量仍然是6,因为集合里不能有重复元素。
127.0.0.1:6379> pfadd mykey5 f
(integer) 1
127.0.0.1:6379> pfcount mykey5
(integer) 6
127.0.0.1:6379> pfadd mykey5 c
(integer) 0
127.0.0.1:6379> pfcount mykey5
(integer) 6
127.0.0.1:6379>
那么问题来了,Redis本身就有 set
类型,也提供了 scard
命令查询元素数量,为何还要多此一举呢?
其原因就在于节省空间,因为它并不实际存储元素。可以想象,大概是用了hash之类的方法,把实际元素映射到一个hash值上。重复的元素自然hash值也相同,而不同的元素大概率hash值也不同,具体概率就涉及到hash算法了。hash值大小是固定的,这样一来,只用非常少的存储空间,就能获取cardinality的估算值了。估算值无法保证精确性,因为不同元素的hash值也可能相同。打个比方,有10000个元素,但是实际上只统计为9900个元素(损失了1%的精度),但是只用了千分之一甚至更少的存储空间,这对于精度要求不是特别高的场景,还是挺有意义的。
简言之,就是牺牲cardinality的精度为代价,以换取存储空间。当然,因为并不实际存储元素,所以也没法查询具体元素的内容。
发布-订阅
常用命令有 publish
, subscribe
等。
启动两个命令行。
在第一个命令行订阅 mychannel1
:
27.0.0.1:6379> subscribe mychannel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "mychannel1"
3) (integer) 1
在第二个命令行发布消息:
127.0.0.1:6379> publish mychannel1 "hello world"
(integer) 1
127.0.0.1:6379>
切回到第一个命令行,可见已经捕获了该消息:
1) "message"
2) "mychannel1"
3) "hello world"