3.通用命令【由浅入深-redis】 文章目录第一章 Redis 通用命令1.Redis 命令交互基础1.1 redis-cli 与 Redis 服务1.2 SET 与 GET2.Redis 通用命令2.1 KEYS按照模式查找 key2.2 EXISTS判断 key 是否存在2.3 DEL删除指定 key2.4 EXPIRE、PEXPIRE设置 key 的生命周期补充Redis 一个 key 只有一个过期时间EXPIRE 和 PEXPIRE 本质都是修改这个过期时间因此后设置的会覆盖先设置的不会累加。2.5 TTL、PTTL查询剩余生命周期3.Redis 过期 key 的实现思路3.1 定期删除与惰性删除3.2 为什么不为每个 key 创建一个定时器3.3 基于优先级队列的定时器3.4 时间轮3.5 Redis 的事件循环4.TYPE查询 value 的数据类型5.Redis 通用命令总结第一章 Redis 通用命令Redis 的数据最终都以key-value的形式组织。无论 value 使用 String、List、Set、Hash、ZSet 还是 Stream访问数据时都需要先定位 key因此 Redis 提供了一组与具体 value 类型无关的通用命令用于完成 key 的查找、存在性判断、删除、过期时间设置以及类型查询等操作。在学习这些命令之前需要先理解 Redis 命令的执行方式以及最基本的键值模型。Redis 命令大小写不敏感Redis 存储的数据大小写敏感。字符串可以不加引号也可以使用单引号或双引号表示但引号只是客户端解析参数的方式不会作为实际数据保存。只有字符串包含空格等特殊字符时SET name hello world通常才需要加引号。Redis 内部通过 哈希表dict存储 key1.Redis 命令交互基础1.1 redis-cli 与 Redis 服务Redis 采用客户端与服务器结构。redis-server负责保存和处理数据redis-cli则是 Redis 自带的命令行客户端。进入redis-cli后输入的命令会由客户端发送给 Redis 服务器执行再将服务器返回的结果显示出来。redis-cli连接本机默认 Redis 服务后会看到类似下面的提示符127.0.0.1:6379其中127.0.0.1表示当前连接的 Redis 服务器地址6379是 Redis 默认端口。之后输入的SET、GET、KEYS、DEL等命令本质上都需要经过一次客户端与服务器之间的请求和响应。Redis 的命令数量很多没有必要依靠死记硬背掌握全部参数。实际使用时一方面需要熟悉高频命令另一方面需要能够根据命令文档确认语法、参数、返回值以及时间复杂度。对于 Redis 这类基础组件而言理解命令解决什么问题比单纯记住命令名称更加重要。1.2 SET 与 GET理解 Redis 通用命令之前可以先通过SET和GET建立最基本的键值模型。SET用于写入一个 String 类型的键值对SET key value例如127.0.0.1:6379SET key1 value1 OK127.0.0.1:6379SET key2 value2 OKRedis 中的key 本质上都是字符串。对于SET命令来说value 同样按照字符串或字节序列进行存储因此执行上述操作后可以理解为 Redis 中形成了如下映射关系key1 -value1 key2 -value2GET则根据 key 获取对应的 String value127.0.0.1:6379GET key1value1如果查询的 key 不存在Redis 不会返回一个普通字符串而是返回空值在redis-cli中通常表现为(nil)127.0.0.1:6379GET not-exist(nil)这里的(nil)表示这个 key 没有对应的值不要把它理解成字符串nil。后续很多 Redis 命令都会涉及“不存在的 key”理解这一点非常重要。2.Redis 通用命令Redis 的 value 可以拥有不同的数据类型而 key 的组织方式始终一致因此有一部分命令并不关心 value 到底是 String、List 还是 Hash只围绕 key 本身工作。这类命令就是 Redis 的通用命令。2.1 KEYS按照模式查找 keyKEYS用于查找当前数据库中符合指定模式的 keyKEYS pattern#KEYS 命令只能接受一个匹配模式不能同时写多个 pattern。这里的pattern不是普通字符串而是支持通配符的匹配模式。例如数据库中存在hello hallo hbllo hllo heeeelloRedis 常见的模式匹配规则如下模式含义示例*匹配任意数量字符包括 0 个字符h*llo?匹配任意一个字符h?llo[ae]匹配集合中的任意一个字符h[ae]llo[^e]匹配除指定字符以外的一个字符h[^e]llo[a-b]匹配指定范围内的一个字符h[a-b]llo例如127.0.0.1:6379KEYS h?llo1)hello2)hallo3)hbllo其中?只能匹配一个字符因此hllo和heeeello都不符合要求。而127.0.0.1:6379KEYS h*llo其中*可以匹配任意长度的字符因此既可以匹配hllo也可以匹配hello、heeeello。方括号用于进一步限定单个字符。例如KEYS h[ae]llo只允许对应位置出现a或e因此可以匹配hallo和hello。如果使用KEYS h[^e]llo则表示该位置不能是e因此hello不再匹配。KEYS使用非常直观但它也是 Redis 中需要重点注意的命令。其时间复杂度为O(N)其中 N 是当前数据库中的 key 数量。执行KEYS *时需要遍历整个数据库中的 key当数据库只有几十个或者几百个 key 时通常感觉不到明显影响但如果线上 Redis 中存在几十万甚至上百万个 key全量遍历就可能占用较长时间。Redis 的核心命令执行过程是串行推进的。一个耗时较长的命令没有执行完成时其他客户端请求也会受到影响。因此在生产环境中直接执行KEYS *可能把原本非常快的 Redis 变成一个明显的阻塞点。生产环境并不是开发机器的另一种称呼。软件通常会经历办公环境、开发环境、测试环境以及线上生产环境。办公环境主要承担日常工作开发环境用于编码、编译和调试可能是开发者本机也可能是公司内部服务器测试环境用于验证程序是否符合预期生产环境则是真正部署业务、直接承载用户请求的环境。开发和测试环境中的一次慢操作通常影响有限而生产环境中的阻塞操作可能直接影响真实用户因此线上 Redis 对危险命令必须更加谨慎。如果线上确实需要遍历大量 key不应简单依赖一次性全量扫描而应采用增量遍历一类的方案例如SCAN避免一次操作长时间占用 Redis。2.2 EXISTS判断 key 是否存在EXISTS用于判断一个或多个 key 是否存在EXISTS key[key...]对于单个 key存在返回1不存在返回0127.0.0.1:6379EXISTS hello(integer)1EXISTS也可以一次判断多个 key返回值表示参数中存在的 key 的数量127.0.0.1:6379EXISTS hello hallo aaa(integer)2KEYS 是搜索命令需要遍历整个 Redis 数据库EXISTS 是查询命令通过 Redis 内部哈希表直接定位 key不需要遍历整个数据库。如果hello和hallo存在而aaa不存在结果就是2。需要注意Redis 统计的是参数中的存在次数因此同一个已经存在的 key 被重复传入时也会被重复统计127.0.0.1:6379EXISTS hello hello(integer)2对于单个 key 的判断可以视为常数级查找一次传入多个 key 时则需要依次检查这些参数因此整体开销会随着参数数量增长。Redis 本身是客户端—服务器程序。无论通过redis-cli还是 Java、C 等语言的 Redis 客户端底层最终都要将命令发送到 Redis 服务端再等待服务器返回结果。客户端库中看到的各种 API本质上就是对 Redis 命令以及网络通信过程进行了一层封装。这也意味着程序性能不能只考虑服务器内部执行一条命令需要多少 CPU 时间还需要考虑网络 I/O 和请求次数。如果一条 Redis 命令本身支持一次操作多个 key在场景允许的情况下使用批量参数往往可以减少多次独立请求带来的网络往返开销。不过批量操作同样不能无限扩大一次处理的数据过多仍然可能让单条命令执行时间变长。2.3 DEL删除指定 key1.DEL 和 EXISTS 类似不会遍历整个 Redis。它也是通过 key 直接定位删除。2.FLUSHALL 是 Redis 实例级别的数据清理命令它会清空所有逻辑数据库中的所有 key。默认同步执行数据量大时会阻塞 Redis因此生产环境通常谨慎使用必要时使用 FLUSHALL ASYNC 降低主线程阻塞风险。DEL用于删除一个或多个 keyDEL key[key...]返回值表示本次实际删除的 key 数量。不存在的 key 不会报错也不会计入删除数量127.0.0.1:6379DEL hello(integer)1127.0.0.1:6379DEL hello hallo aaa(integer)2第二条命令即使传入三个 key只要实际只有两个存在最终返回值就是2。删除操作本身并不复杂但在生产环境中需要非常谨慎。数据库中的删除一直属于高风险操作例如关系型数据库中的DROP DATABASE、DROP TABLE、DELETERedis 中的DEL同样意味着对应数据被直接移除而不是简单地“隐藏”起来等待恢复。Redis 经常被作为缓存使用。在这种架构中真正完整的数据可能保存在 MySQL 等持久化数据库中即使某些 Redis key 被误删业务仍有机会重新从数据库加载数据只是可能造成缓存失效、数据库压力上升等问题。但是如果 Redis 本身承担的是主要数据存储、消息队列或其他无法简单重建的数据职责错误的DEL就可能直接造成业务数据丢失。此外不能简单地把所有DEL都理解成完全没有成本。删除一个普通 String key 通常很快但如果 value 本身是包含大量元素的 List、Set、Hash 或 ZSet释放其内部数据同样需要时间。因此删除 key 的风险不仅来自“删错数据”也来自删除超大对象可能带来的执行开销。2.4 EXPIRE、PEXPIRE设置 key 的生命周期很多数据从业务角度就不应该永久存在。例如验证码只应该在几分钟内有效某些优惠信息只在指定时间段内有效缓存数据也通常需要在一段时间后重新加载。Redis 因此允许直接为 key 设置过期时间。EXPIRE使用秒作为时间单位EXPIRE key seconds例如让code:1001在 300 秒后过期127.0.0.1:6379SET code:10019527OK127.0.0.1:6379EXPIRE code:1001300(integer)1设置成功返回1如果指定的 key 根本不存在则返回0。如果需要更高的时间精度可以使用PEXPIRE它以毫秒为单位PEXPIRE key milliseconds因此PEXPIRE task3000表示让task在大约 3000 毫秒后失效。过期机制除了缓存和验证码之外也经常用于具有生命周期的数据。例如某些临时状态只允许存在几分钟时间一到便自动失效实现分布式锁时也通常需要为锁设置合理的过期时间避免持有锁的进程异常退出后锁永远无法释放。这里需要理解一个关键概念Redis 中设置过期时间是把生命周期附加到 key 上而不是附加到 value 的某一部分。一旦 key 到期这个 key 以及它对应的整个 value 都会失效。补充Redis 一个 key 只有一个过期时间EXPIRE 和 PEXPIRE 本质都是修改这个过期时间因此后设置的会覆盖先设置的不会累加。操作是否覆盖 TTL详细说明示例EXPIRE key seconds✅ 覆盖给 key 设置过期时间。如果原来有 TTL则直接替换没有 TTL则新增 TTLEXPIRE name 100PEXPIRE key milliseconds✅ 覆盖和 EXPIRE 一样只是单位是毫秒。如果之前是秒级 TTL也会被替换PEXPIRE name 5000EXPIRE → PEXPIRE✅ 覆盖后执行的命令覆盖前面的 TTL不会叠加先EXPIRE name 100再PEXPIRE name 5000最终 5 秒后过期PEXPIRE → EXPIRE✅ 覆盖同理后面的 EXPIRE 覆盖前面的 PEXPIRE先PEXPIRE name 5000再EXPIRE name 100最终 100 秒后过期SET key value✅ 删除 TTL重新设置 value 时会清除原来的过期时间key 变成永久存在SET name Jack后 TTL 变为-1DEL key✅ 删除 TTL删除整个 keyvalue 和 TTL 都不存在DEL namePERSIST key✅ 删除 TTL保留 key 和 value只删除过期时间TTL 变为永久INCR key❌ 不影响 TTL修改 value但不会删除过期时间计数器常用HSET key field value❌ 不影响 TTL修改 Hash 内部数据不影响 key 的 TTL用户信息缓存常用LPUSH key value❌ 不影响 TTL修改 List 内容不影响 key 的 TTL消息队列常用2.5 TTL、PTTL查询剩余生命周期设置过期时间后可以使用TTL查询 key 还剩多少秒TTL key例如127.0.0.1:6379TTL code:1001(integer)287表示该 key 大约还剩 287 秒。如果需要毫秒级精度则使用PTTLPTTL keyTTL是Time To Live的缩写即剩余生存时间。网络协议中也存在 TTL 这个名称例如 IP 协议中的 TTL但两者具体语义并不完全相同在 Redis 中它非常直接地表示 key 距离失效还剩多长时间。至此一个带生命周期的 Redis key 可以形成完整过程SET session:1001 xxx EXPIRE session:1001300TTL session:1001 GET session:1001先创建数据再设置生命周期之后可以随时查询剩余时间当生命周期结束后再访问这个 key 时就会表现为不存在。3.Redis 过期 key 的实现思路给 key 设置过期时间看起来只是执行了一条EXPIRE命令但真正需要解决的问题是Redis 如何知道某个 key 已经过期又应该在什么时候把它删除一种最直接的想法是不断扫描所有带过期时间的 key检查当前时间是否已经超过它们的截止时间。但如果 Redis 中存在大量 key这种方式会持续消耗大量 CPU因此 Redis 不会简单地不停遍历整个数据库。Redis 的过期处理主要结合了定期删除和惰性删除两种思路。3.1 定期删除与惰性删除定期删除的核心思想不是每次检查所有 key而是在一定时间间隔内抽取一部分带过期时间的 key 进行检查将其中已经过期的数据删除。这样能够主动回收过期数据又不需要每一次都扫描完整数据库。惰性删除则将检查时机推迟到 key 被访问的时候。一个 key 即使已经超过过期时间也不要求在时间到达的那一瞬间立刻执行删除后续客户端再次访问这个 key 时Redis 发现它已经过期就将其删除并按照 key 不存在进行处理。两种方式结合后可以在 CPU 与内存之间取得平衡定期删除负责主动清理一部分过期数据惰性删除负责保证客户端不会读取到已经过期的数据。如果只采用定期删除就必须决定多久扫描一次以及一次检查多少 key。扫描过于频繁会浪费 CPU扫描过少又可能让大量已经过期的数据长期占用内存如果只采用惰性删除一些过期后再也没有被访问的 key 就可能长时间停留在内存中。因此两种策略互相补充。这也是为什么一个 key 的逻辑过期时间到达后并不意味着 Redis 必须在那个时间点精确地启动一个独立线程把它删除。“已经过期”与“物理内存已经立即释放”是两个不同概念。Redis 在内存不足时还涉及内存淘汰机制但内存淘汰和 key 的过期删除解决的是两个不同问题前者主要解决内存容量压力后者解决数据生命周期。Redis 的定期删除是在内部时间事件循环serverCron中周期执行的通过随机抽样检查过期字典中的 key 并删除惰性删除是在客户端访问 key 时检查 TTL如果发现过期再删除。两者结合避免了全量扫描导致性能下降同时减少过期 key 长时间占用内存的问题。3.2 为什么不为每个 key 创建一个定时器既然已经知道每个 key 的过期时间看起来还可以为每一个 key 单独创建一个定时任务时间一到立即删除这个 key。少量任务时这种设计很直观但如果存在几十万甚至几百万个带过期时间的 key就意味着需要维护大量定时任务。因此定时任务系统通常不会真的简单地“一个任务创建一个独立线程”而是借助统一的数据结构管理大量任务。比较典型的实现思路包括基于优先级队列的定时器和时间轮。需要注意这两种方案是通用的高效定时器设计思路并不代表 Redis 对过期 key 就是直接按照这两种结构实现的。理解它们主要是为了理解“大量定时任务应该如何统一调度”。3.3 基于优先级队列的定时器普通队列强调先进先出而优先级队列会根据元素的优先级决定谁先出队。对于定时任务而言最自然的优先级就是任务的到期时间过期时间越早优先级越高。假设有三个 keykey1 -12:00 key2 -13:00 key3 -14:00如果按照过期时间组织成优先级队列那么队首始终是最早需要处理的key1。定时线程只需要关注队首元素而不需要遍历所有任务。如果当前是 11:00而队首任务 12:00 才到期那么线程完全没有必要持续高速检查可以根据当前时间与 12:00 之间的时间差进入等待到达时间后再唤醒并处理任务。删除key1后新的队首变成key2再按照它的到期时间继续等待。这种设计把“大量任务的逐个扫描”转换成了“始终关注最近到期的任务”可以明显减少无意义的检查。但它还必须处理一个问题假设线程原本正在等待 12:00 的任务此时突然新增了一个 11:30 就要执行的任务那么原来的等待时间已经不再正确。系统需要能够唤醒等待线程重新计算下一个真正应该执行的任务。3.4 时间轮另一类常见定时器实现是时间轮。它可以想象成一个不断旋转的表盘整个圆环被划分成若干个时间槽每个槽负责某一段时间范围。每个槽上可以挂一个链表链表中的节点代表需要执行的任务。任务节点中保存需要执行的操作以及相应参数在不同语言中可以表现为函数指针、回调对象或任务对象。假设时间轮每隔100ms向前移动一个槽0ms → 100ms → 200ms → 300ms → ...如果某个任务需要在3000ms后执行就可以按照时间差计算它应该进入哪个槽。时间轮的指针每经过一个槽就处理该槽链表中的到期任务。这种方式不要求系统不断遍历所有定时任务而是随着时间推进只处理当前位置对应的任务集合。时间槽的数量、每个槽代表的时间以及是否需要多级时间轮都需要根据实际场景设计。时间精度越高槽通常越密集能够表示的时间范围越大所需要管理的结构也会更加复杂。基于优先级队列的定时器和时间轮都是大量定时任务中常见的设计方案但Redis 的 key 过期机制并不是简单地为每个 key 创建一个这样的独立定时任务。3.5 Redis 的事件循环Redis 源码中一个非常核心的机制是事件循环。Redis 不断通过事件循环处理客户端请求、网络事件以及周期性任务。在 key 过期问题上Redis 更倾向于把过期检查作为服务器周期性工作的一部分再结合访问 key 时的惰性检查完成数据清理而不是为每一个带 TTL 的 key 单独维护一个定时器。这种设计本质上仍然是在做取舍如果追求“某个 key 到期的那一毫秒就必须立即从内存删除”会引入更多任务管理和 CPU 开销Redis 更关注的是在保证逻辑过期正确性的同时以合理成本完成内存回收。因此对于应用程序而言只需要关心 key 到期以后不能继续作为有效数据读取并不应该依赖“到期瞬间物理内存一定已经释放”这一假设。4.TYPE查询 value 的数据类型Redis 中所有 key 都可以看作字符串但 key 对应的 value 可以使用不同的数据类型。TYPE命令用于查询某个 key 当前对应的 value 类型TYPE key常见返回结果包括string、list、set、zset、hash、stream。如果 key 不存在则返回none例如127.0.0.1:6379SET name redis OK127.0.0.1:6379TYPE name stringTYPE的时间复杂度为O(1)因为 Redis 已经知道一个 key 当前关联的对象类型不需要遍历 value 内容才能完成判断。这里需要区分两个概念。Redis 对外提供的数据结构除了 String、List、Set、Hash、Sorted Set、Stream 之外还有 Geospatial、HyperLogLog、Bitmap、Bitfield 等面向特殊场景的能力但这些结构中的一部分实际上建立在基础数据类型之上。例如 Bitmap、Bitfield 本质上围绕 String 进行位操作Geospatial 建立在 Sorted Set 能力之上。因此TYPE更关注一个 key 在 Redis 内部属于哪种核心 value 类型而不是它当前被业务拿来完成什么功能。不同数据类型的命令差异很大。例如 String 有自己的读写操作List 围绕列表两端进行操作Hash 围绕字段和值组织数据ZSet 则同时维护成员与分数。因此在进入具体数据结构之前首先掌握这些不依赖 value 类型的通用命令可以建立完整的 Redis key 管理基础。5.Redis 通用命令总结Redis 通用命令的核心不是操作某一种具体数据结构而是围绕key 本身进行管理。当前涉及的主要命令可以归纳如下命令作用关键特点时间复杂度KEYS pattern查找符合模式的 key遍历当前数据库所有 key生产环境尤其需要警惕全量扫描O(N)N 为数据库中 key 的数量EXISTS key [key ...]判断 key 是否存在根据 key 直接从哈希表查找返回存在的 key 数量O(M)M 为传入的 key 数量单个 key 时 O(1)DEL key [key ...]删除 key根据 key 定位删除返回实际删除数量O(M)M 为删除的 key 数量删除单个普通 key 时 O(1)大对象释放可能耗时EXPIRE key seconds设置秒级过期时间修改 key 的过期时间已有 TTL 会被覆盖O(1)PEXPIRE key milliseconds设置毫秒级过期时间EXPIRE的毫秒版本已有 TTL 会被覆盖O(1)TTL key查询剩余秒数返回 key 剩余生存时间Time To LiveO(1)PTTL key查询剩余毫秒数TTL的毫秒版本O(1)TYPE key查询 value 类型根据 key 直接获取对象类型O(1)其中最需要建立的三个认识是KEYS虽然简单但全量扫描存在明显的线上风险DEL是实际删除操作数据职责不同删除风险也不同过期时间并不是为每个 key 建立一个独立定时器而是通过过期检查机制在正确性、CPU 和内存之间进行权衡。掌握这些通用命令后Redis 的整体结构就已经比较清晰key 使用统一方式管理而 value 根据业务需要采用不同的数据结构。接下来围绕 String、List、Hash、Set、ZSet 等具体类型展开时重点就会从“如何管理 key”转向“不同 value 结构如何组织和操作数据”。