🔥 本文专栏:Redis
🌸作者主页:努力努力再努力wz



💪 今日博客励志语录 :
真正拉开差距的,不是某一天突然开窍,而是你在看不到回报的时候,依然愿意把今天该做的事做完。
思维导图

引入
在此前的学习中,我们已经对 Redis 建立了一个基本认知:Redis 和 MySQL 一样,都可以对外提供数据存储与访问能力。不过二者的定位并不完全相同。MySQL 是关系型数据库,核心职责是持久化存储、事务控制、复杂查询以及数据一致性保障;而 Redis 则是一个基于内存的 key-value 数据结构服务,在实际开发中经常被用作缓存,用来存储热点数据,从而降低 MySQL 的访问压力。
虽然 MySQL 内部也存在缓存机制,例如 InnoDB 存储引擎会通过 Buffer Pool 缓存数据页、索引页等内容,尽可能减少磁盘 IO,但是需要注意的是,MySQL 的访问开销并不只来自磁盘 IO。对于一条 SQL 语句来说,它从客户端发送到 MySQL 服务端之后,通常还需要经过连接管理、词法分析、语法分析、语义检查、权限校验、优化器选择执行计划、执行器调用存储引擎等多个阶段。
而在 InnoDB 存储引擎内部,数据默认又是以 16KB 的页作为基本管理单位,并且通常通过 B+ 树来组织索引和数据。因此,当 MySQL 访问目标数据时,往往需要从根页开始向下查找,逐层定位到目标叶子页。如果目标页没有命中 Buffer Pool,还会进一步触发磁盘 IO。在事务场景下,InnoDB 还可能涉及 MVCC 可见性判断;如果是当前读或者写操作,还可能涉及加锁、锁等待等额外开销。
相比之下,Redis 的访问路径要轻量得多。Redis 主要将数据存储在内存中,并通过 key-value 的方式组织数据。客户端向 Redis 服务端发送命令后,Redis 通常可以根据 key 在内部字典结构中快速定位到对应的 value,然后返回结果。因此,Redis 访问数据比 MySQL 更快,其中一个重要原因就在于:Redis 的数据访问路径更短,执行过程更轻量;另一个重要原因则是:Redis 的数据主要存储在内存中,避免了大量磁盘 IO 带来的延迟。
不过,Redis 并不是程序内部定义的一块普通内存空间。它本质上是一个独立运行的客户端-服务器网络服务程序。它自己占用一块进程内存,在里面组织 key-value 数据结构。你的 C++ 程序、Java 程序、命令行客户端并不能直接访问这块内存。如果想操作 Redis 中的数据,就需要先与 Redis 服务端建立连接,然后通过命令请求 Redis 服务端完成对应的数据读写操作。也就是说,如果我们想使用 Redis 提供的数据存储能力,就必须先通过客户端与 Redis 服务端建立连接,然后向 Redis 服务端发送命令,由 Redis 服务端解析并执行对应的操作。
所以总结来说,Redis 并不是程序进程内部的一块普通内存空间,而是一个运行在用户态的应用层内存数据管理服务。它负责在自己的进程内存中组织和管理数据,并通过网络协议对外提供访问能力。既然 Redis 是通过命令来完成数据操作的,那么接下来的内容,我们就从 Redis 中几个核心且基础的命令开始展开。
Redis 基础命令入门
启动 Redis 客户端:redis-cli 的连接方式与 PATH 问题解析
根据上文,我们已经认识到,Redis 本质上是一个客户端-服务器模型的网络服务程序。如果我们想操作 Redis 服务端管理的数据,就需要先通过客户端与 Redis 服务端建立连接,然后在客户端输入命令。客户端会将这些命令按照 Redis 协议进行编码,并将其作为请求数据发送给 Redis 服务端,由 Redis 服务端解析并执行对应的操作。
在学习具体命令之前,我们首先需要认识如何启动 Redis 客户端。Redis 提供的命令行客户端是 redis-cli。当我们使用 redis-cli 连接 Redis 服务端时,底层通常会创建 TCP 套接字,并与 Redis 服务端建立 TCP 连接。
redis-cli 后面还可以携带一些连接选项,其中比较常用的是 -h 和 -p。-h 用来指定 Redis 服务端所在主机的 IP 地址,-p 用来指定 Redis 服务端监听的端口号。例如:
bash
redis-cli -h 127.0.0.1 -p 6379
其中,127.0.0.1 表示本机回环地址,6379 是 Redis 默认监听端口。如果 Redis 客户端和服务端都运行在同一台主机上,并且服务端使用的是默认端口,那么也可以直接执行:
bash
redis-cli
此时,客户端默认会连接本机的 127.0.0.1:6379。需要注意的是,即使 Redis 客户端和服务端位于同一台主机上,这个过程通常仍然基于 TCP/IP 协议完成。只不过此时数据会通过本地回环接口在本机内部完成传输,而不会真正经过外部网络。
这里还可以顺便补充一个细节:如果我们直接在终端中输入 redis-cli,但是系统提示 command not found,并不一定代表 Redis 客户端程序不存在,而是可能因为 redis-cli 这个可执行文件所在的目录没有被加入到 PATH 环境变量中。
当我们在 Bash 中输入一个外部命令时,Bash 会先判断该命令是否是别名、Shell 函数或者内建命令。如果都不是,Bash 才会将其视为一个外部可执行程序,并根据 PATH 环境变量中保存的目录列表,依次查找与该命令同名的可执行文件。只有当 Bash 找到对应的可执行文件之后,才会继续创建子进程,并在子进程中通过 exec 系列接口完成进程替换,最终运行对应的程序。
因此,如果我们输入:
bash
redis-cli
但是 Bash 在 PATH 保存的目录中找不到名为 redis-cli 的可执行文件,就会提示:
bash
-bash: redis-cli: command not found
而如果我们进入了 Redis 源码目录即redis-8.6.3,它里面通常会有这些内容:
text
redis-8.6.3/
├── src/
├── deps/
├── tests/
├── redis.conf
├── Makefile
└── README.md
而如果我们在 Redis 源码目录下执行:
bash
src/redis-cli
这里的命令中携带了明确的相对路径,Bash 就不需要再从 PATH 中搜索,而是会直接到当前目录下的 src 目录中查找 redis-cli 可执行文件。因此,src/redis-cli 能够执行,而 redis-cli 不能直接执行,本质原因就是 redis-cli 所在目录没有被加入到 PATH 环境变量中。
如果希望在任意目录下都能直接执行 redis-cli,可以使用 root 权限,将 redis-cli 这个可执行文件复制到系统命令目录中。例如:
bash
sudo cp ~/redis-8.6.3/src/redis-cli /usr/local/bin/
这里的 /usr/local/bin/ 通常属于系统命令搜索路径中的一个目录。当我们把 redis-cli 可执行文件复制到该目录之后,后续无论当前处于哪个目录,Bash 在解析 redis-cli 命令时,都可以在系统命令目录中找到对应的可执行文件。
因此,此时就可以直接执行:
bash
redis-cli

而不再需要每次都进入 Redis 源码目录,然后通过:
bash
src/redis-cli
这种相对路径的方式启动 Redis 客户端。
从 GET/SET 到 MGET/MSET:Redis 字符串数据的基础读写
根据上文,我们已经知道如何启动 Redis 客户端。接下来,我们就可以正式进入 Redis 基础命令的学习。
其实,学习 Redis 基础命令的思路和学习 MySQL 的 SQL 语句有一定相似性。学习 MySQL 时,我们主要围绕数据库表中的数据进行 CRUD 操作;而学习 Redis 命令时,本质上也是围绕 Redis 服务端所存储和管理的数据进行 CRUD 操作。只不过 MySQL 是以数据库表和记录为核心组织数据,而 Redis 是以 key-value 结构为核心组织数据,因此 Redis 的命令通常会围绕 key 以及 key 对应的 value 类型展开。
首先需要说明的是,Redis 支持的命令非常多,但是在初学阶段,我们没有必要一开始就掌握所有命令。更合理的学习方式,是先掌握最核心、最常用的命令,通过这些命令建立对 Redis 数据读写方式的基本认识。
Redis 本质上是一个客户端-服务器模型的内存数据管理服务。Redis 服务端进程会在自己的进程地址空间中组织和管理数据,并通过网络协议对外提供数据读写能力。因此,Redis 提供的最核心能力,必然离不开对数据的读取和写入:一方面,我们需要从 Redis 服务端查询已经存储的数据;另一方面,我们也需要向 Redis 服务端写入新的数据。
所以,这里首先认识的两个基础命令就是 GET 和 SET。
GET 命令
GET 命令用于根据指定的 key 查询对应的 value,其基本语法如下:
bash
GET key
例如:
bash
GET name

这里需要注意,Redis 是一种 key-value 结构的数据存储服务。对于 Redis 中的一条记录来说,可以将其理解为一个键值对。其中,key 一定是字符串类型,而 value 则可以是多种不同的数据类型,例如 string、list、set、hash、zset 等。
也正因为 value 支持多种数据类型,所以不同类型的数据通常需要使用不同的命令进行操作。例如:
text
string -> GET / SET
hash -> HGET / HSET
list -> LPUSH / LRANGE
set -> SADD / SMEMBERS
zset -> ZADD / ZRANGE
因此,GET 命令并不是用于获取所有类型的 value,而是主要用于获取 string 类型的 value。如果某个 key 存在,但是它对应的 value 并不是 string 类型,那么使用 GET 命令访问时,Redis 会返回类型错误。
当我们在客户端输入 GET 命令后,客户端会将该命令按照 Redis 协议进行编码,并将其作为请求数据发送给 Redis 服务端。Redis 服务端接收到请求之后,会解析请求内容,并根据 key 在内部字典结构中查找对应的 value。如果查找命中,就将对应的 value 封装到响应数据中返回给客户端;如果 key 不存在,则会返回:
bash
(nil)
这里的 (nil) 表示没有查询到对应的数据,可以理解为"空结果"。它在语义上类似于编程语言中常见的 NULL,但二者并不是完全相同的概念。
另外,Redis 的命令名本身不区分大小写。例如:
bash
GET name
get name
Get name
这些写法在命令层面是等价的。但是,命令中的 key 和 value 通常是区分大小写的。例如:
bash
GET name
GET Name
这里的 name 和 Name 会被 Redis 当作两个不同的 key。
SET 命令
认识了 GET 命令之后,接下来再来看与它配套的 SET 命令。
SET 命令用于向 Redis 中设置一个 string 类型的键值对,其基本语法如下:
bash
SET key value
例如:
bash
SET name wz
这条命令表示向 Redis 中写入一个 key 为 name、value 为 wangzhe 的键值对。执行成功后,Redis 会返回:
bash
OK
此时,再通过 GET 命令查询:
bash
GET name
就可以得到:
bash
"wz"
需要注意的是,SET 命令不仅可以插入新的键值对,也可以覆盖已有的键值对。也就是说,如果 Redis 中原本不存在对应的 key,那么 SET 会创建一条新的记录;如果 Redis 中已经存在对应的 key,那么 SET 会使用新的 value 覆盖原来的 value。并且,这种覆盖并不要求原来的 value 一定也是 string 类型。即使该 key 原本对应的是 list、hash、set 等其他类型的 value,执行 SET key value 之后,原来的数据也会被覆盖掉,该 key 对应的新 value 会变成 string 类型。因此,SET 命令本质上是将指定 key 设置为一个 string 类型的 value,而不是只更新原有 string 类型的数据。
例如:
bash
SET name wz
SET name redis
GET name
最终查询到的结果就是:
bash
"redis"

因此,对于 string 类型的数据来说,SET 负责写入或更新数据,GET 负责根据 key 查询数据。通过这两个命令,我们就可以完成 Redis 中最基础的 key-value 数据读写操作。
MGET 和 MSET
其次,我们还需要注意一个细节:Redis 本质上是一个客户端-服务器模型的网络服务程序。也就是说,我们在客户端输入的每一个命令,都会被客户端按照 Redis 协议进行编码,并作为请求数据发送给 Redis 服务端;Redis 服务端执行完命令之后,也需要再将执行结果作为响应数据返回给客户端。
因此,Redis 命令的执行并不只是服务端内部的数据结构操作,还会涉及客户端与服务端之间的网络通信成本。即使 Redis 本身处理命令的速度很快,多次请求和响应仍然会带来额外的网络往返开销,也就是 RTT 开销。
例如,如果我们需要连续查询多个 key,可以分别执行多次 GET 命令:
bash
GET key1
GET key2
GET key3
但是这种方式意味着客户端需要多次向 Redis 服务端发送请求,Redis 服务端也需要多次返回响应。如果这些查询动作本身可以合并,那么更合理的方式是使用 MGET 命令一次性查询多个 key:
bash
MGET key1 key2 key3
MGET 命令会按照 key 的输入顺序返回对应的 value。如果某个 key 不存在,那么对应位置会返回 (nil)。例如:
bash
MGET name age address
可能得到如下结果:
bash
1) "wz"
2) "20"
3) (nil)
这表示 name 和 age 查询到了对应的 value,而 address 这个 key 不存在。
与 MGET 对应,如果我们需要一次性写入多个 string 类型的键值对,就可以使用 MSET 命令。它的基本语法如下:
bash
MSET key1 value1 key2 value2 key3 value3
需要注意的是,MSET 中的 key 和 value 是成对出现的,而不是先写完所有 key,再写所有 value。例如:
bash
MSET name wz age 20 city chengdu
这条命令表示一次性向 Redis 中写入三个 string 类型的键值对:
text
name -> wz
age -> 20
city -> chengdu
和 SET 命令一样,如果 MSET 中指定的 key 已经存在,那么新的 value 会覆盖原来的 value。并且,即使原来的 value 不是 string 类型,也会被新的 string 类型 value 覆盖。
因此,MGET 和 MSET 的核心意义在于:当我们需要批量读取或批量写入 string 类型的数据时,可以将多个操作合并成一次命令发送给 Redis 服务端,从而减少客户端与服务端之间多次请求和响应带来的网络往返开销。
KEYS 命令详解:按模式查询 key 及其性能风险
认识了 GET 和 SET 命令之后,接下来我们再来看另一个基础命令:KEYS。
在使用 Redis 的过程中,我们可能会不断向 Redis 中写入新的数据。随着数据逐渐增多,有时候我们可能会忘记当前 Redis 中到底存在哪些 key。此时,就可以使用 KEYS 命令按照指定模式查询当前数据库中匹配的 key。
KEYS 命令的基本语法如下:
bash
KEYS pattern
其中,pattern 表示匹配模式。也就是说,KEYS 并不是直接查询某一个具体的 key,而是根据指定的模式,对 Redis 当前数据库中已有的 key 进行匹配查询。
例如,如果我们想查看当前 Redis 中存储的所有 key,可以输入:
bash
KEYS *
这里的 * 是一个通配符,表示匹配任意数量的任意字符。因此,KEYS * 表示匹配当前数据库中的所有 key。
需要注意的是,KEYS * 返回的是 key 的名字,而不是完整的键值对。例如,它可能返回:
bash
1) "name"
2) "age"
3) "city"
这只表示 Redis 中存在 name、age、city 这些 key,并不会直接返回它们对应的 value。如果想查看某个 key 对应的 value,还需要根据 value 的具体数据类型使用对应的查询命令,例如 string 类型可以使用 GET 命令。
除了 * 之外,KEYS 命令还支持其他匹配规则。例如:
text
* 匹配任意数量的任意字符
? 匹配单个任意字符
[abc] 匹配方括号中的任意一个字符
[^abc] 匹配不在方括号中的任意一个字符
[a-z] 匹配指定范围内的任意一个字符
例如:
bash
KEYS user:*
表示查询所有以 user: 开头的 key。
再比如:
bash
KEYS name?
表示匹配以 name 开头,并且后面只跟一个字符的 key,例如 name1、name2 等。
不过,虽然 KEYS 命令使用起来非常方便,但它有一个非常重要的问题:不适合在生产环境中随意使用。
原因在于,Redis 是一种 key-value 结构的数据存储服务,key 会被 Redis 维护在内部的数据结构中。当我们执行 KEYS pattern 时,无论 pattern 写得多具体,Redis 都需要在当前数据库已有的所有 key 中进行遍历匹配,并逐个判断这些 key 是否符合指定的匹配模式。因此,KEYS 命令的时间复杂度是 O(N),其中 N 表示当前数据库中的 key 数量。
如果 Redis 中存储的 key 数量很少,那么执行 KEYS 命令通常不会带来明显影响。但是如果 Redis 中已经存储了大量 key,例如几十万、几百万甚至更多,此时执行 KEYS * 就可能会导致 Redis 在一段时间内忙于遍历和匹配 key。
而 Redis 的命令执行主路径通常由主线程处理。这样设计的好处是可以避免多线程并发访问核心数据结构带来的复杂锁竞争问题,使 Redis 的命令执行模型更加简单高效。但是,这也意味着如果某个命令执行时间过长,就可能阻塞后续其他命令的处理。
因此,在大数据量场景下执行 KEYS 命令,可能会导致 Redis 无法及时响应其他客户端请求,从而造成请求阻塞、延迟升高,严重时甚至会影响整个 Redis 服务的可用性。
所以,KEYS 命令一般更适合在学习、调试,或者数据量较小的测试环境中使用。在生产环境中,如果确实需要遍历 Redis 中的 key,通常更推荐使用 SCAN 这类增量式遍历命令。SCAN 不会一次性遍历并返回所有 key,而是将完整的遍历过程拆分成多次迭代,从而降低单次命令对 Redis 服务端造成的阻塞风险。
EXISTS 命令详解:指定 key 的存在性判断
根据上文,我们已经认识了 KEYS 命令。KEYS 命令的作用是按照指定模式查询 Redis 中是否存在匹配的 key。如果存在符合模式的 key,Redis 就会将这些 key 返回给客户端。
但是,KEYS 命令有一个非常明显的问题:它的时间复杂度是 O(N)。也就是说,无论我们指定的匹配模式是什么,Redis 都需要遍历当前数据库中已经存在的所有 key,并逐个进行模式匹配。
而 Redis 的命令执行主路径通常由主线程处理。如果执行 KEYS * 这类命令时,Redis 中已经存储了大量 key,那么主线程就会在一段时间内忙于扫描和匹配 key,导致其他客户端请求无法及时得到响应,从而造成请求阻塞、延迟升高,影响 Redis 服务的整体性能。因此,KEYS 命令一般更适合在学习、调试或者数据量较小的测试环境中使用,不建议在生产环境中随意执行。
除了 KEYS 命令之外,Redis 还提供了另一个用于判断 key 是否存在的命令:EXISTS。
EXISTS 命令的基本语法如下:
bash
EXISTS key [key ...]
也就是说,EXISTS 后面可以跟一个 key,也可以跟多个 key。它的作用不是按照模式匹配 key,而是判断我们指定的 key 是否存在。
例如:
bash
EXISTS name
如果 name 这个 key 存在,Redis 会返回:
bash
(integer) 1
如果 name 这个 key 不存在,Redis 会返回:
bash
(integer) 0
需要注意的是,EXISTS 命令返回的不是简单的"存在"或者"不存在"这两种布尔结果,而是返回存在的 key 的数量 。这是因为 EXISTS 支持一次性查询多个 key。
例如:
bash
EXISTS name age city
如果其中 name 和 city 存在,而 age 不存在,那么 Redis 会返回:
bash
(integer) 2
这表示本次查询的多个 key 中,有 2 个 key 是存在的。
因此,EXISTS 和 KEYS 的定位并不相同。KEYS 是按照指定模式在当前数据库的所有 key 中进行匹配查询;和 KEYS 不同,EXISTS 并不会遍历 Redis 当前数据库中的所有 key,也不会进行模式匹配。它是根据我们明确传入的 key,到 Redis 内部字典结构中进行查找,从而判断这些 key 是否存在。因此,EXISTS 的开销主要取决于我们传入的 key 数量,而不是 Redis 当前数据库中所有 key 的总数量。
不过,EXISTS 也有一定局限性。如果我们一次性查询多个 key,它只会返回存在的 key 的数量,而不会告诉我们具体是哪几个 key 存在。例如,EXISTS name age city 返回 (integer) 2 时,我们只能知道这三个 key 中有两个存在,但无法仅凭这个返回值判断到底是 name、age、city 中的哪两个存在。
所以总结来说,EXISTS 更适合用于判断一个或多个明确指定的 key 是否存在;而 KEYS 更适合用于按照模式查看当前 Redis 中有哪些 key。二者虽然都和 key 查询有关,但使用场景和执行成本并不相同。
DEL 命令详解:删除 key 与缓存失效语义
认识了 KEYS 命令之后,接下来再来看 Redis 中用于删除数据的命令:DEL。
DEL 命令用于根据 key 删除 Redis 中已经存在的数据,其基本语法如下:
bash
DEL key [key ...]
也就是说,DEL 后面既可以跟一个 key,也可以跟多个 key。例如:
bash
DEL name
这条命令表示删除 name 这个 key 以及它对应的 value。如果想一次性删除多个 key,也可以写成:
bash
DEL name age city
当 Redis 服务端收到 DEL 请求之后,会根据指定的 key 在内部字典结构中查找对应的键值对。如果该 key 存在,就将整个 key-value 记录删除;如果该 key 不存在,则不会产生实际删除效果。DEL 命令最终会返回实际删除成功的 key 数量。
例如:
bash
DEL name age
如果 name 存在,而 age 不存在,那么 Redis 可能会返回:
bash
(integer) 1
这表示本次命令实际删除成功的 key 数量为 1。
这里还需要注意 Redis 在不同业务场景下的定位。如果 Redis 只是作为缓存层使用,那么删除 Redis 中的某个 key,通常不会造成业务数据本身的永久丢失。因为完整、可靠的数据仍然保存在 MySQL 中,Redis 中保存的只是部分热点数据的缓存副本。此时删除 Redis 中的数据,本质上更像是让缓存失效。后续请求如果再次访问该数据,就需要重新从 MySQL 中查询,并根据业务逻辑重新写入 Redis。
但是,如果某个业务直接将 Redis 作为主要数据存储,而不是单纯作为缓存使用,那么执行 DEL 命令就需要更加谨慎。因为此时 Redis 中的数据本身就是业务数据的一部分,如果删除之前没有持久化、备份或者恢复机制,就可能造成真实数据丢失。
EXPIRE 命令详解:过期时间设置与 Redis 过期删除策略
认识了 DEL 命令之后,接下来再来看另一个非常常用的命令:EXPIRE。
EXPIRE 命令的作用是为指定的 key 设置过期时间,也可以理解为给这个 key 设置一个生存时间,也就是 TTL。当超过指定时间之后,Redis 会认为这个 key 已经过期,后续会将该 key 以及它对应的 value 从 Redis 中删除。
有些读者虽然知道 EXPIRE 命令可以设置过期时间,但是可能还不清楚它在实际业务中的应用场景。这里可以通过几个例子来辅助理解。
例如,在外卖或者电商场景中,用户提交订单之后,如果长时间没有完成支付,系统通常会将订单状态从"待支付"修改为"已超时"或者"已取消"。在这个过程中,Redis 可以用来保存一些具有时效性的临时数据,例如待支付订单的缓存、支付状态标记,或者订单倒计时相关的信息。当这些 key 到达过期时间之后,Redis 中对应的缓存数据就会被删除。
不过这里需要注意,Redis 中的数据过期删除,并不意味着 MySQL 中的订单记录也会被直接删除。对于订单这种重要业务数据来说,MySQL 通常仍然会保留完整记录,只是将订单状态修改为"已超时"或者"已取消"。这样一来,用户后续仍然可以查询到历史订单记录。
类似地,验证码、登录状态、优惠券等数据,也都具有明显的时效性。例如验证码可能只允许 5 分钟内有效,优惠券可能只允许在指定时间范围内使用。对于这类数据,就可以通过 Redis 的过期时间机制来控制它们的有效期。
EXPIRE 命令的基本语法如下:
bash
EXPIRE key seconds
其中,key 表示要设置过期时间的目标 key,seconds 表示过期时间,单位是秒。例如:
bash
EXPIRE code:1001 300
这条命令表示为 code:1001 这个 key 设置 300 秒的生存时间。也就是说,从当前命令执行成功开始,300 秒之后该 key 就会过期。
需要注意的是,从命令使用角度来看,EXPIRE 后面传入的是一个时间段,而不是一个具体的时间点。但是在 Redis 内部,Redis 会根据当前时间计算出该 key 对应的具体过期时间点,并将这个过期时间记录下来。
EXPIRE 命令执行成功后会返回:
bash
(integer) 1
如果 key 不存在,或者设置条件不满足,则会返回:
bash
(integer) 0
也就是说,EXPIRE 对不存在的 key 设置过期时间时,并不会返回 (nil),而是返回整数 0。
默认情况下,如果某个 key 原本已经设置了过期时间,再次对它执行 EXPIRE 命令,就会使用新的过期时间覆盖原来的过期时间。例如:
bash
EXPIRE name 60
EXPIRE name 120
第一次命令表示让 name 在 60 秒后过期,第二次命令则会将它的过期时间重新设置为 120 秒。也就是说,后一次设置会覆盖前一次设置。
当然,EXPIRE 后面还可以携带一些选项,用来控制过期时间的设置行为。例如:
text
NX:只有 key 当前没有设置过期时间时,才设置新的过期时间
XX:只有 key 当前已经存在过期时间时,才更新过期时间
GT:只有新的过期时间大于当前过期时间时,才更新
LT:只有新的过期时间小于当前过期时间时,才更新
这些选项可以帮助我们在不同业务场景下,更精细地控制 key 的过期时间更新逻辑。
从命令本身来看,EXPIRE 的作用并不复杂。但是如果进一步思考它的底层实现,就会涉及 Redis 的过期删除策略。
很多读者可能会自然想到一种实现方式:Redis 内部是否可以维护一个全局的小根堆,用来保存所有设置了过期时间的 key?在这个小根堆中,堆顶保存最早过期的 key。这样 Redis 只需要定期检查堆顶元素,如果堆顶 key 已经过期,就将其删除,然后继续检查新的堆顶。
这种思路在定时器设计中是比较常见的。因为堆可以通过数组来模拟一棵完全二叉树,删除堆顶时,只需要将堆顶元素和数组最后一个元素交换,然后删除最后一个元素,再对新的堆顶向下调整即可。
但是,Redis 并没有简单地采用"全局小根堆 + 专门线程持续扫描堆顶"这种方式来管理 key 的过期时间。原因在于,Redis 中 key 的过期时间可能会被频繁新增、修改或者删除。如果使用全局小根堆,那么每次修改某个 key 的过期时间时,都需要定位它在堆中的位置,并重新调整堆结构。对于 Redis 这种追求高吞吐、低延迟的内存服务来说,精确维护所有 key 的过期顺序并不是最划算的选择。
Redis 实际采用的是惰性删除 和定期删除相结合的策略。
所谓惰性删除,就是当客户端访问某个 key 时,Redis 会先检查这个 key 是否已经过期。如果发现该 key 已经过期,Redis 就会将其删除,并向客户端表现为这个 key 不存在。
例如,当客户端执行:
bash
GET name
Redis 在返回数据之前,会先查询过期字典,判断 name 是否设置了过期时间以及是否已经过期。如果 name 已经过期,Redis 会先删除这个 key,然后向客户端返回空结果。
text
访问 key
↓
查询过期字典
↓
判断是否设置过期时间
↓
如果设置了,再判断当前时间是否超过过期时间
↓
过期则删除 key,并返回空结果
但是,仅仅依靠惰性删除是不够的。因为有些 key 过期之后,可能很长时间都不会再被访问。如果 Redis 只在访问时才删除过期 key,那么这些已经过期但没有被访问的 key 就会一直占用内存空间。
因此,Redis 还会采用定期删除策略。Redis 会额外维护一个过期字典,用来记录哪些 key 设置了过期时间。这个过期字典可以理解为一种 key-value 结构,其中 key 对应 Redis 中的某个键,value 保存该 key 的过期时间。Redis 会周期性地从这些设置了过期时间的 key 中抽取一部分样本进行检查,如果发现某些 key 已经过期,就将它们删除,从而逐步释放内存空间。
所以总结来说,Redis 并不是通过一个全局小根堆精确维护所有 key 的过期顺序,而是通过惰性删除 + 定期删除的方式来处理过期 key。惰性删除保证了被访问到的过期 key 能够及时清理,而定期删除则负责主动清理一部分已经过期但暂时没有被访问的 key。这样既能避免过期 key 长期占用大量内存,也能避免为了精确维护全局过期顺序而引入过高的维护成本。
需要注意的是,EXPIRE 命令设置的是秒级过期时间。如果希望设置毫秒级过期时间,可以使用 PEXPIRE 命令。PEXPIRE 和 EXPIRE 的作用基本一致,都是为指定 key 设置生存时间,只不过 PEXPIRE 后面传入的时间单位是毫秒。
例如:
bash
EXPIRE code:1001 300
表示 code:1001 在 300 秒后 过期。
bash
PEXPIRE code:1001 300000
表示 code:1001 在 300000 毫秒后 过期,也就是 300 秒后 过期。
另外,如果是在 SET 时顺便设置过期时间,也可以使用:
bash
SET key value EX seconds
SET key value PX milliseconds
其中 EX 表示秒级过期时间,PX 表示毫秒级过期时间。
TTL 命令详解:查询 key 的剩余生存时间
最后再来看 TTL 命令。
TTL 命令通常会和 EXPIRE 配合使用,用来查询某个 key 当前还剩多少时间过期。这里需要注意,Redis 中的 TTL 和 TCP/IP 协议报文中的 TTL 字段不是同一个概念。
在 TCP/IP 协议中,TTL 字段用于限制 IP 报文在网络中的最大存活范围,主要目的是防止报文因为路由环路而在网络中无限转发。每当报文经过一个路由器时,路由器都会将 TTL 的值减一;如果 TTL 被减到 0,报文就会被丢弃。
而 Redis 中的 TTL,表示的是某个 key 的剩余生存时间,也就是这个 key 距离过期还剩多少秒。
TTL 命令的基本语法如下:
bash
TTL key
如果该 key 已经设置了过期时间,那么 TTL 会返回它距离过期还剩多少秒;如果该 key 存在,但是没有设置过期时间,则返回 -1;如果该 key 不存在,则返回 -2。
例如:
bash
SET code:1001 8888
EXPIRE code:1001 300
TTL code:1001
此时,TTL code:1001 可能会返回:
bash
(integer) 295
这表示 code:1001 这个 key 距离过期还剩 295 秒。
需要注意的是,TTL 返回的是秒级剩余时间。如果希望查询毫秒级剩余时间,可以使用 PTTL 命令。PTTL 的作用和 TTL 类似,只不过它返回的是毫秒级的剩余生存时间。

结语
那么这就是本篇文章的全部内容,带你认识Redis基础命令,我会持续更新,希望你能够多多关注,如果本文有帮助到你的话,还请三连加关注,你的支持就是我创作的最大动力!
