你第一次在终端里执行 SET name xiaohu,再用 GET name 拿回结果,大概率会觉得 Redis 命令挺简单:一个动词,后面跟着键和值,回车就结束。
真正容易翻车的地方,往往不在语法。HGET 和 HGETALL 都是在读 Hash,前者只拿一个字段,后者却可能把几十万个字段一次性塞进网络;DEL 和 UNLINK 都能删键,面对大 key 时,对主线程的影响却不一样;Pipeline、事务和 Lua 都能“把多条命令放在一起”,解决的又是三类不同问题。
所以这一章不打算把命令表从 A 背到 Z。我们会先看业务场景,再选数据结构和命令,最后追问三个问题:这条命令是不是原子的?它会处理多少数据?它会不会让别的请求排队?

学习 Redis 命令时,建议同时记住四件事:输入参数、返回值、时间复杂度和最坏数据规模。只记命令名,到了生产环境很容易把一个看似普通的读取写成慢命令。
Redis 的 key 都是字符串,真正的数据类型属于 key 对应的 value。你可以把 Redis 想成一排带标签的储物柜:key 是柜门标签,类型决定柜子内部的结构,命令则是针对这种结构设计的操作工具。
同一个 key 在某一时刻只能对应一种 value 类型。一个 key 现在存的是 String,就不能直接对它执行 LPUSH;如果这么做,Redis 会返回 WRONGTYPE,而不是偷偷帮你转换。
127.0.0.1:6379> SET demo:value "你好"
OK
127.0.0.1:6379> TYPE demo:value
string
127.0.0.1:6379> LPUSH demo:value "新元素"
(error) WRONGTYPE Operation against a key holding the wrong kind of value面对一个需求,可以先按下面的直觉做第一轮判断:
这张表只负责帮你进门,不负责替你做最终决定。比如 List 能做队列,但它不会自动记录“哪条任务交给了哪个消费者”;如果业务需要确认、重试和积压追踪,Stream 往往更合适。
拿到一条陌生命令时,不妨按这个顺序读它:
HGET 只操作 Hash,TTL 则能检查任意类型的 key。SISMEMBER 判断一个成员,SMEMBERS 返回整个集合,两者的成本完全不同。INCR 自己是原子的,GET 后在客户端加一再 SET 不是。下面这个交互选择器可以先帮你把“业务约束”和“数据结构能力”对齐。先按直觉做选择,再阅读它给出的翻车点。
很多缓存事故都不是值写错了,而是 key 的边界没设计清楚:命名混在一起、TTL 漏设、批量清理时扫错范围,最后只能在一片相似的 key 里人工排查。
常见的命名方式是用冒号分段,例如:
业务域:对象类型:对象标识:用途
shop:product:842:detail
shop:product:842:stock
auth:user:42:session
report:2026-08-13:uv命名不需要追求花哨,重点是稳定、可预测,并且能把不同生命周期的数据分开。商品详情缓存和库存即使属于同一商品,也不该因为“放在一起方便”而共用一个不可拆分的大 JSON;它们的更新频率和一致性要求通常不同。
key 太短会失去语义,太长又会让元数据和网络传输变重。更现实的做法是保留真正有区分度的段,并为动态标识设置长度上限。不要把未经约束的完整 URL、用户输入或整段查询条件直接拼进 key。
假设你要保存一个 30 分钟有效的会话。下面这条命令把值、条件和过期时间放在同一个原子操作里:
127.0.0.1:6379> SET auth:user:42:session "token-7f3a" EX 1800 NX
OK
127.0.0.1:6379> GET auth:user:42:session
"token-7f3a"
127.0.0.1:6379> TTL auth:user:42:session
(integer) 1797EX 1800 表示 1800 秒后过期,NX 表示 key 不存在时才写入。与先 SET 再 EXPIRE 相比,一条 SET ... EX ... 没有“值已经写入,但客户端在设 TTL 前断线”的空窗。
SET 还有几组常用选项:
NX:只在 key 不存在时写入。XX:只在 key 已存在时写入。EX / PX:设置秒级或毫秒级相对过期时间。EXAT / PXAT:按 Unix 时间设置绝对过期点。GET:写入新值,同时返回旧值。KEEPTTL:覆盖值时保留原 TTL。这些选项可以减少网络往返,也能缩小并发窗口。不过 SET key value NX EX 30 只解决“条件写入一个带期限的值”,它不自动等于完整的分布式锁方案。锁的持有者标识、续期、超时任务是否仍在运行,以及安全释放,都还需要单独设计。
TTL 返回的不只是一个倒计时:
127.0.0.1:6379> SET cache:article:9 "正文"
OK
127.0.0.1:6379> TTL cache:article:9
(integer) -1
127.0.0.1:6379> TTL cache:not-found
(integer) -2
127.0.0.1:6379> EXPIRE cache:article:9 300
(integer) 1
127.0.0.1:6379> TTL cache:article:9
(integer) 298
127.0.0.1:6379
-1 表示 key 存在,但没有过期时间。-2 表示 key 已不存在,可能从未创建,也可能刚刚过期。PTTL 的语义相同,只是单位换成毫秒。排查“缓存怎么一直不失效”时,TYPE、TTL 和业务日志通常比先猜 Redis 是否坏了更有效。
普通的 SET key newValue 会覆盖旧值,并清除这个 key 原有的 TTL;带 KEEPTTL 才会保留。对容器内部做 HSET、LPUSH、SADD 这类原地修改时,key 级 TTL 通常会继续保留。更新路径不同,过期语义也可能不同。

KEYS cache:* 很诱人,因为一次就能拿到所有匹配项。但它需要遍历当前数据库的 keyspace,并一次返回完整结果。数据量大时,这条命令会长时间占住执行线程,还会制造一个很大的网络响应。
生产环境的常规遍历应使用 SCAN:
127.0.0.1:6379> SCAN 0 MATCH cache:product:* COUNT 100 TYPE hash
1) "84"
2) 1) "cache:product:901"
2) "cache:product:842"返回结果的第一项是下一次要传入的游标,第二项才是本批 key。把游标 84 继续传给下一次 SCAN,直到返回游标 0,一轮遍历才算结束。
这里有三个很容易误读的点:
COUNT 100 是工作量提示,不保证恰好返回 100 个 key。0,就不能停止。因此,消费 SCAN 结果的动作最好可重复执行。删除、重新设置 TTL 这类幂等操作比较容易处理;如果每看到一次 key 就计费,重复项会直接变成业务错误。
清理一批前缀 key 时,安全思路不是 KEYS 后接一个巨大的 DEL,而是:
先用 SCAN 按模式分批发现候选 key,并记录游标。需要在集群中执行时,要遍历相关主节点,不能把单节点扫描结果当成整个集群。
每一批只处理受控数量,例如 100 到 500 个。先抽样检查命名和类型,确认模式没有误伤,再扩大处理范围。
对可能很大的容器优先使用 UNLINK。它先把 key 从 keyspace 摘掉,再把实际内存回收交给后台线程,主线程上的删除阶段更短。
记录扫描数量、实际删除数量、耗时和错误。限速并观察延迟,不要让一次“后台清理”变成前台流量抖动。
127.0.0.1:6379> UNLINK cache:product:842 cache:product:901
(integer) 2UNLINK 也不是零成本魔法。后台仍然要释放内存,删除量太大时会增加后台任务和内存压力。它解决的是把重释放工作移出主线程,不是让工作消失。
Redis 的优势不是“所有东西都能当字符串存”,而是让常见的数据操作直接发生在服务端。你本来要把整个对象拉回应用、修改、再写回;选对结构后,可能只需要一条字段更新或成员操作。

String 是最基础的 value 类型,可以保存文本、整数形式的字符串、JSON、图片二进制或压缩数据。它是二进制安全的,但“能放进去”不等于“适合放进去”。
最普通的读写是 SET 和 GET:
127.0.0.1:6379> SET article:9:title "Redis 命令不是背单词"
OK
127.0.0.1:6379> GET article:9:title
"Redis 命令不是背单词"
127.0.0.1:6379> STRLEN article:9:title
(integer) 27批量读取可以用 MGET,批量写入可以用 MSET。它们能减少协议和往返开销,但成本会随 key 数量和返回字节增长。一次 MGET 一万个大值,命令条数虽然只有一条,响应仍然可能很重。
String 最经典的原子能力是计数:
127.0.0.1:6379> SET article:9:views 0
OK
127.0.0.1:6379> INCR article:9:views
(integer) 1
127.0.0.1:6379> INCRBY article:9:views 9
(integer) 10
127.0.0.1:6379> DECRBY article:9:views 3
(integer) 7INCR 内部完成读取、加一和写回。多个客户端同时执行时,每次加一不会互相覆盖。相反,客户端自己执行 GET、计算、SET,三步之间可能被别的客户端插入。
String 里存一个不断膨胀的大 JSON,读取一个字段也要传输和解析整块内容,修改一个字段还要重写整个值。对象字段需要独立读写时,先考虑 Hash;层级很深、需要路径更新时,再评估 Redis JSON 或其他数据模型。
Hash 适合保存扁平对象或一组相关计数器。比如用户资料可以放在一个 key 下,每个属性是一个 field:
127.0.0.1:6379> HSET user:42 name "小林" plan "pro" login_count 7
(integer) 3
127.0.0.1:6379> HGET user:42 plan
"pro"
127.0.0.1:6379> HMGET user:42 name login_count missing_field
1) "小林"
2) "7"
3) (nil)
127.0.0.1:6379> HINCRBY user:42 login_count
HSET 的返回值是“新增加了多少字段”,覆盖已有字段时不会计入新增数。字段值本质上仍是字符串,所以 HINCRBY 要求对应值能解析成整数。
小 Hash 使用 HGETALL 很方便:
127.0.0.1:6379> HGETALL user:42
1) "name"
2) "小林"
3) "plan"
4) "pro"
5) "login_count"
6) "8"但 HGETALL 的成本和字段数成正比。如果一个 Hash 被当成“整张表”塞了几十万字段,一次全量读取就会产生大响应。此时应按已知字段用 HGET / HMGET,需要遍历时用 HSCAN,并认真考虑是否应该按实体拆 key。
Hash 的 key 可以设置 TTL。不要默认每个字段都能独立过期:字段级过期能力取决于 Redis 版本和部署兼容性,设计跨版本系统时应先核对目标环境。一个常见且兼容的方案,是把生命周期明显不同的字段拆成不同 key。
List 维护插入顺序,最擅长在头尾推入和弹出。以邮件任务队列为例,生产者从右侧放入,消费者从左侧取出:
127.0.0.1:6379> RPUSH queue:email task:1001 task:1002 task:1003
(integer) 3
127.0.0.1:6379> LPOP queue:email
"task:1001"
127.0.0.1:6379> LRANGE queue:email 0 -1
1) "task:1002"
2) "task:1003"如果消费者轮询 LPOP,空队列会产生大量无意义请求。BLPOP、BRPOP 会让这个客户端连接等待,直到元素到来或超时:
127.0.0.1:6379> BLPOP queue:email 5
1) "queue:email"
2) "task:1002"这里阻塞的是等待结果的客户端连接,不是 Redis 主线程站在原地睡 5 秒。Redis 仍能处理其他连接。客户端应为阻塞命令准备合适的连接,不要把连接池里唯一的连接长期占住。
如果任务刚弹出,消费者就崩溃,简单 LPOP 的任务已经从队列消失。可以用 LMOVE / BLMOVE 把任务原子地从待处理列表移到处理中列表:
127.0.0.1:6379> BLMOVE queue:email queue:email:processing LEFT RIGHT 5
"task:1003"
127.0.0.1:6379> LRANGE queue:email:processing 0 -1
1) "task:1003"处理成功后再从 processing 列表删除;超时未完成的任务要由应用重新入队。这比直接弹出可靠一些,但确认、重试次数、消费者归属和积压统计都要自己补。如果这些能力是核心需求,Stream 更省心。
List 的“随机访问”只是接口上能做,不代表像数组一样便宜。LINDEX、LSET、LINSERT、LREM 往往要沿列表寻找位置,越靠中间成本越高。LRANGE 0 -1 也会返回整条列表。最近记录通常用 LPUSH 后接 LTRIM 0 99 控制长度,不要让它无限长。
Set 是无序且成员唯一的字符串集合。权限、标签和关注关系都很适合用它表达:
127.0.0.1:6379> SADD role:editor:users user:7 user:42 user:88
(integer) 3
127.0.0.1:6379> SADD role:editor:users user:42
(integer) 0
127.0.0.1:6379> SISMEMBER role:editor:users user:42
(integer) 1
127.0.0.1:6379> SCARD role:editor:users
(integer) 3SADD 重复添加返回 0,这让幂等写入很自然。判断单个成员用 SISMEMBER,一次判断多个成员可以用 SMISMEMBER。
集合运算让关系查询很直观:
127.0.0.1:6379> SADD user:42:follows user:7 user:8 user:9
(integer) 3
127.0.0.1:6379> SADD user:7:follows user:8 user:10
(integer) 2
127.0.0.1:6379> SINTER user:42:follows user:7:follows
1) "user:8"
127.0.0.1:6379> SDIFF user:42:follows user:7:follows
1) "user:7"
2) "user:9"问题出在规模。SINTER 需要检查集合成员,SUNION 和 SDIFF 也会处理大量元素;结果还要完整返回给客户端。对超大集合执行 SMEMBERS 或多集合交集,可能让主线程忙很久。只需要交集数量时,可以使用返回基数的命令并设置上限;需要遍历单个大 Set 时,用 SSCAN 分批读取。
Sorted Set 常简称 ZSet。每个 member 唯一,并关联一个浮点 score。它适合排行榜、优先队列、时间窗口和按权重排序的索引。
127.0.0.1:6379> ZADD game:rank 980 user:42 1210 user:7 1210 user:9
(integer) 3
127.0.0.1:6379> ZRANGE game:rank 0 2 REV WITHSCORES
1) "user:9"
2) "1210"
3) "user:7"
4) "1210"
5) "user:42"
6) "980"
127.0.0.1:6379>
相同 score 的成员会再按 member 的字典序决定顺序;使用 REV 时顺序相反。如果业务必须按“最早达到该分数的人优先”,不能只依赖相同 score,需要把时间或额外排序规则编码进设计。
按分数区间查询时,可以限制返回数量:
127.0.0.1:6379> ZRANGE game:rank +inf 1000 BYSCORE REV LIMIT 0 10 WITHSCORES
1) "user:42"
2) "1280"
3) "user:9"
4) "1210"
5) "user:7"
6) "1210"ZSet 的插入和按范围定位通常是对数级,但返回 个成员仍要付出与 成正比的遍历和网络成本。ZRANGE 0 -1 放在百万成员排行榜上,和 SMEMBERS 一样危险。
核心类型能覆盖大多数业务,但有些问题如果硬用 Set 或 Hash,会浪费大量内存或失去关键语义。下面四种能力都有很明确的适用边界。

Bitmap 不是独立的底层类型,而是把 String 当作位数组操作。假设用户编号连续,你要记录某天是否签到,可以让 offset 等于用户编号:
127.0.0.1:6379> SETBIT sign:2026-08-13 42 1
(integer) 0
127.0.0.1:6379> SETBIT sign:2026-08-13 88 1
(integer) 0
127.0.0.1:6379> GETBIT sign:2026-08-13 42
(integer) 1
127.0.0.1:6379> BITCOUNT sign:2026-08-13
(integer) 2SETBIT 返回这个位置原来的位值,GETBIT 读取一个位置,BITCOUNT 统计值为 1 的位数。多个日期的 Bitmap 还可以用 BITOP AND / OR 计算连续签到或任意一天签到的人群。
它的优势来自编号稠密。如果用户 ID 是一个巨大的随机数,第一次设置很远的 offset,Redis 需要把中间空间补出来,内存优势可能瞬间消失。Bitmap 只保存位,不保存用户详情;通常还要配合其他 key 找到实体数据。
统计独立访客时,用 Set 保存每个访客可以得到精确答案,但内存会随人数增长。HyperLogLog 用固定且很小的空间估算基数,代价是结果存在小误差。
127.0.0.1:6379> PFADD uv:article:9 user:42 user:7 user:88 user:42
(integer) 1
127.0.0.1:6379> PFCOUNT uv:article:9
(integer) 3
127.0.0.1:6379> PFADD uv:article:10 user:7 user:99
(integer) 1
127.0.0.1:6379> PFMERGE uv:articles:all uv:article:9 uv:article:10
OK
127.0.0.1:6379> PFCOUNT uv:articles:all
它适合趋势、仪表盘和大规模 UV 估算,不适合余额、库存或计费人数。HyperLogLog 也不能告诉你“有哪些用户”,更不能做成员存在性判断。如果之后需要拿到成员名单,就应该保留 Set、数据库明细或事件日志。
Geospatial 命令把经纬度和成员存进一种地理索引。添加坐标时参数顺序是经度在前、纬度在后:
127.0.0.1:6379> GEOADD shop:stores 116.397128 39.916527 store:故宫 116.403963 39.915119 store:王府井
(integer) 2
127.0.0.1:6379> GEODIST shop:stores store:故宫 store:王府井 km
"0.6030"
127.0.0.1:6379> GEOSEARCH shop:stores FROMLONLAT 116.397 39.916 BYRADIUS 2 km WITHDIST ASC
1) 1) "store:故宫"
2) "0.0114"
2
GEO 适合“某点两公里内有哪些门店”这类简单查询。它计算的是地理距离范围,不理解道路、桥梁、行政边界和实时路况。外卖骑手的真实行驶时间、复杂多边形区域和路径规划,仍需要专业 GIS 或地图服务。
Pub/Sub 的消息在线广播后就过去了,简单 List 队列又缺少消费者组和确认记录。Stream 更像一条持续追加的事件流水账:每条记录有递增 ID,消费者可以回看,消费者组还能追踪哪些消息已经发出但尚未确认。
先追加两条订单事件:
127.0.0.1:6379> XADD orders:events * type created order_id 9001 amount 199
"1786617600123-0"
127.0.0.1:6379> XADD orders:events MAXLEN ~ 100000 * type paid order_id 9001
"1786617600456-0"
127.0.0.1:6379> XRANGE orders:events - + COUNT 2
1) 1) "1786617600123-0"
2) 1) "type"
2)
* 让 Redis 生成 ID,MAXLEN ~ 100000 以近似方式控制长度,通常比精确裁剪更省工作。别忽略裁剪:如果只 XADD 不设置保留策略,Stream 会一直增长。
消费者组的基本流程是创建组、读取新消息、处理、确认:
127.0.0.1:6379> XGROUP CREATE orders:events billing $ MKSTREAM
OK
127.0.0.1:6379> XREADGROUP GROUP billing worker-1 COUNT 10 BLOCK 2000 STREAMS orders:events >
(nil)
127.0.0.1:6379> XADD orders:events * type refunded order_id 9001
"1786617600789-0"
127.0.0.1:6379> XREADGROUP GROUP billing worker-1 COUNT
消息交给消费者后会进入该组的待确认记录。消费者崩溃时,消息不会因为没 XACK 就自动消失;应用需要用 XPENDING 观察积压,并用认领机制把超时消息交给健康消费者。这带来的是“至少一次”式处理思路:同一消息可能再次送达,所以消费端要幂等。
Stream 提供了确认和重投所需的状态,但不会替你完成业务幂等。扣款、发券、发货等副作用仍要用事件 ID 或业务单号去重。XACK 表示这个消费组认为处理完成,不等于数据库事务已经自动和 Redis 对齐。
“把几条命令一起执行”这句话太模糊了。你是想减少网络往返,还是不让其他客户端插队,或者需要读取结果后做判断再写入?三个目标分别对应 Pipeline、事务和服务端脚本。

在一个 Redis 实例上,一条普通命令的执行不会被另一条命令从中间切开。INCR counter 的读、加、写是一个命令,因此并发计数不会丢增量。
但业务动作经常跨多条命令。下面这个库存判断就有竞争窗口:
GET inventory:sku:42
客户端判断库存大于 0
DECR inventory:sku:42两个客户端可能同时读到库存为 1,然后都继续扣减。每一条命令都原子,三条拼起来却不是。原子性的单位不等于业务正确性的单位。
普通请求每发一条命令都要等待一次响应,网络往返时间可能比命令执行本身还贵。Pipeline 允许客户端连续发送多条命令,之后按顺序读取回复:
无 Pipeline:发送 A → 等 A → 发送 B → 等 B → 发送 C → 等 C
有 Pipeline:发送 A、B、C → 一次读取 A、B、C 的回复它适合批量写日志、预热缓存或一次读取多个互不依赖的 key。Pipeline 中的命令仍是一条条执行,其他客户端的命令可能插在它们之间。Pipeline 也不会自动回滚前面已经成功的命令。
批次不是越大越好。服务器要暂存回复,客户端也要保存待处理结果。一次塞进百万条命令,可能把节省的往返时间换成内存峰值和长尾延迟。实践中应设定批次上限,处理完一批回复再发下一批。
有些客户端把 Pipeline 和 Transaction 放在同一个 API 附近,甚至用一个布尔参数切换。不要因为代码对象叫 pipeline,就默认它有事务原子性;要确认客户端最终发送的是普通批量命令,还是 MULTI / EXEC。
Redis 事务从 MULTI 开始。之后的命令先进入队列,EXEC 到达时才按顺序执行。在执行这组命令期间,其他客户端不会插进来。
127.0.0.1:6379> MSET wallet:alice 1000 wallet:bob 500
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY wallet:alice 100
QUEUED
127.0.0.1:6379(TX)> INCRBY wallet:bob 100
QUEUED
127.0.0.1:6379(TX)> XADD wallet:events * type transfer from alice to bob amount 100
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 900
2) (
要注意,Redis 事务没有关系型数据库那种自动回滚。语法错误可能让整组事务在执行前被拒绝;但如果命令已经成功入队,执行期才遇到类型错误,其他命令仍会继续:
127.0.0.1:6379> SET wallet:alice 100
OK
127.0.0.1:6379> SET transfer:log "我不是列表"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY wallet:alice 10
QUEUED
127.0.0.1:6379(TX)> LPUSH transfer:log "扣款 10"
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 90
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value余额已经变成 90,不会因为第二条失败就恢复成 100。所以事务前要控制 key 类型和参数,不能把回滚想象成默认保险丝。
事务里的命令在 EXEC 前不会真正执行,因此你不能在 MULTI 后读取一个值,再根据这个回复决定下一条排队命令。需要“先读、再判断、再写”时,可以在事务前用 WATCH 做乐观锁:
127.0.0.1:6379> WATCH inventory:sku:42
OK
127.0.0.1:6379> GET inventory:sku:42
"10"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY inventory:sku:42 2
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 8如果从 WATCH 到 EXEC 之间,别的客户端修改、过期或淘汰了这个 key,EXEC 会返回空结果,整组命令不执行。客户端必须重新读取、重新判断并重试,同时设置重试上限和退避策略。
高竞争热点上,反复 WATCH 冲突会浪费网络往返。此时可以寻找更合适的单条命令,或把短小的条件逻辑放进 Lua。
Lua 脚本在 Redis 内连续执行,可以把多步条件更新变成一个原子操作。下面的脚本只有库存足够时才扣减:
127.0.0.1:6379> SET inventory:sku:42 10
OK
127.0.0.1:6379> EVAL "local stock=tonumber(redis.call('GET',KEYS[1]) or '-1'); local n=tonumber(ARGV[1]); if stock<n then return {err='库存不足'} end; return redis.call('DECRBY',KEYS[1],n)" 1 inventory:sku:42 2
(integer) 81 表示后面有一个 key 参数,脚本通过 KEYS[1] 取 key,通过 ARGV[1] 取普通参数。所有会访问的 key 都应显式传入,不要在脚本中根据数据临时拼接隐藏 key。集群环境还要确保脚本涉及的 key 位于允许的哈希槽范围。
Lua 的原子性来自“脚本执行期间,其他命令不能插入”。这也意味着慢脚本会堵住所有客户端。脚本应该短小、有上界,避免大范围 SCAN、遍历巨大集合或不可控循环。重复使用的脚本应参数化并用脚本缓存或 Functions 管理,别为每次请求动态生成一份略有不同的脚本文本。
Redis 的命令通常很快,但“通常”依赖数据规模。一条命令的成本至少要看三层:算法处理多少元素、序列化和网络返回多少字节、释放或复制多少内存。

GET 查找 key 可以看作常数级,但如果 value 是 50 MB,服务器仍要把 50 MB 数据写进响应,客户端要接收和解析。HGET 只定位一个字段,也挡不住这个字段本身是一大块数据。
多参数命令也要看参数数量。MGET 的成本随 key 数量增长,DEL key1 key2 ... 至少要处理所有参数。复杂度里的 、 具体代表什么,要到命令说明里确认。
下面是几组常见对照:
大 key 指单个 value 过大,可能是一个超长 String,也可能是拥有大量元素的 Hash、List、Set、ZSet 或 Stream。它会带来一串连锁反应:
解决大 key 不能只靠换一条命令。更根本的办法是限制集合长度、按用户或时间分桶、把大字段拆开、只取所需字段,并在数据模型阶段设置增长上限。
SCAN、HSCAN、SSCAN 和 ZSCAN 每次只做一小段工作,让其他请求有机会穿插执行。一整轮扫描的总工作仍与数据量相关,而且应用要维护游标、处理重复和容忍变化。
127.0.0.1:6379> HSCAN user:42:events 0 MATCH login:* COUNT 50
1) "12"
2) 1) "login:2026-08-12"
2) "success"
3) "login:2026-08-13"
4) "success"如果业务每个请求都要完整扫一遍百万字段,换成 HSCAN 只是把一次长阻塞拆成很多次短操作,总成本和业务延迟依然很高。这通常说明你缺少索引,或者 key 拆分方式不适合查询路径。
BLPOP、XREAD BLOCK 这类命令会让当前客户端等待数据,但等待期间 Redis 可以继续服务其他客户端。它们属于“连接级等待”。
大集合上的 SMEMBERS、复杂 SINTER、大范围 LRANGE、慢 Lua 脚本会让执行线程忙于当前工作,其他客户端只能排队。这才是“服务器级阻塞”的核心风险。
一个阻塞命令被标成 @blocking,通常是在描述连接可能等待;一个命令被标成 @slow,也不代表每次必然慢,而是提醒你它的成本会随参数或数据规模增长。标签要结合复杂度和业务数据读。
下面的实验台不会假装给出真实毫秒数,它帮助你把元素数量、返回字节和命令复杂度放在一起估算风险。
线上出现延迟时,最糟糕的排查方式是凭感觉执行更多重命令。正确顺序应该是先确认 key 类型和规模,再看慢日志与延迟事件,最后才决定是否扫描或修改数据。
拿到一个可疑 key,可以从这组命令开始:
127.0.0.1:6379> TYPE cart:user:42
hash
127.0.0.1:6379> TTL cart:user:42
(integer) 863
127.0.0.1:6379> HLEN cart:user:42
(integer) 37
127.0.0.1:6379> MEMORY USAGE cart:user:42
(integer) 1848
127.0.0.1:6379> OBJECT ENCODING cart:user:42
"listpack"容器长度分别用 HLEN、LLEN、SCARD、ZCARD、XLEN 查看。MEMORY USAGE 给出 key 和 value 占用的估算字节;复杂容器默认可能采样内部元素,追求全量样本本身也有成本。OBJECT ENCODING 适合解释内存差异,不应让业务代码依赖具体编码,因为编码会随版本、配置和数据规模自动变化。
同一个逻辑对象在不同 Redis 版本、内存分配器和编码阈值下,MEMORY USAGE 的具体数值可能不同。这里要观察的是数量级和变化趋势,不要把示例里的字节数当成固定答案。
SLOWLOG 记录超过阈值的命令执行时间,不包含客户端网络传输时间。先看最近记录:
127.0.0.1:6379> SLOWLOG LEN
(integer) 12
127.0.0.1:6379> SLOWLOG GET 3
1) 1) (integer) 31
2) (integer) 1786618000
3) (integer) 18642
4) 1) "HGETALL"
2) "profile:all-users"
5) "10.0.0.8:52144"
6) "api-worker"第三项通常是服务器执行耗时,单位为微秒;后面能看到命令参数、客户端地址和名称。给连接设置有意义的 client name,会让排查比一串 IP 更快。
如果延迟来自 fork、持久化、内存回收或其他系统事件,只看 SLOWLOG 可能没有答案。启用合适阈值的延迟监控后,可以结合 LATENCY LATEST、LATENCY HISTORY 和 LATENCY DOCTOR 查看事件。监控配置属于运维动作,应在了解当前环境和权限后执行。
redis-cli 提供了扫描式工具:
redis-cli --bigkeys -i 0.1
redis-cli --memkeys -i 0.1
redis-cli --keystats -i 0.1--bigkeys 更关注各类型的长度或基数,--memkeys 关注内存,--keystats 合并大小与分布信息。-i 0.1 会在一批 SCAN 调用之间稍作等待,降低对繁忙实例的影响。
这些工具仍会遍历 keyspace,不应该在高峰期随手运行。集群中还要理解它连接和扫描了哪些节点。发现大 key 后,先记录类型、业务归属、读写路径和增长速度,再决定拆分或删除。
以下命令不是“永远不能用”,而是必须有场景、权限和影响评估:
KEYS:适合受控小数据集的临时调试,不是生产遍历默认方案。FLUSHDB / FLUSHALL:删除当前库或全部库的 key;异步模式只是改变释放方式,不会提供撤销按钮。MONITOR:实时复制服务器收到的命令,既有明显性能成本,也可能暴露业务参数。CONFIG、DEBUG、SAVE、SHUTDOWN:会改变配置、触发重工作或控制实例生命周期。SORT、集合运算和脚本:即使不是破坏性命令,也可能制造长时间阻塞。生产应用应该使用独立 ACL 用户,只开放需要的 key 前缀和命令类别,并显式移除危险与管理命令。管理账号、排障账号和应用账号不要共用。
MONITOR 看起来像最直接的抓包工具,但它会把几乎所有命令持续推给客户端。繁忙实例上可能显著降低吞吐。优先使用指标、慢日志、采样追踪和客户端命名;只有在明确时间窗口、数据风险和停止方式后,才短暂使用 MONITOR。
遇到“Redis 偶尔卡一下”,可以按下面顺序收窄范围:
先从应用侧确认超时发生在哪个命令、哪个 key 模式、哪个实例,并区分连接获取、网络往返和 Redis 执行时间。
查看实例 CPU、内存、网络、连接数、命中率、过期和淘汰情况,再对照 SLOWLOG 与延迟事件,不要只盯平均延迟。
对可疑 key 使用 TYPE、长度命令、MEMORY USAGE 和 TTL 做抽样。避免第一步就执行全量返回命令。
确认是大 key、热 key、慢命令、脚本、持久化还是网络问题后,再选择限批扫描、拆 key、限制返回、改命令或迁移流量。
下面的练习不考冷门参数,重点是判断边界。先自己选,再展开解析。
你要创建一个只在 key 不存在时写入、20 分钟后自动失效的会话。下面哪条命令最合适?
一个 Hash 有 30 万个字段,接口每次只需要其中 4 个字段。你会怎么读?
你要展示一篇热门文章每天的大致 UV,只关心数量,不需要用户列表,可以接受不到 1% 左右的统计误差。应该用 Set 还是 HyperLogLog?
线上发现 cache:catalog 是一个拥有百万字段的 Hash。现在要逐步下线它,合理步骤是什么?
以后准备在代码里加入一条 Redis 命令,可以在提交前快速过一遍:
UNLINK 方案?命令选得好,通常不是因为你记住了更多缩写,而是因为你知道每条命令替你完成了哪一步,又把哪些成本留给了主线程、网络和客户端。