跳到主要内容

Redis 常用命令与实战手册

这份手册围绕日常开发和排障中最常见的需求:缓存、计数、对象字段、去重、列表、排行榜、限流和问题定位。“覆盖 80%”是选材目标,不是统计结论;真正能用于工作,需要把命令、数据类型、过期时间和并发边界一起记住。

适用范围:示例采用 Redis Open Source 6.2 起支持的语法,兼顾后续版本;这是语法基线,不是部署版本建议。新版本特性另行注明。官方文档核对日期:2026-09-17。

示例约定:bash 代码在操作系统终端执行,text 中的命令在 redis-cli 交互界面执行;Lua 代码需通过 EVAL 或客户端脚本接口调用。示例使用测试环境的 demo: 前缀,各组独立操作示例键。返回值说明假设这些键没有旧数据,也没有其他客户端修改;除明确标注的集群示例外,按单机数据库 0 演示。

想直接动手操作:先看第 15 节的完整案例,按“业务需求 → 执行命令 → 对照结果 → 理解边界”学习,再回到前面的命令表查参数。

1. 优先记住这张表​

先掌握“第一优先级”,覆盖常见业务读写;第二轮再学习组合操作和排障。后面的 Stream、位图、地理位置按业务需要查阅。

优先级工作需求优先记住的命令一句话记忆
第一缓存一个值或一份 JSONSET、GET、MGET写缓存通常把 EX 一起带上
第一判断、查类型、删除EXISTS、TYPE、DEL、UNLINK不知道类型先 TYPE,大键优先 UNLINK
第一控制生命周期EXPIRE、TTL、PTTL秒、毫秒要分清;-1 没过期时间,-2 不存在
第一并发计数INCR、INCRBY、DECR让 Redis 加减,不在客户端“先读再写”
第一修改对象的部分字段HSET、HGET、HMGET、HDEL、HINCRBYHash 是一个键里面的字段表
第一去重、点赞、是否加入SADD、SREM、SISMEMBER、SCARDSet 成员唯一,顺序不保证
第一最近记录、简单队列LPUSH、RPUSH、LPOP、LRANGE、LTRIM、LLEN同端进出是栈,异端进出是队列
第一排行榜、按分值查询ZADD、ZINCRBY、ZRANGE、ZREVRANK、ZSCORE、ZREMZSet 的成员唯一,按分数排序
第二按前缀找键SCAN、HSCAN、SSCAN、ZSCAN用游标分批查,不在生产库全量 KEYS
第二查慢、查内存、查复制INFO、SLOWLOG GET、MEMORY USAGE先拿证据,再改配置
第二多步并发操作MULTI、EXEC、WATCH、EVALPipeline 减少往返,事务与脚本处理执行边界

选数据结构时,可以先问自己:整份读写用 String;字段单独更新用 Hash;只关心有无用 Set;关心插入顺序用 List;关心分数或时间顺序用 ZSet。

2. 连接、命名与返回值​

2.1 连接到正确的实例​

本地测试实例:

redis-cli -h 127.0.0.1 -p 6379 -n 0

需要用户名、密码和 TLS 的环境,替换下面的地址、用户名和证书路径。--askpass 交互输入密码,避免把密码写进命令历史;TLS 用于加密连接。

redis-cli -h redis.example.com -p 6379 --user app --askpass --tls --cacert /path/to/ca.pem

进入交互界面后:

PING
INFO server
ACL WHOAMI
SELECT 0

PING 通常返回 PONG;INFO server 的 redis_version 是服务端版本,redis-cli --version 只显示客户端版本。SELECT 切换当前连接的逻辑数据库;Redis Cluster 只支持数据库 0。逻辑数据库共用实例资源,不能代替环境和权限隔离。连接参数见 Redis CLI 官方文档,集群数据库限制见 Cluster 文档。

2.2 键名和类型​

建议键名表达环境、应用、业务和对象,例如 prod:shop:cache:user:42。冒号只是命名约定,Redis 不会因此建立目录。不要把所有用户塞进一个无限增长的 Hash 或 Set。

一个键对应一个值及其数据类型。demo:cache:user:42 可以存 JSON 字符串;demo:user:42 可以存 Hash,二者是两个不同的键。对 Hash 执行 GET 通常会报 WRONGTYPE,应使用 HGET 或 HMGET。

2.3 不要把所有返回值都当成“成功或失败”​

返回值常见含义
OK命令执行成功,例如普通 SET
(nil) / 客户端的空值键不存在,或 SET ... NX 条件不满足;依命令判断
整数 0、1 或更大可能是新增数、删除数、成员数量或布尔结果
空数组查询没有匹配的元素
WRONGTYPE使用了不适合当前值类型的命令

例如,HSET 更新一个已有字段可能返回 0,意思是没有新增字段,更新仍然成功。客户端对返回值的包装不同,要看对应方法的约定。

3. 通用键命令:查找、过期、删除​

命令示例用途和结果
TYPE demo:user:42查类型,例如 hash;不存在为 none
EXISTS demo:user:42单键存在返回 1,不存在返回 0;多键返回存在数量
EXPIRE demo:user:42 600从现在起 600 秒过期;成功设置返回 1
PEXPIRE demo:user:42 1500从现在起 1500 毫秒过期
TTL demo:user:42剩余秒数;非负数表示剩余时间
PTTL demo:user:42剩余毫秒数;需要更细粒度时使用
PERSIST demo:user:42移除过期时间,不是“保存到磁盘”
DEL demo:user:42删除键,返回实际删除数量
UNLINK demo:user:42从键空间移除,内存回收交给后台处理
SCAN 0 MATCH demo:user:* COUNT 100从游标 0 开始,分批查找匹配键

3.1 过期时间最容易出错的地方​

TTL / PTTL 返回 -1 表示键存在但没有过期时间,返回 -2 表示键不存在。TTL 返回 0 也属于非负剩余时间,不代表“永不过期”。见 TTL。

SET demo:ttl "v1" EX 60
TTL demo:ttl
SET demo:ttl "v2"
TTL demo:ttl

第一次 TTL 接近 60;普通 SET 覆盖后,第二次返回 -1。需要保留原过期时间时使用 SET demo:ttl "v2" KEEPTTL,需要重新计时时使用 SET demo:ttl "v2" EX 60。KEEPTTL 不会为原本没有 TTL 的键补上 TTL。见 SET。

INCR、HSET、LPUSH 等原地修改已有键的命令,保留该键已有的过期时间;新建出来的键不会自动带 TTL。EXPIRE 再次执行会重设剩余时间,非正数过期时间会使键被删除。Redis 通过访问时检查和后台周期检查清理过期键,因此不能把 TTL 当成准点执行任务的定时器。见 EXPIRE。

3.2 SCAN 的正确循环​

SCAN 0 MATCH demo:user:* COUNT 100

结果分两部分:下一次游标、这一批键。把返回的游标用于下一次请求,直到返回游标为 0。例如本次返回 37,下一次才是 SCAN 37 MATCH demo:user:* COUNT 100。

  • COUNT 100 是工作量提示,不能保证恰好返回 100 个键。
  • 本批为空也不能停止,是否结束只看游标。
  • 可能返回重复键;边遍历边修改时,结果不是某个时刻的完整快照。
  • MATCH 不是前缀索引,一次完整遍历仍可能扫描整个键空间。
  • HSCAN、SSCAN、ZSCAN 用来遍历单个集合,遵守相同的游标规则。
  • Cluster 下需要遍历各主节点;redis-cli -c --scan 不会自动扫完整个集群。

在终端快速查找本地测试实例的键,可以使用:

redis-cli -h 127.0.0.1 -p 6379 -n 0 --scan --pattern 'demo:user:*'

批量清理按“确认前缀 → 分批扫描 → 小批删除 → 限速和核对”处理。并发业务可能重新创建同名键,扫描后再删存在时间差;需要严格区分新旧数据时,应使用缓存版本前缀等机制。不要直接把扫描结果通过 xargs 全量删除,键名含空格、换行及集群跨槽时也容易出错。见 SCAN 和 UNLINK。

4. String:缓存、计数、短期占位​

4.1 常用命令​

命令示例解决什么需求
SET demo:cache:user:42 '{"name":"小王"}' EX 300整份缓存,有效期 5 分钟
GET demo:cache:user:42读取字符串;不存在返回空值
MGET demo:cache:user:42 demo:cache:user:43一次读多个字符串,结果与键顺序对应
MSET demo:cfg:a "on" demo:cfg:b "off"一次设置多个值;不支持同时指定 TTL
SET demo:guard:job:42 "request-token" NX EX 30键不存在时才创建,并同时设置过期时间
SET demo:cfg:a "off" XX只覆盖已经存在的键
INCR demo:views:42加 1;键不存在时从 0 开始
INCRBY demo:views:42 10加指定整数,返回更新后的值
DECR demo:stock:42减 1;不会自动阻止减到负数
GETEX demo:session:abc EX 1800读字符串并重新设置 30 分钟有效期
GETDEL demo:temp-result:42原子地取出字符串并删除
STRLEN demo:cache:user:42字符串字节数,不是中文字符数

SET ... NX 成功返回 OK,键已存在则返回空值。不要用 EXISTS 再 SET 代替它,两个请求之间可能被其他客户端插入。NX 只判断是否存在,不比较旧值;XX 也不是“旧值相等才更新”。

需要缓存过期时间时,优先 SET ... EX/PX,不要拆成 SET 和 EXPIRE 两次调用,中间崩溃可能留下永久键。MSET 覆盖也不会保留原 TTL;批量写带 TTL 的缓存可用客户端 Pipeline 发送多条 SET ... EX。批量覆盖的语义见 MSET。

MGET 对不存在的键以及非 String 键都返回对应位置的空值,排查时不能只据此断言数据不存在。相关语义见 String、MGET 和 SET。

4.2 计数与读取后删除的边界​

并发计数不要执行“GET → 本地加 1 → SET”。两个请求可能同时读到 10,最后都写入 11。INCR 直接由 Redis 完成加法;值必须是整数表示,且不能超出有符号 64 位整数范围。需要精确金额时,优先存整数分并使用整数加减,避免用浮点数累计金额。见 INCR。

GETEX 适合滑动续期的会话,但不适合任何缓存都“读一次续一次”,否则热点旧数据可能持续不刷新。会话如果还有绝对有效期,要在应用里另行判断。见 GETEX。

GETDEL 适合“领取后移除”的临时结果。它不负责校验用户输入:验证码如果要求“输错还能重试”,不能先 GETDEL 再比较;应在一个短脚本内比较、成功后删除,并限制尝试次数。见 GETDEL。

5. Hash:对象字段、购物车​

Hash 可以理解为一个键下的字段表,例如一个用户的 name、city、login_count,字段值仍由应用编码、解码。

HSET demo:user:42 name "小王" city "上海" login_count 0
HGET demo:user:42 name
HMGET demo:user:42 name city
HINCRBY demo:user:42 login_count 1
HEXISTS demo:user:42 city
HLEN demo:user:42
HDEL demo:user:42 city
EXPIRE demo:user:42 600

依次可以得到姓名、两个字段值、递增后的登录次数、字段是否存在、字段数量。最后的 EXPIRE 设置整个 Hash 键的有效期。

需求命令注意点
查看一个小对象所有字段HGETALL demo:user:42返回全部字段和值,只用于大小可控的对象
遍历较大的 HashHSCAN demo:user:42 0 COUNT 100按游标继续,不是固定页码
购物车增加某商品数量HINCRBY demo:cart:42 sku:1001 1数量减到 0 时是否删字段,由业务决定
多字段写入HSET key field1 value1 field2 value2新代码用 HSET,旧 HMSET 已弃用

String JSON 适合整份读取、整份替换;Hash 适合局部字段读写。不要为了使用 Hash,把所有用户都放进 users 一个键,导致大键、过期粒度和集群分布问题。

HSET 与 EXPIRE 分开发送也有中间失败窗口;新建 Hash 且必须带 TTL 时,在参数与类型正确的前提下,用同一事务或短脚本组合。Redis 6.2 的 Hash 只能设置整个键的 TTL;Redis 7.4 起提供 HEXPIRE 等字段过期命令,使用前检查实际服务端和客户端支持。

参考:Hash、HMSET 弃用说明、HEXPIRE 版本说明。

6. List:最近记录与简单队列​

List 保留插入顺序,允许重复值;它不是按分数排序的集合。

6.1 最近浏览记录​

LPUSH demo:recent:42 item:1001
LPUSH demo:recent:42 item:1002
LRANGE demo:recent:42 0 9
LTRIM demo:recent:42 0 99
LLEN demo:recent:42

LRANGE ... 0 9 读取前 10 项,示例顺序为 item:1002、item:1001。LTRIM ... 0 99 只保留最近 100 项。索引从 0 开始,区间两端都包含,-1 表示最后一项。

重复浏览会重复插入;要按商品去重并按最后访问时间排序,可考虑 ZSet。并发写入时若需要把“插入和截断”作为一个操作执行,可放在同一事务内。

6.2 先进先出的简单队列​

RPUSH demo:queue:email task:1 task:2
LPOP demo:queue:email
BLPOP demo:queue:email 5

RPUSH + LPOP 先取出 task:1,再取出 task:2。BLPOP 在没有数据时等待,最多 5 秒;阻塞的是等待的客户端连接,Redis 仍可服务其他客户端。阻塞消费宜使用专用连接。

弹出成功后,任务已经从 List 删除。如果消费者此时崩溃,任务可能丢失。需要确认和重试时,考虑 Stream,或用 LMOVE / BLMOVE 转入处理中列表并自行维护确认、超时回收;普通 List 队列没有自动确认机制。见 List 官方说明。

7. Set:去重、点赞、集合关系​

SADD demo:likes:article:1001 user:42 user:43
SADD demo:likes:article:1001 user:42
SISMEMBER demo:likes:article:1001 user:42
SCARD demo:likes:article:1001
SREM demo:likes:article:1001 user:42

第一次 SADD 返回新增的 2 个成员;重复添加 user:42 返回 0。取消点赞前,SISMEMBER 返回 1,SCARD 返回 2。只要总数可直接用 SCARD 获取,就不必额外维护一个容易不同步的计数键。

需求命令示例
查看小集合SMEMBERS demo:likes:article:1001
遍历大集合SSCAN demo:likes:article:1001 0 COUNT 100
共同关注SINTER demo:follow:42 demo:follow:43
合并去重SUNION demo:follow:42 demo:follow:43
42 关注了、43 没关注SDIFF demo:follow:42 demo:follow:43
随机抽取但保留成员SRANDMEMBER demo:candidates 2
随机抽取并移除成员SPOP demo:candidates 2

SDIFF 的顺序有意义,反过来就是另一个结果。交并差集可能遍历大量成员,不能认为一次命令就一定便宜;多键集合运算在 Cluster 中也受同槽限制。资料:Set。

8. ZSet:排行榜和时间范围查询​

ZSet 保存唯一成员 member 和对应分数 score。成员不重复,分数允许重复;默认按分数升序,同分按成员的字节字典序排列,并非按插入先后。

8.1 排行榜​

ZADD demo:rank:weekly 100 user:42 80 user:43 120 user:44
ZINCRBY demo:rank:weekly 30 user:43
ZRANGE demo:rank:weekly 0 9 REV WITHSCORES
ZREVRANK demo:rank:weekly user:43
ZSCORE demo:rank:weekly user:43
ZCARD demo:rank:weekly

此时分数为 user:44 = 120、user:43 = 110、user:42 = 100。ZRANGE ... REV 获取降序榜单;ZREVRANK 返回从 0 开始的名次,展示给用户时再加 1。成员不存在时名次为空值,不要显示为第 1 名。

8.2 按分数查询与删除​

ZRANGE demo:rank:weekly 100 120 BYSCORE WITHSCORES
ZRANGE demo:rank:weekly +inf 100 BYSCORE REV LIMIT 0 10 WITHSCORES
ZCOUNT demo:rank:weekly 100 120
ZREM demo:rank:weekly user:42
ZREMRANGEBYSCORE demo:rank:weekly -inf 80

第一条按分数查询闭区间 [100, 120];加 REV 时,分数范围的参数也要按“最大值、最小值”填写。上界、下界可用 +inf、-inf;用 "(100" 表示排除 100。

ZRANGE key 0 9 是按名次取区间,加 BYSCORE 后才按分数查询。大偏移量的 LIMIT 仍有遍历成本,持续变化的榜单也不提供分页快照。

Redis 6.2 起可以用 ZRANGE ... REV/BYSCORE 表达常用的降序和按分数查询;维护旧代码时还会见到 ZREVRANGE、ZRANGEBYSCORE。见 ZRANGE。

把分数换成时间戳,也能找出“截止某时刻到期的任务”。但“查到任务 → 删除任务”分两次执行会被并发消费者重复领取;领取要原子完成,处理失败还需要确认与重试设计。不要把一个 ZSet 查询等同于完整可靠的延迟队列。

9. 多步操作:Pipeline、事务、Lua 怎么选​

原子执行在这里指操作执行过程中,其他客户端不能插进来修改相关数据;它不自动代表出错回滚、已经落盘,或已经复制到从节点。

方式适合解决什么问题不能指望它解决什么
单条命令INCR 计数、SADD 去重、SET ... NX EX 占位不能把随后访问数据库的操作一起包住
Pipeline客户端连续发送一批命令,减少网络往返不保证整批原子执行,也不自动回滚
MULTI / EXEC多条已确定命令连续执行,中途不穿插其他客户端命令不支持根据中间返回值在客户端临时分支,不提供回滚
WATCH + 事务先读再计算;提交前检查读过的键是否被修改冲突后需重新读取、计算并有限重试
Lua / EVAL“读取 → 判断 → 更新”在服务端一次完成长脚本会阻塞命令执行,运行时错误不会自动撤销已做的写入

Pipeline 通常是客户端 API,不存在一条通用的 PIPELINE Redis 命令。批量大小要同时限制条数和字节数,逐条检查返回结果。更多见 Pipeline 和 Lua 脚本。

9.1 两条固定命令一起提交​

MULTI
LPUSH demo:recent:42 item:1003
LTRIM demo:recent:42 0 99
EXEC

EXEC 前的命令先进入队列,通常返回 QUEUED;EXEC 后逐条返回结果。未执行时可用 DISCARD 放弃队列。

  • 命令入队阶段发现语法等错误,事务会被标记失败,EXEC 不执行这批命令。
  • 执行阶段某条命令因类型不符等原因失败,其他命令仍可能执行,已经成功的操作不回滚。
  • WATCH 要放在读取之前,在同一条连接完成 WATCH → 读取 → MULTI → 写入 → EXEC。被监视键期间发生修改,包括过期,EXEC 可返回空结果;这表示冲突未提交,应重新读后重试。
  • 放弃业务操作时用 UNWATCH 清理监视状态;不能把带监视状态的连接直接当成干净连接交回连接池。

事务具体保证见 Transactions。

9.2 一个计数与过期必须一起完成的限流例子​

假设规则是:一个用户从本窗口首次请求开始,60 秒内允许 100 次请求;拒绝的请求也记入计数。键只由这段逻辑维护,不被其他代码重置或减计数。

EVAL "local n = redis.call('INCR', KEYS[1]); if n == 1 then redis.call('EXPIRE', KEYS[1], 60) end; return n" 1 demo:rate:api:user:42

应用读取返回计数:不超过 100 放行,超过 100 拒绝。EVAL 中的 1 表示后面有 1 个键参数,脚本通过 KEYS[1] 访问它。

这段脚本把“首次计数”和“设置 TTL”放在一起,避免客户端在两条命令中间崩溃,留下永久计数键。只在新窗口第一次请求设置过期时间,否则每次续期会改变限流规则。此方案是固定窗口,有窗口交界处的突发问题,不能保证任意连续 60 秒都不超过 100 次;更严格的需求再考虑滑动窗口或令牌桶。算法思路参考 INCR 的限流示例。

脚本只做短小、可控的逻辑。把键放进 KEYS,普通参数放进 ARGV;不要用拼接用户输入的方式生成 Lua 代码。动态参数和类型要尽量在写入前校验,避免脚本中途报错留下部分修改。高频脚本可由客户端使用 SCRIPT LOAD / EVALSHA,并处理脚本缓存不存在时的重新加载。见 Lua 脚本。

10. 把命令用于实际业务​

10.1 缓存查询与更新​

常用的 Cache Aside,也叫旁路缓存,流程是:

读取:GET 缓存 → 命中则返回 → 未命中查数据库 → SET 缓存并设置 TTL
更新:先提交数据库事务 → 成功后删除对应缓存 → 后续读取重新加载

数据库是权威数据源,TTL 根据业务允许的陈旧时间选择。更新数据库后删除缓存,是便于维护的常见起点;相关模式见 Redis Cache Aside。

它仍有并发窗口:请求 A 查到旧数据后暂停;请求 B 更新数据库并删除缓存;A 恢复后又把旧数据写回缓存。删除失败也可能留下旧值。因此,这个流程加上 TTL 并不等于强一致。

按业务要求补强:缓存删除失败要有可追踪的重试;更严格的场景使用数据版本校验、数据库变更事件驱动失效,或直接从数据库读取关键结果。不能用“Redis 很快”或固定延迟再删一次,代替并发一致性设计。

10.2 穿透、击穿、雪崩怎么区分​

问题具体例子常用处理
缓存穿透持续请求数据库也没有的用户 ID校验输入;短时间缓存“没有该记录”的明确标记;按需增加布隆过滤器
缓存击穿一个热门商品缓存刚过期,大量请求同时查库合并同键加载请求,或短期互斥重建;等待方有超时与降级策略
缓存雪崩大量缓存同一时间过期,或 Redis 整体不可用TTL 加随机偏移;限制回源并发;预热与降级,保护数据库

“空值缓存”要能区分没缓存与已确认不存在,且设置较短 TTL;新增记录后也要清除对应的“不存在”缓存。布隆过滤器是一种节省内存的集合判断结构,可能把不存在的值判断为可能存在,维护规则要与数据新增保持一致。

热门键加载合并的做法可参考 官方缓存示例。上表是应用方案选择,不是 Redis 自动提供的完整保护机制。

10.3 短期互斥锁:会写加锁,也要会安全解锁​

加锁:

SET demo:lock:rebuild:user:42 "unique-request-token" NX PX 10000

实际程序每次获取锁都生成新的随机 token,不能照抄示例的固定字符串。只有拿到 OK 的请求才开始受保护的工作。锁最长 10 秒;超时或返回不明确时,不能假定已经安全持有锁。

释放时,比较 token 与删除必须在同一个原子操作内:

EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end; return 0" 1 demo:lock:rebuild:user:42 "unique-request-token"

假设 A 的锁已过期,B 重新拿到锁;A 工作结束后若直接 DEL,会删掉 B 的锁。比较 token 能防止这种误删,但不能阻止过期后的 A 继续执行业务。

还要考虑任务超时、续期失败、进程暂停和主从切换时的锁丢失。涉及扣款、库存等正确性要求时,需由数据库条件更新、唯一约束,或受保护资源支持的递增令牌校验阻止旧请求继续写入;只靠这个锁示例不够。以上限制见 Redis 分布式锁文档。

10.4 防重复请求与超时重试​

SET ... NX EX 可以在一段时间内抢占一个请求 ID,但抢占成功后进程仍可能崩溃。它不能独自证明业务已经成功,更不能保证订单永不重复创建。

涉及持久业务时,记录“处理中 / 已成功 / 失败”等状态及结果,并用数据库唯一约束等机制兜底。这里的幂等指同一个业务请求重试,不重复产生业务效果。

客户端报超时也不代表 Redis 没执行。例如 INCR 已执行但响应丢失,盲目重试会再加一次;普通 SET 重试虽然写入相同值,也可能重新计算 TTL。重试策略需要区分命令和业务语义。

11. 排障:从现象找到要看的命令​

11.1 第一轮检查​

以下命令用于读取状态;托管环境或受限账号可能没有部分权限。

PING
INFO server
INFO clients
INFO memory
INFO stats
INFO commandstats
INFO replication
INFO persistence
INFO keyspace
SLOWLOG GET 10
现象先查什么如何理解证据
键“突然没了”实例地址、数据库号、TYPE、TTL、INFO stats检查过期、业务删除、内存淘汰,也检查是否连错环境
写入提示 OOMINFO memory、CONFIG GET maxmemory、CONFIG GET maxmemory-policy看内存上限和淘汰策略,不能只靠增加重试
接口变慢SLOWLOG GET 10、INFO commandstats看命令执行耗时及调用量,再查网络与应用连接池等待
连接数异常INFO clients、按需 CLIENT LIST看连接泄漏、阻塞连接、连接池配置
读到旧数据、写报 READONLYINFO replication、ROLE确认主从角色、读路由及复制状态
重启后数据缺失INFO persistence、持久化配置、服务日志确认是否开启持久化,以及最近保存是否成功
命令报 CROSSSLOTCLUSTER KEYSLOT key多键操作涉及不同槽位,不能用重试解决
命令报 MOVED客户端的 Cluster 配置服务端要求访问正确节点,应用要使用集群客户端
报 NOAUTH / NOPERM认证参数、ACL WHOAMI分别排查未认证及账号权限不足

INFO stats 中,expired_keys 是过期键累计数量,evicted_keys 是因内存限制淘汰的累计数量;它们不能单独证明某个具体键的删除原因。keyspace_hits / keyspace_misses 是键查找统计,观察时间段内的增量通常比历史总数更有用,也不能直接替代应用层缓存命中率。字段含义见 INFO。

SLOWLOG 记录服务端执行命令的耗时,单位是微秒,不包含客户端网络往返和连接池等待;没有慢日志不代表接口一定快。见 SLOWLOG GET。

LATENCY LATEST / LATENCY DOCTOR 可进一步辅助排查,但需要先配置非零的 latency-monitor-threshold 开启采样,阈值单位为毫秒;无记录不能排除问题。见 延迟监控。

11.2 大键和热键​

大键是单个键的值很大或成员很多;热键是访问过于集中。一个很短的字符串也可能是热键,二者处理方向不同。

定位已知键:

TYPE demo:user:42
MEMORY USAGE demo:user:42
HLEN demo:user:42

根据类型换用 STRLEN、LLEN、SCARD、ZCARD,不要把上述命令都对同一个键执行。MEMORY USAGE 返回字节数;集合默认通过采样估算,SAMPLES 0 全量取样可能更贵。见 MEMORY USAGE。

需要扫描测试实例寻找大键时:

redis-cli -h 127.0.0.1 -p 6379 -n 0 --bigkeys -i 0.1
redis-cli -h 127.0.0.1 -p 6379 -n 0 --memkeys -i 0.1

--bigkeys 按类型报告字符串长度或集合成员数,--memkeys 看内存占用。它们会扫描键空间,生产环境应低峰、限速并观察影响,Cluster 要覆盖各节点。--hotkeys 依赖 LFU 淘汰策略下的访问频次计数,不是所有实例都可直接使用;也可从应用监控识别热点请求。工具说明见 Redis CLI。

大键通常通过限制集合长度、拆分和批量访问来处理,删除优先考虑 UNLINK。热键可考虑合并请求、本地缓存或读取副本,同时明确数据一致性要求;单纯增加 Cluster 节点不会自动拆开一个热键。

11.3 这些操作先想清楚再执行​

操作主要影响常见替代或使用边界
KEYS *一次遍历整个键空间,拖慢其他请求用 SCAN 分批遍历
大集合的 HGETALL / SMEMBERS / LRANGE 0 -1大量计算、内存和网络响应指定字段、限制范围,或使用扫描命令
大键 DEL回收复杂对象可能长时间占用主线程优先 UNLINK,并控制批量大小
MONITOR持续输出命令流,增加开销,也会暴露业务参数优先慢日志和指标,确需使用时限定时间
SAVE同步生成快照,阻塞服务按持久化方案使用后台快照,仍要评估资源
FLUSHDB / FLUSHALL清空当前数据库 / 所有数据库不是日常缓存清理命令;ASYNC 也不缩小删除范围

在 Redis 6.2 的常见模型中,命令执行主要由主线程完成,网络 I/O 和部分后台工作可由其他线程或进程承担。不能据此说“Redis 不会受 CPU 限制”:大集合操作、长 Lua 和高频请求都可能让命令执行成为瓶颈。

12. 必须配套理解的运行机制​

12.1 过期与内存淘汰是两回事​

过期回答“这个值还能有效多久”;淘汰回答“内存到上限后让谁腾位置”。带 TTL 的键可能在到期前就被淘汰,没有 TTL 的键也可能被 allkeys-* 策略淘汰。

策略含义常见选择依据
noeviction不主动淘汰;受内存限制的写命令会失败不能随意丢数据时,需要容量保障和写失败处理
allkeys-lru在所有键中近似淘汰最近较少使用的键常见缓存场景
allkeys-lfu在所有键中近似淘汰低频访问键访问频率能较好体现热点时
volatile-lru / volatile-lfu只从带 TTL 的键中按上述规则选择没有可淘汰键时仍可能写入失败
volatile-ttl优先淘汰即将到期的键TTL 能反映保留价值时

LRU 关注最近用没用,LFU 关注一段时间内用得多不多;Redis 的实现是近似算法。可重建的缓存,与锁、消息队列等不希望被淘汰的数据,通常应分开配置实例。策略选择见 Key eviction。

12.2 持久化、复制、哨兵、集群分别解决什么​

机制解决的问题工作中要记住的边界
RDB 快照保存某个时刻的数据状态,用于恢复和备份故障可能丢失最近快照之后的更新
AOF 日志记录写入以便重放恢复appendfsync everysec 通常存在约一秒的数据丢失窗口,并非零丢失保证
主从复制把主节点数据复制给副本默认异步,从节点可能读到旧值,切换可能丢失已确认写入
Sentinel 哨兵监测主从并自动故障切换,供客户端发现主节点不负责把数据拆到多个分片
Cluster 集群把数据按槽分配给多个主节点,并配合副本故障切换有多键同槽约束,不能任意跨节点执行事务或脚本

BGSAVE 在后台子进程生成快照;后台执行仍有创建子进程时的暂停,以及额外内存和磁盘开销。AOF 重写是压缩恢复当前数据所需的表示,不是把业务数据清空。持久化和副本都不代替独立备份与恢复验证。见 持久化。

WAIT 可以等待副本确认收到写入,但不把异步主从变成强一致系统,也不等价于落盘保证。复制和自动切换的边界见 复制 与 Sentinel。

12.3 Cluster 的同槽限制​

Redis Cluster 把键分配到 16384 个槽,再把槽分配给各主节点。多键命令、事务和 Lua 脚本通常要求涉及的键处在同一个槽;MGET、多键 DEL、集合交集也在检查范围内。

需要共同操作的少量关联键,可以使用相同的 hash tag,也就是键名中花括号里的内容:

SET demo:user:{42}:name "小王"
SET demo:user:{42}:city "上海"
MGET demo:user:{42}:name demo:user:{42}:city
CLUSTER KEYSLOT demo:user:{42}:name
CLUSTER KEYSLOT demo:user:{42}:city

这些键共享 {42},槽位相同。连接集群时可给 redis-cli 加 -c 跟随重定向;普通单机执行 CLUSTER KEYSLOT 会报未启用集群。不要给所有键使用同一个 tag,否则会把大量数据挤进一个槽,失去分片意义。

有的客户端会把跨槽批量读取拆成多次请求,但这不再是服务端单条命令的原子读取;必须核对客户端行为。详见 Redis Cluster。

13. 按需查阅:消息、位图、估算和地理位置​

13.1 Pub/Sub 与 Stream​

需求优先考虑基本命令
在线订阅者即时接收通知,可容忍丢失Pub/SubPUBLISH、SUBSCRIBE、UNSUBSCRIBE
保存消息、消费者分工、确认与重试StreamXADD、XREADGROUP、XACK、XPENDING、XAUTOCLAIM

Pub/Sub 不保存消息供离线订阅者补读;发布命令返回了订阅者数量,也不意味着业务已处理成功。见 Pub/Sub。

Stream 最小流程如下,在测试环境按顺序执行。创建消费者组只需执行一次,重复创建会报 BUSYGROUP。

XGROUP CREATE demo:events workers 0 MKSTREAM
XADD demo:events * type order-created orderId 1001
XREADGROUP GROUP workers worker-1 COUNT 10 BLOCK 5000 STREAMS demo:events >
XPENDING demo:events workers

0 表示消费者组从头处理,MKSTREAM 在键不存在时创建 Stream;* 让 Redis 生成消息 ID。> 读取尚未投递给该组任何消费者的新消息,BLOCK 5000 的单位是毫秒。业务成功后,把下面的 消息ID 替换为实际返回值再确认:

XACK demo:events workers 消息ID

确认只移除该组的待确认记录,不会删除 Stream 中的消息。消费者崩溃后,未确认消息仍要主动回收处理,例如:

XAUTOCLAIM demo:events workers worker-2 60000 0-0 COUNT 10

这会尝试接管空闲至少 60000 毫秒的待确认消息;按返回游标继续,直到游标为 0-0,后续再周期检查。接管后重新处理,成功后 XACK。正常任务也可能运行很久,因此接管阈值要匹配业务耗时;消费者必须能处理重复投递。

消息不被提前删除、持久化与复制配置符合要求,且应用维护确认和重试流程,才能形成可靠的消费方案。还应限制重试次数、安排失败消息去向,并监控积压。保留长度可用 XADD ... MAXLEN ~ 数量 或 XTRIM 控制,但裁剪可能删除尚未处理的消息,不能照抄一个固定长度就认为安全。

参考:XGROUP CREATE、XREADGROUP、XAUTOCLAIM、XTRIM。

13.2 其他三个实用方向​

场景命令示例必须知道的限制
按日期记录用户是否签到SETBIT demo:signin:2026-09-17 42 1;GETBIT demo:signin:2026-09-17 42;BITCOUNT demo:signin:2026-09-17位图用一个二进制位表示有无;稀疏、极大的用户 ID 会撑大内存,应先映射或分段
统计当天近似去重访客数PFADD demo:uv:2026-09-17 user:42 user:43;PFCOUNT demo:uv:2026-09-17HyperLogLog 只估算数量,不能列出成员或精确判断某人是否来过
查附近门店GEOADD demo:shops 121.4737 31.2304 shop:1;GEOSEARCH demo:shops FROMLONLAT 121.47 31.23 BYRADIUS 5 km ASC COUNT 10 WITHDIST参数先经度后纬度,查询返回半径内的候选门店;不是道路导航距离

位图偏移量会影响内存分配,见 SETBIT。HyperLogLog 的标准误差约为 0.81%,这不是每次结果误差的硬上限,见 HyperLogLog。附近搜索语法见 GEOSEARCH。

14. 工作时的使用顺序​

拿到需求,先确定用哪种数据结构,再确定键的范围、生命周期、并发要求和数据丢失容忍度。

  1. 普通读写先用原生命令:缓存 SET/GET,计数 INCR,去重 SADD,排行 ZADD/ZRANGE。
  2. 有多步依赖就检查执行边界:只是减少网络往返用 Pipeline;读取后要判断再写入时,选择 WATCH 或短 Lua。
  3. 有增长就限制大小:缓存有 TTL,列表和消息有保留策略,批量请求有大小上限。
  4. 上线前覆盖三类例子:键不存在、键已存在且有 TTL、两个请求同时操作;还要明确超时后能否重试。
  5. 出问题先定位范围:实例和数据库 → 键类型与 TTL → 内存与慢日志 → 连接、复制和应用行为。

第一轮只记第 1 节速查表;第二轮练习各类型的小例子;实际接入业务前再对照第 9~12 节检查组合操作与运行边界。这样记住的命令才能转化为可用的工作方法。

15. 常用命令完整操作案例​

本节使用独立的 demo:case: 测试键。只在测试实例执行开头的 DEL,它用于重置本案例列出的键,返回数量取决于是否已做过该案例。后续按顺序执行,并在示例 TTL 到期前核对结果。

命令块只包含输入,可复制到 redis-cli;结果表用逻辑值表达,省略客户端显示的序号、转义和 (integer) 等包装。预期结果根据命令语义推演,本节没有在 Redis 实例上实际执行;TTL、扫描游标、内存和日志内容以实际返回为准。

案例业务需求主要命令
15.1商品详情缓存、批量读取、更新后失效SET、GET、MGET、DEL、TTL
15.2会话续期、保留 TTL、领取一次性结果GETEX、SET ... KEEPTTL、GETDEL
15.3访问计数与过期检查INCR、INCRBY、EXPIRE、TTL
15.4购物车增减商品、修改字段、删除商品HSET、HINCRBY、HMGET、HDEL
15.5点赞去重、取消点赞、找共同关注SADD、SISMEMBER、SCARD、SINTER
15.6保留最近记录、按顺序领取任务LPUSH、LTRIM、RPUSH、BLPOP
15.7更新积分、查看名次与分数区间ZADD、ZINCRBY、ZRANGE、ZREVRANK
15.8按前缀定位并删除指定测试缓存SCAN、UNLINK、EXISTS
15.9两个客户端修改同一值时检测冲突WATCH、MULTI、EXEC
15.1060 秒内最多放行 3 次请求EVAL、INCR、EXPIRE
15.11竞争互斥锁,验证不能误删他人的锁SET ... NX PX、EVAL
15.12排查类型不符和遗漏过期时间TYPE、TTL、HGET、MEMORY USAGE

15.1 商品详情缓存:从未命中到更新失效​

需求:商品 1001 的详情缓存 5 分钟,数据库更新价格后让旧缓存失效。

先模拟首次访问。SET 中的数据代表应用查数据库后得到的结果,Redis 本身不会自动查数据库。

DEL demo:case:product:1001 demo:case:product:1002
GET demo:case:product:1001
SET demo:case:product:1001 '{"name":"keyboard","price_cents":19900}' EX 300
GET demo:case:product:1001
TTL demo:case:product:1001
MGET demo:case:product:1001 demo:case:product:1002
操作预期结果应用怎么处理
第一次 GET空值缓存未命中,去数据库查询
SET ... EX 300OK详情和 300 秒有效期一起写入
第二次 GET{"name":"keyboard","price_cents":19900}反序列化后返回详情
TTL刚写入时接近 300,随后递减验证确实设置了过期时间
MGET[商品 1001 的 JSON, 空值]结果按输入键顺序对应,1002 单独回源

假设应用已经在数据库中将价格更新为 18900 分,并成功提交事务,再执行:

DEL demo:case:product:1001
GET demo:case:product:1001
SET demo:case:product:1001 '{"name":"keyboard","price_cents":18900}' EX 300
GET demo:case:product:1001

在原键未过期的前提下,删除返回 1,紧接的读取为空,重新填充后读到新价格。最后的 SET 模拟下一次请求查库后的回填。这里演示正常路径,并发旧值回填和删除失败按第 10.1 节处理。

15.2 会话续期:读一次,再保留或修改有效期​

需求:会话初始有效期 5 分钟,访问后延长为 30 分钟;修改会话内容时保留当前剩余时间。

DEL demo:case:session:abc
SET demo:case:session:abc "user:42" EX 300
GETEX demo:case:session:abc EX 1800
TTL demo:case:session:abc
SET demo:case:session:abc "user:42:verified" KEEPTTL
GET demo:case:session:abc
TTL demo:case:session:abc

GETEX 返回原值 user:42,同时把有效期重新设为 1800 秒。修改后 GET 返回 user:42:verified;两次 TTL 均为剩余时间,后一次通常更小,不应变为 -1。普通 GET 不续期。

另一个场景:临时结果只能被领取一次。

DEL demo:case:result:job:42
SET demo:case:result:job:42 "download-ready" EX 300
GETDEL demo:case:result:job:42
GETDEL demo:case:result:job:42

第一次领取返回 download-ready,第二次为空。如果第一次已经删除但网络响应丢失,重试也读不到原结果;需要可靠重试时,应保留结果并维护领取状态。相关语义见 GETEX 和 GETDEL。

15.3 访问计数:原子累加不等于自动过期​

需求:统计一篇文章的访问次数,观察计数命令与 TTL 的关系。

DEL demo:case:views:article:1001
INCR demo:case:views:article:1001
INCR demo:case:views:article:1001
INCRBY demo:case:views:article:1001 10
GET demo:case:views:article:1001
TTL demo:case:views:article:1001
EXPIRE demo:case:views:article:1001 600
INCR demo:case:views:article:1001
TTL demo:case:views:article:1001
操作预期结果
两次 INCR依次返回 1、2
INCRBY ... 10返回 12
GET字符串 "12"
第一次 TTL-1,计数键没有自动获得过期时间
EXPIRE ... 600返回 1
再次 INCR返回 13
最后 TTL不超过 600 的剩余秒数,计数没有清掉原 TTL

这统计的是次数,同一个用户刷新两次也计两次。这里分开发命令是为了观察行为;业务要求计数与首次过期同时完成时,使用第 15.10 节的脚本。参考:INCR。

15.4 购物车:字段是商品,字段值是数量​

需求:用户 42 放入两种商品,增加一种商品的数量,再移除另一种商品。价格仍由商品或订单系统校验,不由购物车中的数量决定。

DEL demo:case:cart:42
MULTI
HSET demo:case:cart:42 sku:1001 2 sku:1002 1
EXPIRE demo:case:cart:42 86400
EXEC
HINCRBY demo:case:cart:42 sku:1001 1
HMGET demo:case:cart:42 sku:1001 sku:1002 sku:9999
HSET demo:case:cart:42 sku:1002 4
HLEN demo:case:cart:42
HDEL demo:case:cart:42 sku:1002
HGETALL demo:case:cart:42
操作预期结果要记住的含义
初始 EXEC[2, 1]新增 2 个字段,设置 TTL 成功;事务内命令此前返回 QUEUED
HINCRBY3商品 1001 的数量从 2 变为 3
HMGET["3", "1", 空值]商品 9999 不在购物车
HSET 修改商品 10020更新已有字段成功,没有新增字段
HLEN2有 2 种商品,不是总件数 7
HDEL1删除 1 个商品字段
HGETALL仅有 sku:1001 = 3示例对象很小,可以一次取完

HINCRBY 允许把数量减成 0 或负数,不会自动删除字段。若业务要求“数量必须为正,减到 0 就移除”,校验与修改应放在同一个脚本或使用 WATCH 的事务中。返回值见 HSET 和 HINCRBY。

15.5 点赞与共同关注:利用集合去重​

需求:同一用户对文章重复点赞不增加人数,取消后再查状态。

DEL demo:case:likes:1001
SADD demo:case:likes:1001 user:42
SADD demo:case:likes:1001 user:42
SADD demo:case:likes:1001 user:43
SISMEMBER demo:case:likes:1001 user:42
SCARD demo:case:likes:1001
SREM demo:case:likes:1001 user:42
SISMEMBER demo:case:likes:1001 user:42
SCARD demo:case:likes:1001

三次 SADD 依次返回 1、0、1。取消前用户 42 的点赞状态为 1,人数为 2;取消后状态为 0,人数为 1。应用把 SISMEMBER 的结果转换成是否点亮按钮,把 SCARD 的结果显示为点赞人数。重复添加的语义见 SADD。

扩展场景:查询两个用户共同关注的人。

DEL demo:case:follow:42 demo:case:follow:43
SADD demo:case:follow:42 user:1 user:2 user:3
SADD demo:case:follow:43 user:2 user:3 user:4
SINTER demo:case:follow:42 demo:case:follow:43
SDIFF demo:case:follow:42 demo:case:follow:43
SUNION demo:case:follow:42 demo:case:follow:43

交集为 user:2、user:3;差集为 user:1;并集为 user:1、user:2、user:3、user:4。只检查成员是否一致,不依赖返回顺序。大集合需要评估计算量,Cluster 还需处理同槽约束。参考:Set。

15.6 最近记录与任务队列:注意进出方向​

需求一:最近操作记录只保留 3 条。

DEL demo:case:recent:42
LPUSH demo:case:recent:42 page:1
LPUSH demo:case:recent:42 page:2
LPUSH demo:case:recent:42 page:3
MULTI
LPUSH demo:case:recent:42 page:4
LTRIM demo:case:recent:42 0 2
EXEC
LRANGE demo:case:recent:42 0 2
LLEN demo:case:recent:42

前三次插入依次返回长度 1、2、3;EXEC 返回 [4, OK],表示先插入至 4 条再截断。读取结果按顺序是 [page:4, page:3, page:2],长度为 3。正式代码每次插入都应按同样方式截断。LPUSH 返回的是插入后的长度,见 LPUSH。

需求二:按照入队顺序领取两个邮件任务。

DEL demo:case:queue:email
RPUSH demo:case:queue:email task:1 task:2
LPOP demo:case:queue:email
BLPOP demo:case:queue:email 1
BLPOP demo:case:queue:email 1

RPUSH 返回 2;LPOP 返回 task:1;第一次 BLPOP 返回 [demo:case:queue:email, task:2]。第二次 BLPOP 在约 1 秒后返回空值,因为队列已空且没有生产者追加任务。阻塞弹出也会移除任务,领取成功后崩溃的恢复问题仍需另外设计。参考:List。

15.7 积分排行榜:更新积分、取前几名、查区间​

需求:维护 3 个用户的积分,给用户 43 增加 50 分,展示前两名和自己的名次。

DEL demo:case:rank:weekly
ZADD demo:case:rank:weekly 100 user:42 80 user:43 120 user:44
ZINCRBY demo:case:rank:weekly 50 user:43
ZRANGE demo:case:rank:weekly 0 1 REV WITHSCORES
ZREVRANK demo:case:rank:weekly user:42
ZSCORE demo:case:rank:weekly user:43
ZRANGE demo:case:rank:weekly 100 120 BYSCORE WITHSCORES
ZCOUNT demo:case:rank:weekly 100 120
ZREM demo:case:rank:weekly user:44
ZCARD demo:case:rank:weekly
操作预期结果
ZADD3,新增 3 个成员
ZINCRBY新分数 130
降序前两名user:43 = 130、user:44 = 120
用户 42 的 ZREVRANK2,页面显示第 3 名
用户 43 的 ZSCORE130
分数区间 [100, 120]按升序返回 user:42 = 100、user:44 = 120
ZCOUNT2
ZREM / 随后的 ZCARD依次为 1、2

ZADD 再次写入同一个成员会更新分数,默认返回值只数新增成员;名次则由分数重新决定。见 ZADD 和 ZRANGE。

15.8 按前缀定位缓存:查到后只删除指定键​

需求:找出本案例的两个用户缓存,删除它们,同时保留另一类数据。

DEL demo:case:scan:user:42 demo:case:scan:user:43 demo:case:scan:keep
MSET demo:case:scan:user:42 "alice" demo:case:scan:user:43 "bob" demo:case:scan:keep "keep-me"
SCAN 0 MATCH demo:case:scan:user:* COUNT 100

先读返回的游标。若不为 0,把它作为下一次 SCAN 的第一个参数,使用相同的 MATCH 继续扫描。完整一轮结束后,去重后的匹配结果应包含这两个用户键;前提是测试环境没有额外的同前缀键。

游标不固定,第一批也可能为空,因此这里不编造一个能通用的游标数字。确认本次只处理这两个测试键后,再执行:

UNLINK demo:case:scan:user:42 demo:case:scan:user:43
EXISTS demo:case:scan:user:42 demo:case:scan:user:43
GET demo:case:scan:keep

预期依次为 2、0、keep-me。这说明两个用户键已不可访问,保留键未受影响;后台内存回收仍可能尚未结束。正式批量清理时要分批、限速,并按第 3.2 节处理并发重建的风险。参考:SCAN、UNLINK。

15.9 两个客户端争抢最后一份库存​

需求:库存为 1 时,两个请求都读到了 1,只允许一个请求完成扣减。

打开两个连接到同一个测试实例、同一个数据库的 redis-cli 窗口,分别叫 A 和 B。先在 A 中初始化:

DEL demo:case:stock:1001
SET demo:case:stock:1001 1

严格按表从上到下执行,每行只在指定窗口输入命令。A 在读取后暂停,先让 B 提交,以便观察冲突。

顺序窗口输入命令预期结果
1AWATCH demo:case:stock:1001OK
2AGET demo:case:stock:1001"1",应用判断还有库存
3BWATCH demo:case:stock:1001OK
4BGET demo:case:stock:1001"1",B 也判断还有库存
5BMULTIOK
6BDECR demo:case:stock:1001QUEUED
7BEXEC[0],扣减后库存为 0,B 提交成功
8AMULTIOK
9ADECR demo:case:stock:1001QUEUED
10AEXEC空值,通常显示为 (nil);A 监视的键被 B 改过,本次未执行扣减
11AGET demo:case:stock:1001"0",没有减成负数

A 不能拿旧的库存 1 直接重试事务。重新 WATCH、GET 后会读到 0,此时应用应判断库存不足,执行 UNWATCH 并结束请求,不再扣减。

这里必须区分:B 的 [0] 是包含一个整数 0 的成功结果数组,A 的空值表示发生冲突未提交,不是一个空数组。案例仅验证 Redis 内的并发扣减,订单创建、支付取消后的库存恢复和故障切换仍需完整业务方案。参考:Transactions。

15.10 限流:同一用户 60 秒内放行 3 次​

需求:每个窗口从首次请求开始计时;前三次放行,第四次起拒绝,拒绝也计数。

先清理本案例的旧计数:

DEL demo:case:rate:user:42

在 60 秒内把下面同一条命令执行 4 次,分别代表 4 次请求:

EVAL "local n = redis.call('INCR', KEYS[1]); if n == 1 then redis.call('EXPIRE', KEYS[1], 60) end; return n" 1 demo:case:rate:user:42
第几次请求脚本返回应用的决定
11放行,并已开始本窗口的 60 秒计时
22放行
33放行
44拒绝,例如返回 HTTP 429
TTL demo:case:rate:user:42
GET demo:case:rate:user:42

未到期时,TTL 应是递减的非负秒数,计数为 "4"。Redis 只返回计数,应用必须据此判断是否执行业务。原键过期后再次执行脚本,计数重新从 1 开始;窗口交界处的突发限制见第 9.2 节。参考:INCR 限流模式。

15.11 锁竞争:验证失败者不能释放成功者的锁​

需求:两个请求竞争同一个缓存重建任务,只有持有正确 token 的请求可以解锁。

为便于观察,用 test-token-a 和 test-token-b 代表两个请求;真实代码每次获取锁都生成新的随机 token。在测试锁尚未到期时按顺序执行:

DEL demo:case:lock:rebuild:42
SET demo:case:lock:rebuild:42 "test-token-a" NX PX 60000
SET demo:case:lock:rebuild:42 "test-token-b" NX PX 60000
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end; return 0" 1 demo:case:lock:rebuild:42 "test-token-b"
GET demo:case:lock:rebuild:42
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end; return 0" 1 demo:case:lock:rebuild:42 "test-token-a"
SET demo:case:lock:rebuild:42 "test-token-b" NX PX 60000
操作预期结果含义
A 获取锁OKA 可以开始工作
B 首次获取锁空值B 未持有锁,不应开始受保护的工作
用 B 的 token 解锁0未删除 A 的锁
GETtest-token-a锁仍属于 A
用 A 的 token 解锁1锁已删除
B 再次获取锁OKB 现在可以获取锁

这个案例验证 token 校验,不代表任务超过 TTL 后仍互斥。锁过期、续期和主从切换的边界见第 10.3 节及 分布式锁文档。

15.12 排障:把 WRONGTYPE 与 TTL = -1 复现出来​

现象:读取用户缓存报错,而且缓存一直不消失。下面先构造一个 Hash 缓存,再故意用错读取命令;出现错误是本案例的预期行为,之后继续执行检查命令。

DEL demo:case:debug:user:42
HSET demo:case:debug:user:42 name "alice" city "shanghai"
GET demo:case:debug:user:42
TYPE demo:case:debug:user:42
TTL demo:case:debug:user:42
HGET demo:case:debug:user:42 name
HLEN demo:case:debug:user:42
MEMORY USAGE demo:case:debug:user:42
证据预期结果结论与处理
GETWRONGTYPE 错误这次失败是命令与数据类型不符
TYPEhash应使用 Hash 读取命令
TTL-1当前键没有过期时间
HGET ... namealice正确命令可以读取字段
HLEN2当前有 2 个字段
MEMORY USAGE正整数,单位字节具体大小与 Redis 版本、编码和分配器有关,不硬编码数值

如果业务确认这个测试缓存应该在 10 分钟后失效:

EXPIRE demo:case:debug:user:42 600
TTL demo:case:debug:user:42
PTTL demo:case:debug:user:42

EXPIRE 返回 1,随后的 TTL 接近 600 秒、PTTL 接近 600000 毫秒。然后修正应用的读取命令,并在创建缓存时把过期设置纳入流程;手工补一次 TTL 不会修复后续请求的代码。若实际现象是网络超时而非 WRONGTYPE,再按第 11 节查看慢日志、连接池与网络,不能套用本例结论。