环境:

  • 操作系统: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 命令还有 nxxx 两个参数:

  • 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 (相应的也有 decrdecrby )改变其值。比如:

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 命令的返回值只有 stringlistsetzsethashstreamnone (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”。

常用命令有 lpushlpoplrange (查询指定范围的元素), ltrim (保留指定范围的元素),以及类似的 rpushrpoprrangertrim 等。

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>

对于 lrangeltrim ,需要指定范围,即开始下标和结束下标,下标值从 0 开始。注意, -1 表示最后一个下标, -2 表示倒数第二个下标。

lpoprpop 命令有一个变种称为 blpopbrpop (其中 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

常用命令有 hsethmsethgethmget 等。

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 ,则直接比较 AB 的字符串;

常用命令有 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 (第一个 01 出现的位置)等。

例如:我们知道字符 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>

把字符 m01101101 )和字符 k01101011 )做“按位与”运算,结果是 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>

分别统计 mki 字符中 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>

字符 m01101101 )中第一个 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)。

常用命令有 pfaddpfcountpfmerge 等。实际上,它跟 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的精度为代价,以换取存储空间。当然,因为并不实际存储元素,所以也没法查询具体元素的内容。

发布-订阅

常用命令有 publishsubscribe 等。

启动两个命令行。

在第一个命令行订阅 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"