距离上次更新本文已经过去了 401 天,文章部分内容可能已经过时,请注意甄别

学习 Redis 的命令可以参考官网文档:Commands | Redis。我还发现官网的 examples 里面是可以敲 Redis 命令的,如果你没有配置 Redis 环境,应该可以白嫖这里的 Redis 做个临时的练习。

image.png

本文使用 Redis 版本:

plaintext
1
2
3
4
❯ sudo redis-server --version
Redis server v=6.0.16 sha=00000000:0 malloc=jemalloc-5.2.1 bits=64 build=a3fdef44459b3ad6
❯ sudo redis-cli --version
redis-cli 6.0.16

注意,本文并非 Redis 命令大全,只是对最常用的命令做的记录。

1.set 和 get

基本使用

在 Redis 中,最常用的就是这两个命令。顾名思义,set 是用来设置 key 值的,get 是用来获取某个 key 值的。

注意 set/get 命令只能用于设置字符串 string 变量,其他类型有对应的设置命令,用 get 获取一个非 string 的 key 会失败。

plaintext
1
2
set key value
get key

在 redis 中不区分命令的大小写(但是区分 key 和 value 的大小写),字符串也不需要加引号(如果有空格就需要加),set 成功会返回 OK,失败会返回 nil

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> set key2 value2
OK
127.0.0.1:6379> get key2
"value2"
127.0.0.1:6379> get key3
(nil)
127.0.0.1:6379> get KEY2
(nil)
127.0.0.1:6379> set k "100 01"
OK
127.0.0.1:6379> get k
100 01

最后的 key3 是一个不存在的键值,Redis 返回了 (nil),意思和 NULL 其实一样,就是没有值。同时大写的 KEY2 也没有值,因为 Redis 中的 key 需要区分大小写。

如果尝试 set 一个已经存在的 key,会自动覆盖旧数据。覆盖的时候可能会修改 key 对应 value 的数据类型。

plaintext
1
2
3
4
5
6
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379> set key1 value11
OK
127.0.0.1:6379> get key1
"value11"

如果你需要设置的字符串中有引号,可以用 \ 进行转义,但还必须在外部再套一个引号

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> set k "10"
OK
127.0.0.1:6379> get k
10
127.0.0.1:6379> set k \"10\"
Invalid argument(s)
127.0.0.1:6379> set k "\"10\""
OK
127.0.0.1:6379> get k
"10"
127.0.0.1:6379> object encoding k
embstr

进阶选项

set 命令还有一些额外的选项可供选择,具体参考官网文档 SET | Redis

plaintext
1
2
SET key value [NX | XX] [GET] [EX seconds | PX milliseconds |
EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

在 Redis 官网上,[] 代表可选命令参数,| 可以理解为 “或”,代表每个可选命令参数中的不同设置值;每个 [] 之间是独立的,可以同时存在。如下是对 set 命令选项的说明:

  • NX:只有 key 不存在的时候才能 set 成功;
  • XX:只有 key 存在的时候才能 set 成功;
  • GET:如果 key 已存在,返回旧值;如果 key 不存在返回 nil(空);如果 key 存在但是 value 不是 string,则本次 set 操作会返回错误并终止;
  • EX/PX:set 的时候指定当前 key 的过期时间,过期时间到了会自动被删除,EX 的单位是秒,PX 的单位是毫秒(类似 sleep_for);
  • EXAT/PXAT:指定过期时间戳,即当前时间到指定时间戳后 key 值过期。EXAT 是秒级时间戳,PXAT 是毫秒级时间戳(类似 sleep_until);
  • KEEPTTL:如果 key 值已存在且有过期时间,使用 KEEPTTL 更新当前 key 的时候会重置过期时间计数器。举例:A 的过期时间是 100 秒,我在 A 设置后 60 秒又更新了 A,此时新的 A 的过期计时器会继承原有时间,即为 40 秒后过期。如果不指定 KEEPTTL 选项,则会被重置为 Redis 的默认 TTL(如果没有配置,默认是永不过期)。

注意,一些选项是高版本中才被支持的。

plaintext
1
2
3
4
Starting with Redis version 2.6.12: Added the EX, PX, NX and XX options.
Starting with Redis version 6.0.0: Added the KEEPTTL option.
Starting with Redis version 6.2.0: Added the GET, EXAT and PXAT option.
Starting with Redis version 7.0.0: Allowed the NX and GET options to be used together.

NX 和 XX

NX 是 key 值不存在才能正常被设置,XX 是 key 值存在才能被正常设置。如果没有被 set 成功,则会返回 nil,set 成功返回的是 OK。

如下所示,a 键值不存在时,nx 设置 a 成功返回 OK。再次用 nx 尝试设置,因为此时 a 已经存在,不符合 nx 的策略,设置失败,返回 nil

plaintext
1
2
3
4
127.0.0.1:6379> set a test1 nx
OK
127.0.0.1:6379> set a test1 nx
(nil)

使用 xx 测试,因为 a 存在,所以设置成功。而 b 不存在,设置失败。

plaintext
1
2
3
4
127.0.0.1:6379> set a test2 xx
OK
127.0.0.1:6379> set b test1 xx
(nil)

KEEPTTL

对 KEEPTTL 来做个测试,为了更好的展示效果,把一个始终摆在终端旁边。

我在 19 秒的时候执行了命令 set a test ex 20,即键值 a 会在 20 秒后过期。如同预期,在 39 秒左右,a 的键值无法被 get 了(截图落后了一些)

image.png

尝试在这 20 秒中重新给 a 设置一个新的值,同时不携带 KEEPTTL 选项。会发现重新设置后的 key 没有继承原有的过期时间,而是不过期了(Redis 的默认策略就是永不过期)

如果携带了 KEEPTTL 选项,则 a 还是会在 20 秒后过期。

image.png

使用 TTL 命令可以看的更直观一点(关于 TTL 命令的介绍参考后文),当我们使用 KEEPTTL 设置了 a 后,它的过期时间还是继承了原本的 10 秒,随后被过期删除,TTL 显示 -2 代表键值不存在。

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> set a t1 ex 10
OK
127.0.0.1:6379> ttl a
(integer) 8
127.0.0.1:6379> set a t2 keepttl
OK
127.0.0.1:6379> ttl a
(integer) 2
127.0.0.1:6379> get a
(nil)
127.0.0.1:6379> ttl a
(integer) -2

如果不使用 KEEPTTL 选项,则过期时间会变成 -1 代表永不过期。

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> set a t1 ex 10
OK
127.0.0.1:6379> get a
"t1"
127.0.0.1:6379> ttl a
(integer) 5
127.0.0.1:6379> set a t2
OK
127.0.0.1:6379> get a
"t2"
127.0.0.1:6379> ttl a
(integer) -1

mset 和 mget

这两个命令是 set/get 的变体,可以用于同时设置 / 获取多个键值的数据。这两个命令的时间复杂度都是 O(N),其中 N 是命令后跟随的 key 的个数。

mset 只能单纯的设置 key 和 value,没有 set 的额外的参数选项。

plaintext
1
2
MSET key value [key value ...]
MGET key [key ...]

测试如下

plaintext
1
2
3
4
5
127.0.0.1:6379> mset key1 t1 key2 t2
OK
127.0.0.1:6379> mget key1 key2
t1
t2

为什么要提供一个这样的命令呢?还是关于网络请求方面的性能节省。如果我知道我需要一次性设置 / 获取大量键值,那可以一条命令写完的绝对好过多次请求 Redis 服务器,节省了多次通信的网络延迟。

image.png

Redis 会保证 mset 的执行是原子的,不会出现 mset 中的多个键值只观察到其中一个被修改的情况。官网原话:MSET is atomic, so all given keys are set at once. It is not possible for clients to see that some of the keys were updated while others are unchanged.

比如我 mset 了 key1 和 key2,客户端可以观察到 key1 和 key2 都被修改,不会观察到只有 key1 或只有 key2 被修改了的情况。

这也是得益于 Redis 采用的单线程模型,它必须处理完毕 mset 的所有 key 后才会去响应其他命令请求,那客户端自然就不可能中断设置多个 key 的过程,也观察不到只有一部分 key 被修改了的情况。

但这也警示我们:如果尝试用 mset 设置特别大量的 key,可能会阻塞 Redis 服务器导致其无法响应其他请求。

2.keys

keys 命令用于查询当前 Redis 中的 key 值有哪些,如果匹配不上则返回 empty array

命令格式如下,KEYS | Redis

plaintext
1
keys [pattern]

其中参数 pattern 是一个字符串,类似于正则表达式,用来匹配 Redis 中已有的 key 的名称。

举个最简单的例子,key* 代表匹配以 key 开头的所有键值,可以看到当前我的 Redis 有上文 set 的两个 key。

plaintext
1
2
3
127.0.0.1:6379> keys key*
1) "key1"
2) "key2"

pattern 支持的格式如下:

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
h?llo     matches hello, hallo and hxllo
问号只匹配一个字符

h*llo matches hllo and heeeello
星号代表匹配0个或者无数个字符

h[ae]llo matches hello and hallo, but not hillo
匹配a或者e这两个字符

h[^e]llo matches hallo, hbllo, ... but not hello
匹配除了e以外的其他单个字符

h[a-b]llo matches hallo and hbllo
匹配a到b这个区间中的单个字符

如果你需要匹配的字符串中包含 []*? 这些特殊字符,可以用 \ 来转义它们。

注意:keys 的时间复杂度是 O(N)(其采用遍历方式来获取结果),所以在生产环境的大数据库中一般都禁止使用 keys 命令,特别是 keys * 全部查询。因为 Redis 是单线程模型,如果用 keys 的查询把 Redis 给弄阻塞了,就没有办法提供服务给其他链接了。

在 Redis+MySQL 的框架中,如果使用 keys 命令导致 Redis 阻塞了,请求就会直接回源到 MySQL 上,导致 MySQL 服务器遭受了意料之外的大批量请求,它也容易挂掉。如果 Redis 和 MySQL 都挂了,业务就完蛋了🤣。

3.exists

命令介绍

判断某个 key 是否存在,返回值是一个数字:返回命令中给出的 key 中,实际存在的 key 的个数。

plaintext
1
exists key [key1 key2 ...]

举个例子,比如我使用如下命令。那么返回值就是这三个 key 中有几个 key 是存在的。

plaintext
1
exists key1 key2 key3

exists 命令检查一个 key 的时间复杂度是 O(1),因为 Redis 底层是用哈希表来管理 key 的。

Time complexity: O(N) where N is the number of keys to check.

Redis 的官方文档说 exists 命令的时间复杂度是 O(N),不要误解,这里的 N 是命令中给出 key 的个数。相当于我们需要检查 N 个 key,每个 key 都是 O(1),最终的时间复杂度就是 O(N) 了。

在实际使用时,如果需要查询多个 key 是否存在,一条命令写完肯定比多次查询更好。因为每一次查询都涉及到了一次网络请求和响应,一条命令把需要查询的多个 key 都告诉 Redis,得到一个返回结果是更好的选择。这就和 MySQL 中的请求一样,一条 SQL 能解决的事情分成两个 SQL 就会因为网络通信而增加延迟!

Redis 中的大部分命令都支持一次性给定多个 key,就是为了减少网络通信带来的影响。

当然,exists 命令的返回结果并没有办法让我们知道给出的 key 中到底是哪一个不存在,这是要看具体业务来确定的。

命令测试

实际测试如下

plaintext
1
2
3
4
5
6
127.0.0.1:6379> exists key1 key2
(integer) 2
127.0.0.1:6379> exists key1
(integer) 1
127.0.0.1:6379> exists key
(integer) 0

注意,exists 在检查某个 key 是否存在的时候并不会进行去重操作,如果你尝试检查两个相同的 key,返回结果也是 2;

plaintext
1
2
127.0.0.1:6379> exists key1 key1
(integer) 2

4.del

用于删除指定 key,返回值是一个数字,给定 key 中被成功删除的个数。

plaintext
1
del key [key1 key2 ...]

删除单个 key 的时间复杂度是 O(1),多个是 O(N)

5.expire/pexpire

用于给某个 key 设置过期时间(时间到了之后自动被删除)其中 expire 的单位是秒,pexpire 的单位是毫秒。

plaintext
1
2
expire key second
pexpire key millisecond

这个功能非常有用,在很多平台上我们都使用过验证码,为了保证一定的安全性,验证码都被设计为有过期时间的。这样系统生成一个验证码后,可以将其 set 进入 Redis,然后使用 expire 命令给验证码设置过期时间(其实 set 命令里面就有选项可以设置过期时间了,这里只是举个例子)。还有限时优惠券、限时 VIP 体验券等等……

如果不使用 Redis 的 expire 功能,就需要在应用层甚至数据库中设计过期操作,相对来说就有些麻烦了。

还有另外一个场景,是基于 Redis 实现分布式锁:即在分布式集群中实现锁,以实现同步 / 互斥功能。

  • 加锁:设置一个特殊的 key,并设置过期时间;
  • 解锁:删除该 key;
  • 判断是否有人占用:exists 判断 key 存在则代表有人已经申请了锁,需要等待到 key 不存在的时候,则代表锁已经被释放。
  • 可以在 set 的时候使用 NX 选项,保证只有 key 值不存在的时候才能设置上 key(即只有锁没有被获取的时候才能加锁);
  • 因为 Redis 是单线程模型,所以 set 以及 exists 的操作是原子性的,不会出现并发访问的问题。

此时为了避免申请锁的应用服务器无法正常解锁(比如申请了锁的服务器突然挂掉了),都会给锁上一个过期时间,避免整个系统陷入死锁状态。这个过期时间应该略大于当前服务器需要访问临界资源的时间(避免还没有访问完毕就因为超时自动解锁了)。

6.ttl/pttl

这两个命令和 expire/pexpire 对应,用于命令显示某个键值的过期时间还剩下多少,ttl 返回的单位是秒,pttl 返回的单位是毫秒。该命令查询的时间复杂度是 O(1)

plaintext
1
2
ttl key
pttl key

这两个命令都有两个特殊的返回值

  • -1 代表当前键值永不过期;
  • -2 代表当前键值不存在;

Starting with Redis version 2.8.0: Added the -2 reply.

Redis 的过期策略

Redis 是怎么知道一个 key 是否过期需要被删除呢?

直接遍历肯定不行,当库很大的时候,遍历一次的时间都够喝一壶了,完全达不到过期时间的要求。

Redis 采用了定时删除 + 惰性删除策略的结合:

  • 定时删除:Redis 定时(默认 100ms 检查一次)抽取一部分 key 检查 TTL 过期时间,并将过期的 key 删除。因为只抽取一部分,所以检查的耗时短;
  • 惰性删除:用户请求某个 key 的时候检查 TTL 过期时间,过期的 key 被删除,不会返回给用户;

这两个操作虽然维持了性能,但并不能保证过期键一定能被清理掉。如果某个键值很久没有被使用,且没有被 Redis 的定期删除策略抽取到,它就会留存在那里。要想保证 Redis 中不存在过期键,还需使用一些辅助策略来处理。

定时器方案有哪些?

这里记录的是实现一个定时器可以采取的方式,和 Redis 采用的策略无关。

新建线程来定时删除

方式 1:使用线程来实现定时器。当需要定时删除的时候,启动一个线程,休眠指定时间,休眠结束后执行删除操作。但这会大大增加线程数量和系统负载,效率低。

基于过期时间的优先级队列

方式 2:使用优先级队列(以 TTL 剩余时间为 key 值,小的在前面),有设置过期时间的键值就被插入该队列,队首元素就是最快过期的键值。配置了优先级队列后,只需要一个线程去轮询检查队首来处理过期键值就行了。但是频繁的扫描会大大增加 CPU 负担。

方式 3:在方式 2 的基础上,添加休眠机制,让线程根据队首键值的过期时间进行休眠。休眠结束后删除队首元素。如果有新的键值插入,则唤醒该线程,重新根据队首键值的过期时间设置休眠时间。这可以通过条件变量的 wait_for 接口加上 notify 接口来实现。

时间轮方案

方式 4:使用时间轮方案, 如下表所示。左侧代表一个基于现在的时间偏移量,每一个格子对应的 100ms 时间偏移,右侧是一个链表。从数据结构来看可以是一个顺序表 + 链表。

我们只需要将 ttl 根据范围插入对应的时间偏移量需要删除的 key 中,线程每次休眠 100ms,休眠结束后就从链表里面取走过期的键值进行删除。

时间偏移量需要删除的 key(链表)
100msttl 在 [0,100] 之间的
200msttl 在 [101,200] 之间的
300ms

举个例子,现在是 0 秒,过期处理线程甲开始休眠 100ms。此时来了几个有过期策略的 key,分别是 50ms 过期 key1、150ms 过期 key2、190ms 过期 key3、210 秒过期 key4。那么我们就可以获得下面这张顺序表 + 链表。

时间偏移量需要删除的 key(链表)
100mskey1
200mskey2,key3
300mskey4

线程甲休眠 100ms 后,key1 的过期时间就已经到了(此时早已过去了 50ms)线程甲可以将其删除,然后再休眠 100ms 后处理 key2 和 key3,再休眠 100ms 处理 key4。

当然,当 key1 被删除后(第一个 100ms 已经休眠过了),表会出现下面的偏移。新来的键值都需要根据距离当前时间的偏移量插入到对应的位置。

时间偏移量需要删除的 key(链表)
100mskey2,key3
200mskey4

实际场景下,我们不可能让这个表格的长度一直增长下去,否则来个 30000ms (30s) 才过期的 key,就需要 300 个表格(左侧顺序表)的空间才能存下这个需要 30s 才过期的键值,而这之中可能有很多表格项是空的(没有对应的过期键值),存在空间浪费。

为了节省这部分空间,要采用类似哈希映射的思想,比如 1050ms 过期的 key 也会被插入到时间偏移量为 100ms 的链表中,当线程休眠结束后需要检查链表中的值的过期时间是否真的到了,到了才执行删除操作。

另外,我们也需要根据具体休眠时间间隔来确定每次应该休眠多久。假设我们的 key 都是 5ms 到 20ms 就过期了,此时设置休眠间隔为 100ms 就不太合理了。

7.type

该命令返回 key 对应 value 的类型,时间复杂度为 O(1)

plaintext
1
type key

返回值可以是 string, list, set, zset, hash and stream. 如果 key 不存在则返回 none(注意返回的不是 nil)。

在 Redis 官网文档中可以看到每个数据类型对应的数据结构:Understand Redis data types | Redis,其中当 redis 作为 消息队列时类型为 stream。

plaintext
1
2
3
4
5
6
127.0.0.1:6379> type key1
string
127.0.0.1:6379> lpush mylist 1 2
(integer) 2
127.0.0.1:6379> type mylist
list

8.object encoding 变量编码类型

Redis 提供了多种数据类型:Understand Redis data types | Redis

这些类型和我们常见的编程语言中的数据类型基本类似,但底层实现就不一样了

Redis 保证提供的数据类型符合这个数据类型应该有的预期,比如 hash 类型的插入和查询的效率应该是 O(1),但底层并不一定严格使用哈希表来实现。

Redis 会根据实际场景动态选择更加合适的底层数据结构实现,来一定程度的提高效率。一个纯整数数字的 string,直接用数字类型来存放会比使用字符串来存放占用更少的存储空间(1 字节可以存放无符号数 0 到 255,但 255 用字符串来存放需要 3 个字节)。

数据结构内部编码
stringraw (原始字符串)/int/embstr (为短字符串优化)
hashhashtable (哈希表)/ziplist (压缩列表,节省空间)
listlinkedlist (链表)/ziplist/quicklist
sethashtable/intset (整形集合)
zsetskiplist (跳表)/ziplist

ziplist 是用来存放小列表的编码方式,使用它相比直接使用链表或哈希表能节省空间占用。当 Redis 中有很多个很小的 list 和 hash 的时候,ziplist 就能辅助节省很可观的内存空间了。具体可以参考:Redis ziplist (压缩列表) CSDN 博客

在 Redis 中可以使用 object encoding 命令来查看一个键值底层实际的编码方式是什么

plaintext
1
object encoding key

可以看到,比较短的字符串是 embstr,list 类型是 quicketlist,全数字的字符串是 int(包括负整数也是用 int 存放的);

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> object encoding key1
"embstr"
127.0.0.1:6379> object encoding mylist
"quicklist"
127.0.0.1:6379> set key3 12345
OK
127.0.0.1:6379> object encoding key3
"int"
127.0.0.1:6379> get a
-1
127.0.0.1:6379> object encoding a
int

Redis 会在键值中数据增加到一定量级后自动切换编码方式,对于用户来说是无感知的,所以我们只需要知道这个思想就行了。

9.flushall 删库跑路

MySQL 可以删库跑路,Redis 自然也可以。这个命令的作用是清除 Redis 中的所有键值

plaintext
1
flushall

如果采用 Redis 作为缓存,清空了问题相对来说还没那么大,但如果是作为数据库,那就祝你好运吧……

请不要在生产环境使用这个命令哦!

plaintext
1
2
3
4
5
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *

127.0.0.1:6379>

The end

以上便是 Redis 的全局命令了(最常用的命令),关于其他数据类型的命令会单独起文讲解。