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

image.png

本文使用Redis版本:

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会失败。

1
2
set key value
get key

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

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的数据类型。

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"

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

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

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(如果没有配置,默认是永不过期)。

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

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

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不存在,设置失败。

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代表键值不存在。

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代表永不过期。

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的额外的参数选项。

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

测试如下

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

1
keys [pattern]

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

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

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

pattern支持的格式如下:

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的个数。

1
exists key [key1 key2 ...]

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

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中到底是哪一个不存在,这是要看具体业务来确定的。

命令测试

实际测试如下

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;

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

4.del

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

1
del key [key1 key2 ...]

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

5.expire/pexpire

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

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)

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)

1
type key

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

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

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命令来查看一个键值底层实际的编码方式是什么

1
object encoding key

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

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中的所有键值

1
flushall

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

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

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的全局命令了(最常用的命令),关于其他数据类型的命令会单独起文讲解。