前阵子帮一个朋友排查线上事故他团队里有人顺手敲了一条KEYS user:*结果那台 Redis 的 CPU 瞬间飙满整个服务的缓存请求全被拖住。Redis 的命令看起来都挺简单随手一条 SET、GET 就能干活但正因为简单很多人从来没想过每条命令背后隐藏着什么代价。Redis 命令详解这个话题如果只是罗列一堆语法其实帮不到任何人真正值得拆解的是每条命令在什么业务场景下该用、不该用、用了之后会有什么连锁反应。这篇文章我不打算给你一份可以背的 API 大全而是用踩过坑的视角把 Redis 的通用命令、五大数据类型命令、分布式锁、线上排障命令背后的逻辑讲透顺便把几个面试高频点也串进去。适合刚学 Redis 的人打基础也适合写过一段时间但没系统梳理过命令体系的人查漏补缺。1. 从一条命令的网络旅程开始理解 Redis 命令为什么“轻”1.1 一条 SET 命令在网络上长什么样RESP 协议的门道很多人用 Redis 几年都没看过协议层。其实 Redis 客户端和服务器之间走的是 RESPREdis 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)设置过期时间键不存在时返回 0TTL keyO(1)查看剩余过期时间-1 表示永不过期-2 表示不存在DEL keyO(1) 平均同步删除键大 Key 慎用会阻塞UNLINK keyO(1)异步删除键4.0 推荐尤其大 KeySCAN cursorO(1) 每批游标遍历键空间生产取键用这个别用 KEYSCOPY src dstO(1) 平均复制键可以带 DB 参数跨库复制SCAN和KEYS的差别值得多说两句。KEYS是一次性返回所有匹配的键在键多的时候会阻塞主线程SCAN则是基于游标的增量式遍历每次返回一部分结果用返回的游标继续迭代直到游标回到 0redis-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 只需要发一条HSETSTRING 方案里你得先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 5BRPOP是阻塞读没消息时一直挂起等待超时时间 5 秒。相比轮询阻塞读能极大减少空轮询的 RTT 和 CPU 浪费。做一个简单的“最近访问记录”LPUSH新元素到左侧然后用LTRIM只保留前 100 条一举两得LPUSH recent:viewed:u1001 sku:8899 LTRIM recent:viewed:u1001 0 99LTRIM的意思是从第 0 个元素截断到第 99 个之外的直接删掉。这个组合比“先 LPUSH 再单独删除超长部分”要优雅得多也避免列表无限增长。分页拉取时间线用LRANGELRANGE timeline:user:1001 0 9注意LRANGE是 O(N)N 是区间长度。取前 10 条没问题取整个大列表就有风险。需要拿一批数据时一定先想清楚列表规模再决定区间大小。2.4 SET去重、标签、抽奖SET 的特性是无序、元素唯一。做抽奖就是SPOP它随机弹出并移除一个元素保证已抽中的人不会重复中奖SADD lottery:20250601 u1001 u1002 u1003 SPOP lottery:20250601SRANDMEMBER则只随机返回但不删除适合做“随机推荐”和“展示”场景。用错了就是逻辑 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 WITHSCORESZINCRBY用一个命令完成“分数加一”天然原子不用先取分再写回。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 30000NX 表示键不存在才设置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 与 WATCHRedis 的乐观锁机制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 endWATCH监控一个键如果从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 0EVAL 上面的脚本 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 用STRLENHASH 用HLENSET 用SCARDZSET 用ZCARDLIST 用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 timeouttimeout决定空闲连接多久被服务端关闭。生产环境如果客户端连接池设计合理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 50DBSIZE看当前库有多少 Key如果数量异常激增可能是某个缓存没有设置过期时间在持续堆积。定期看SLOWLOG能提前暴露危险命令。--bigkeys至少每个月跑一次把大 Key 的清理排期做进去。这套组合不需要任何额外监控系统纯靠 Redis 自带命令就能完成基础体检。等团队规模大起来了再考虑接入 Prometheus 之类的监控体系但命令仍然是监控数据的第一来源。5.3 操作红线与权限最小化我给团队立过一份“Redis 操作红线”核心就几条危险命令后果替代方案KEYS pattern全键扫描阻塞SCAN/redis-cli --scanFLUSHALL/FLUSHDB数据全清确认环境 ASYNCMONITOR 长时间开启性能暴跌短时抓取 30 秒内关闭DEL 大 Key主线程阻塞UNLINKSMEMBERS/HGETALL 大结构网络阻塞 内存暴涨SSCAN/HSCANRedis 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 命令本身并不难难的是在正确的场景选出正确的那一条并且知道它背后的代价。这篇东西如果能在某个排查故障的深夜帮到你我觉得就值了。