☰
Redis命令详解与实战:从性能代价到分布式锁与缓存治理
2026/10/1 17:44:37 网站建设 项目流程

前阵子帮一个朋友排查线上事故,他团队里有人顺手敲了一条KEYS user:*,结果那台 Redis 的 CPU 瞬间飙满,整个服务的缓存请求全被拖住。Redis 的命令看起来都挺简单,随手一条 SET、GET 就能干活,但正因为简单,很多人从来没想过每条命令背后隐藏着什么代价。Redis 命令详解这个话题,如果只是罗列一堆语法,其实帮不到任何人;真正值得拆解的是:每条命令在什么业务场景下该用、不该用、用了之后会有什么连锁反应。这篇文章我不打算给你一份可以背的 API 大全,而是用踩过坑的视角,把 Redis 的通用命令、五大数据类型命令、分布式锁、线上排障命令背后的逻辑讲透,顺便把几个面试高频点也串进去。适合刚学 Redis 的人打基础,也适合写过一段时间但没系统梳理过命令体系的人查漏补缺。

1. 从一条命令的网络旅程开始:理解 Redis 命令为什么“轻”

1.1 一条 SET 命令在网络上长什么样:RESP 协议的门道

很多人用 Redis 几年都没看过协议层。其实 Redis 客户端和服务器之间走的是 RESP(REdis Serialization Protocol),简单说就是一段带长度标识的纯文本。

我用 telnet 直接连接 Redis 的 6379 端口,手动敲命令给你看:

$ telnet 192.168.1.10 6379 SET user:1:name "zhang" +OK

你会看到服务器直接返回+OK。换成原始报文,客户端实际发送的是:

*3\r\n$3\r\nSET\r\n$11\r\nuser:1:name\r\n$5\r\nzhang\r\n

*3表示后面有 3 个参数,$3表示接下来的参数长度是 3 个字节。这套协议的好处是解析快、可读性强,也方便各种语言实现客户端。日常开发里这些细节都由 redis-py、Jedis 这些库封装了,但理解 RESP 有个实际价值:当你需要排查“为什么批量命令这么慢”时,能明白每次命令都至少经历一次网络往返(RTT),也就理解了 Pipeline 和 Lua 脚本为什么能提速。

提示:telnet 是明文交互,只适合本机或测试环境做协议嗅探,生产环境不要用这条路径连 Redis,也千万别在 telnet 会话里顺手敲生产命令。

1.2 单线程执行模型:一条慢命令拖垮整个实例的原因

Redis 命令执行器在很长一段时间里是单线程的,Redis 6.0 之后把网络读写放到了多线程,但命令的真正执行仍然集中在主线程。你可以把它想象成单车道收费站:每一辆车(命令)都要从入口依次通过,只要有一辆车抛锚(慢命令),后面所有车都得堵着。

所以 Redis 命令的执行复杂度不是小事。KEYS是 O(N),N 是整个键空间的大小;SMEMBERS在集合元素多的时候也是 O(N);LRANGE取一个超长 List 的全部数据同样是 O(N)。这些命令一旦在高峰期被触发,你的 p99 延迟会瞬间恶化,数据库流量也可能跟着被冲垮。

我遇到过不止一次因为KEYS误用导致的服务雪崩,后面会专门讲替代方案。这里先记住一个原则:能走 SCAN 系列命令的,绝不用全量扫描命令;能走 O(1) 或 O(logN) 的,绝不用 O(N) 的命令。

1.3 键空间与通用命令:先学会“活下来”再谈优化

不管你用哪种数据类型,下面这些通用命令永远是基础操作,我做了张速查表:

命令复杂度作用注意事项
TYPE keyO(1)查看键的数据类型排障第一步,先确认结构再操作
EXISTS keyO(1)判断键是否存在一次判断多个键
EXPIRE key secondsO(1)设置过期时间键不存在时返回 0
TTL keyO(1)查看剩余过期时间-1 表示永不过期,-2 表示不存在
DEL keyO(1) 平均同步删除键大 Key 慎用,会阻塞
UNLINK keyO(1)异步删除键4.0+ 推荐,尤其大 Key
SCAN cursorO(1) 每批游标遍历键空间生产取键用这个,别用 KEYS
COPY src dstO(1) 平均复制键可以带 DB 参数跨库复制

SCAN和KEYS的差别值得多说两句。KEYS是一次性返回所有匹配的键,在键多的时候会阻塞主线程;SCAN则是基于游标的增量式遍历,每次返回一部分结果,用返回的游标继续迭代,直到游标回到 0:

redis-cli --scan --pattern 'user:*' --count 500

注意 SCAN 是增量遍历,遍历过程中如果有键新增或删除,可能会出现重复或漏取的情况,这是设计如此,不是为了精确快照。日常做清理、排查、迁移时用它足够。

删除键这块,DEL是同步删除,遇到大 List、大 Set 时删除动作本身可能就要阻塞几十甚至上百毫秒。Redis 4.0 引入UNLINK,把释放内存的操作放到后台异步线程执行,返回很快。生产环境我要删大键时,第一选择永远是UNLINK。

2. 五大数据类型命令拆解:先想业务,再选命令

2.1 STRING:缓存、计数、限流的基座

STRING 是最直观的类型,但它的几个命令用法其实有讲究。SET除了基本赋值,还支持 EX、PX、NX、XX 这些参数组合。我最常用的是:

  • SET key value EX 3600:带过期时间的缓存
  • SET key value NX EX 3600:只有键不存在时才设置,同时带过期时间,这是分布式锁的原子基础
  • GETSET key new_value:取出旧值并写入新值,适合做“读取并更新”的原子操作

计数场景,INCR和INCRBY是原子自增,底层就是单命令操作,不用加锁。比如文章阅读量:

INCR article:read:10086 INCRBY article:read:10086 10

再比如验证码场景,SETEX一把梭:

SETEX sms:code:13800138000 300 "4821"

这里过期时间 300 秒,用户重新获取验证码时直接SETEX覆盖旧值,既省了一次删旧键的请求,也保证了一定有效期。

字符串存对象时,最常见的做法是先把对象序列化成 JSON 再写入。序列化方案的选择会影响内存和速度:JSON 可读性强,Java 的 JDK 序列化有安全漏洞风险,Protobuf 空间小但调试不方便。我的建议是:内部缓存用 JSON 足够,追求极致性能再上 Protobuf。关于 Redis 序列化这个热词,后面会在缓存治理部分再补充。

2.2 HASH:对象模型的正确姿势

HASH 类型适合存“一条记录多个字段”的场景,典型如用户资料、购物车、订单状态。

HSET user:1001 name "zhang" age 25 city "hangzhou" HGET user:1001 name HMSET user:1002 name "li" age 30 city "beijing" HMGET user:1002 name city HINCRBY cart:1001 sku:8899 2

用 HASH 而不是 STRING 存对象,最大的好处是字段级读写。比如只更新用户的 age 字段,HASH 只需要发一条HSET,STRING 方案里你得先GET整个 JSON、反序列化、改字段、再序列化写回去。流量大时这个差异很致命。

但 HASH 也不是没有坑。HGETALL会把所有字段和值一次性返回,如果这个 HASH 有上万个字段,就可能造成网络响应体过大和 Redis 阻塞。替代方案是用HSCAN分批取:

HSCAN user:1001 0 COUNT 100

底层编码上,字段少、值小的 HASH 会使用紧凑的 listpack 结构,内存占用很低;字段数增长到一定阈值后才会转为 hashtable。这也是面试常考的点:为什么小 HASH 省内存。

2.3 LIST:队列、时间线、消息缓冲

LIST 的本质是链表,两端操作都是 O(1)。Redis 早期的消息队列就靠它实现:

LPUSH task:queue job:001 BRPOP task:queue 5

BRPOP是阻塞读,没消息时一直挂起等待,超时时间 5 秒。相比轮询,阻塞读能极大减少空轮询的 RTT 和 CPU 浪费。

做一个简单的“最近访问记录”,LPUSH新元素到左侧,然后用LTRIM只保留前 100 条,一举两得:

LPUSH recent:viewed:u1001 sku:8899 LTRIM recent:viewed:u1001 0 99

LTRIM的意思是从第 0 个元素截断到第 99 个,之外的直接删掉。这个组合比“先 LPUSH 再单独删除超长部分”要优雅得多,也避免列表无限增长。

分页拉取时间线用LRANGE:

LRANGE timeline:user:1001 0 9

注意LRANGE是 O(N),N 是区间长度。取前 10 条没问题,取整个大列表就有风险。需要拿一批数据时,一定先想清楚列表规模,再决定区间大小。

2.4 SET:去重、标签、抽奖

SET 的特性是无序、元素唯一。做抽奖就是SPOP,它随机弹出并移除一个元素,保证已抽中的人不会重复中奖:

SADD lottery:20250601 u1001 u1002 u1003 SPOP lottery:20250601

SRANDMEMBER则只随机返回但不删除,适合做“随机推荐”和“展示”场景。用错了就是逻辑 bug:该移除的没移除,该保留的却被删掉。

SINTER 求交集很适合做“共同关注”:

SADD follow:u1001 userA userB userC SADD follow:u1002 userB userC userD SINTER follow:u1001 follow:u1002

返回userB userC。类似地,SUNION 求并集可以用于合并标签,SDIFF 求差集可以用于“我关注了但他没关注我”的关系分析。

集合大了以后,SMEMBERS这条命令就是隐形杀手,它会把整个集合一次性取出来。生产环境想看集合内容,请用SSCAN分批遍历:

SSCAN bigset:u1001 0 COUNT 100

顺便记住:SET 能去重计数,但元素总量是 2^32 - 1 的量级,超过这个上限就得换 HyperLogLog 方案,这是另一个话题了。

2.5 ZSET:排行榜与优先级队列

ZSET 是带分数的有序集合,底层是跳跃表加紧凑结构。排行榜场景基本是 ZSET 的天下:

ZADD sales:rank 100 sku:8899 ZINCRBY sales:rank 1 sku:8899 ZREVRANGE sales:rank 0 9 WITHSCORES

ZINCRBY用一个命令完成“分数加一”,天然原子,不用先取分再写回。ZREVRANGE按分数从高到低返回 Top10,还带分数值。

再看延迟队列这种高级玩法:把任务的执行时间戳当作 score,成员是任务内容,然后反复用ZRANGEBYSCORE取出当前时间之前到期任务:

ZADD delay:queue 1735000000 task:0001 ZRANGEBYSCORE delay:queue -inf 1735000000

谁先到期,谁的 score 最小,范围查询一下就拿到了。这种结构比轮询数据库删数据优雅得多。

ZSET 底层为什么用跳跃表而不是红黑树?我没法避开这个面试高频点,简单说说:红黑树实现复杂,范围查询(比如分数区间内排序列举)需要额外线索结构;跳跃表实现直观、查询和插入都是 O(logN),对范围遍历特别友好,而且并发写的时候更容易调整。Redis 追求简单高效,跳跃表显然是更务实的选择。

3. 把命令组合起来才是真本事:分布式锁、事务与管道

3.1 分布式锁的命令演进:看起来简单为什么总出事

“Redis 分布式锁”是每个后端程序员都会接触到的热词。早期写法是SETNX lock:order 1加锁,然后单独发一条EXPIRE lock:order 300设置过期时间。问题在于这两条命令不是原子的——如果进程在加锁之后、设过期之前崩溃了,这把锁就永远不释放。

Redis 2.6.12 之后的SET已经支持 NX 和 EX 组合,一条命令搞定:

SET lock:order "order_9527" NX EX 30000

NX 表示键不存在才设置,EX 表示过期时间 30 秒。这一步解决了“加锁与过期”的原子性。

真正容易踩坑的是解锁。很多人图省事直接DEL lock:order,但想象这个时序:A 拿到锁后业务执行超过 30 秒,锁自动过期;B 拿到锁开始执行;这时 A 终于走到删除锁的逻辑,一个DEL把 B 的锁删了,业务就乱套了。

解锁必须检查“这个锁是不是我加的”,并且保证检查和删除是原子的:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

用EVAL执行这段 Lua,保证“先比较再删除”中间不会被其他命令插入。解锁脚本要传进去一个唯一的随机值(比如 UUID),只删自己持有的那把锁。如果你用的 Redisson 这类客户端,它内部也是这么处理的,还内置了“看门狗”自动续期机制。

提示:RedLock 这种多节点锁方案在业内争议很大,大部分业务用单节点 Redis 锁配合续期就够用了。不要把分布式锁当成万能的强一致方案,实在需要强一致就换 etcd 或数据库事务。

3.2 MULTI/EXEC 与 WATCH:Redis 的乐观锁机制

Redis 事务和关系型数据库事务不太一样。MULTI开启事务,之后的命令先进入队列,EXEC时一次性执行,DISCARD丢弃队列。但要注意:Redis 事务只是“批量执行命令”,没有回滚。如果第 2 条命令语法错误,前面成功、后面照常,这是设计上的取舍。

乐观锁的玩法靠WATCH。经典案例是库存扣减:

WATCH stock:sku:10086 val = GET stock:sku:10086 if val > 0 then MULTI DECR stock:sku:10086 EXEC else UNWATCH end

WATCH监控一个键,如果从WATCH到EXEC之间这个键被其他客户端改动,EXEC会返回 nil,事务不执行。这时候业务层需要重新读库存、重新尝试。这套机制适合“先读再写”且写错代价高的场景,避免并发扣成负数。

但必须承认,MULTI/EXEC的实用性在日常业务里不如 Lua 脚本强。事务里的命令还是要有 RTT 传输,而 Lua 脚本可以直接在 Redis 服务器端完成读和写逻辑判断,原子性更强。

3.3 Pipeline 与 Lua:减少网络 RTT 的两条路径

Redis 再快,也扛不住一次业务操作发几十条命令、每条都在本机和 Redis 之间打个来回。100 条命令就是 100 个 RTT,延迟再低也架不住量多。

Pipeline 的原理是把多条命令打包到一次网络请求里发给 Redis,执行完再一次性返回结果。客户端代码里可以这样:

redis-cli --pipe < commands.txt

导入几十万条初始化数据时,--pipe能比逐条循环快上几倍到几十倍。但要理解:Pipeline 不是原子操作,它只是把网络往返合并了,命令之间如果有依赖关系,中途失败会导致部分执行。所以 Pipeline 适合批量写入无依赖的数据,比如缓存预热。

Lua 脚本则完全不同。脚本作为一个整体被 Redis 原子执行,执行期间其他客户端的命令都会被阻塞等待:

local stock = tonumber(redis.call("GET", KEYS[1])) if stock > 0 then redis.call("DECR", KEYS[1]) return 1 end return 0
EVAL "上面的脚本" 1 stock:sku:10086

推荐的做法是用SCRIPT LOAD先把脚本加载到 Redis,拿到 sha1 摘要,之后用EVALSHA执行,省去每次传脚本内容的网络开销。这条路径是很多高并发场景下的核心优化手段,比 WATCH + MULTI 的组合更简单、更原子、更快。

4. 线上排障用的命令:出问题时先敲哪几条

4.1 慢查询定位:SLOWLOG 是第一个入口

Redis 会把执行时间超过阈值的命令记进慢查询日志,默认阈值是 10000 微秒(10 毫秒)。先看配置:

CONFIG GET slowlog-log-slower-than

再看最近慢命令:

SLOWLOG GET 100

这个输出里能看到耗时、命令参数、客户端来源。有一次线上实例阻塞,我用SLOWLOG GET抓到的元凶是一条对超大 List 执行DEL的命令,原因很简单:有人用DEL删了一个上百万元素的 List,删除本身导致主线程卡了几秒。后续方案就是改用UNLINK,阻塞立刻消失。

慢查询日志可以调整阈值:

CONFIG SET slowlog-log-slower-than 5000

建议在开发环境就设置成 5 毫秒甚至更低,让潜在问题在压测阶段现形,而不是上线后被用户投诉才发现。

4.2 大 Key 与热 Key 排查:别等事故来教你做人

大 Key 的排查可以用 Redis 自带工具:

redis-cli --bigkeys

它会扫描整个示例,分类列出占用空间大的 Key。想单独看某个键的内存占用:

MEMORY USAGE user:1001:profile

返回的是字节数。如果这个数字大得吓人,再用对应类型的长度命令确认规模:STRING 用STRLEN,HASH 用HLEN,SET 用SCARD,ZSET 用ZCARD,LIST 用LLEN。

真正难处理的是“怎么删大 Key”。直接DEL会阻塞,正确的删法是分批删:LIST 用LTRIM保留尾部一段,HASH 用HDEL每次删几百个字段,SET 用SREM批量删成员,ZSET 用ZREM批量删成员。如果等不及手动分批,就用UNLINK让后台线程慢慢释放。

热 Key 排查通常发生在“某个 Key 的 QPS 高到异常”时。MONITOR可以实时打印所有命令,但它对性能影响很大,只适合短时间抓包,比如高峰期开 30 秒立刻关。想要更体系化,可以开启 LFU 淘汰策略后用客户端扫描:

CONFIG SET maxmemory-policy allkeys-lfu redis-cli --hotkeys

--hotkeys会输出访问频率最高的 Key,这比盲猜哪个 Key 有问题要靠谱得多。

4.3 连接异常现场:CLIENT 系列命令

线上排查最难受的故障之一是“Redis 连接数打满”。新连接进不来,服务大面积报错。这时候先看连接列表:

CLIENT LIST

输出很长,每个连接一行,字段里有 id、addr、name、age、idle、db 这些信息。CLIENT LIST能看到每个客户端连了多久、空闲多久,初步判断是不是有连接泄漏:idle 时间极长但数量疯狂增长的,大概率是客户端连接池没有正确回收。

更主动的做法是给客户端起名字,出问题一眼认出是谁:

CLIENT SETNAME payment-app-01

确认某一波连接是问题源头后,按地址掐断:

CLIENT KILL ADDR 10.0.1.20:54321

或者按类型清理空闲连接。服务器端调优参数也要看一眼:

CONFIG GET maxclients CONFIG GET timeout

timeout决定空闲连接多久被服务端关闭。生产环境如果客户端连接池设计合理,timeout设成 300 秒比较稳妥;设太短会导致偶发建立连接风暴,设太长又占着连接数不放。

4.4 INFO 与持久化命令:快速体检

INFO是 Redis 的体检报告,支持按段查看:

INFO server INFO clients INFO memory INFO stats INFO replication

我平时最关心的是INFO stats里的命中率,可以自己算:

keyspace_hits / (keyspace_hits + keyspace_misses)

命中率低于 90% 就得看看是不是缓存过期策略太激进,或者 Key 设计不合理导致频繁穿透。内存看INFO memory中的used_memory和碎片率mem_fragmentation_ratio,碎片率高于 1.5 要考虑执行MEMORY PURGE或者重启治理。

持久化命令也要熟悉。BGSAVE会在后台生成 RDB 快照文件,BGREWRITEAOF会重写 AOF 文件,LASTSAVE可以查上次成功保存的时间点。做备份、迁移节点时这三条命令都是基础操作。

紧急清库的场景,我建议用异步手段:

FLUSHDB ASYNC FLUSHALL ASYNC

带ASYNC参数只是把释放内存的操作丢到后台,不会阻塞主线程。但请把这句话抄在工位上:生产环境清库前,先确认当前连的是哪台机器,再确认 DB 编号,最后再看一眼有没有人正在用这个库。我见过太多事故是从一条手误的FLUSHALL开始的。

5. 缓存治理实战:Redis 命令如何提高业务稳定性

5.1 缓存穿透、击穿、雪崩的命令级应对

缓存穿透是“查一个根本不存在的数据”,Redis 里没有,数据库里也没有,请求直接打到数据库。命令级别最简单的方案是:查不到时也写入一个空值并设置较短过期时间。

SET order:no:xxxx "null" NX EX 60

不过空值缓存治标不治本,大量随机 Key 穿透时还得靠布隆过滤器拦截。

缓存击穿是“热点 Key 过期瞬间,大量请求同时回源”。命令级别的解法是让回源操作串行化——用分布式锁,只放一个请求去重建缓存,其他请求短暂等待:

SET cache:lock:hotkey "reload_uuid" NX EX 5

拿到锁的请求去查数据库,把结果写回 Redis 并设置过期时间,然后删锁;没拿到锁的请求 sleep 几十毫秒后重新GET,一般已经能读到新缓存了。

缓存雪崩是“大量 Key 同时过期”。命令级别的解法是给过期时间加随机扰动:

SET order:1001 "data" EX 300 SET order:1002 "data" EX 310 SET order:1003 "data" EX 285

让 Key 的过期时间错开,而不是整整齐齐在同一秒集体失效。这一招写起来就一行代码,但能避免数据库被打出翔。

5.2 日常巡检实践:一套命令组合拳

我这几年养成了固定巡检习惯,每隔一段时间在低峰期跑一遍下面这套命令:

redis-cli -h 127.0.0.1 -p 6379 INFO stats | grep -E 'keyspace_hits|keyspace_misses' redis-cli --bigkeys redis-cli -n 0 DBSIZE redis-cli SLOWLOG GET 50

DBSIZE看当前库有多少 Key,如果数量异常激增,可能是某个缓存没有设置过期时间在持续堆积。定期看SLOWLOG能提前暴露危险命令。--bigkeys至少每个月跑一次,把大 Key 的清理排期做进去。

这套组合不需要任何额外监控系统,纯靠 Redis 自带命令就能完成基础体检。等团队规模大起来了,再考虑接入 Prometheus 之类的监控体系,但命令仍然是监控数据的第一来源。

5.3 操作红线与权限最小化

我给团队立过一份“Redis 操作红线”,核心就几条:

危险命令后果替代方案
KEYS pattern全键扫描阻塞SCAN/redis-cli --scan
FLUSHALL/FLUSHDB数据全清确认环境 + ASYNC
MONITOR 长时间开启性能暴跌短时抓取 30 秒内关闭
DEL 大 Key主线程阻塞UNLINK
SMEMBERS/HGETALL 大结构网络阻塞 + 内存暴涨SSCAN/HSCAN

Redis 6.0 开始支持 ACL,生产环境一定要把权限收口。给业务应用一个最小功能账号,别再用默认的 root 账号裸奔:

ACL SETUSER appuser on >StrongPass123 ~cache:* +@read +@write +@hash +@set +@zset -FLUSHALL -KEYS -MONITOR

这里创建了用户 appuser,密码为 StrongPass123,只能访问cache:*前缀的键,只能执行读、写、HASH、SET、ZSET 相关命令,明确禁止FLUSHALL、KEYS、MONITOR。业务应用连接时用这个账号,就算代码里有恶意或误操作,也撞不破权限墙。

5.4 命令学习的路径建议:不要背语法,要建立心智模型

我看过太多新人抱着命令大全从第一个命令背到最后一个,背完就忘。学 Redis 命令的正确姿势是建立三层心智模型:

第一层,每种数据结构天然对应一类需求。STRING 是缓存和计数,HASH 是对象,LIST 是队列和时间线,SET 是去重和关系,ZSET 是排行榜和优先级。拿到业务需求时,第一步是选数据结构,而不是翻命令。

第二层,读命令前先看命令复杂度。Redis 官网每个命令都标注了时间复杂度,O(1) 的命令随便用,O(N) 的命令提前问自己一句:这个 N 有多大?会不会阻塞?

第三层,理解组合的威力。很多复杂问题不是靠命令,而是靠命令的组合:SET NX EX是有过期时间的原子加锁,LPUSH + LTRIM是固定长度的滑动窗口,ZADD + ZRANGEBYSCORE是延迟队列。命令像积木,组合方式决定业务上限。

这个心智模型建立起来之后,再去看那些“Redis 为什么快”“Redis 为什么是单线程”“Redis 跳表原理”的面试题,你会发现自己根本不用背答案——理解了执行模型和数据结构,自然就能推导出答案。

最后分享两点我自己的习惯。一是手机里长期存一份常用命令表,不是拿来背诵,而是在出问题时快速提醒自己“这件事 Redis 提供了什么工具”。二是任何危险操作前先敲两条命令:PING确认服务还活着,INFO server确认自己连的到底是哪台机器哪个实例。Redis 命令本身并不难,难的是在正确的场景选出正确的那一条,并且知道它背后的代价。这篇东西如果能在某个排查故障的深夜帮到你,我觉得就值了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询