首页 / 资讯中心 / 文章详情

Redis数据类型与常见命令:选型、底层编码与实战坑位

Redis数据类型与常见命令:选型、底层编码与实战坑位 ★ FEATURED ARTICLE
上一篇把单机 Redis 装起来、连上 redis-cli 之后很多人的第一反应是敲几个 SET/GET 就以为掌握了。真正上手项目才发现Redis 的数据类型和常见命令远不止五个名字那么简单——选错类型会让内存翻三倍用错命令会让 QPS 从十万掉到八千。这篇就专门聊 Redis 的数据类型和常见命令把五种基础类型、四种特殊类型、key 级通用命令以及它们背后的编码规则、参数含义和实操坑位一次讲透。适合刚学完安装、准备在项目里用 Redis 的同学也适合已经用了一两年但没系统梳理过底层编码的老手对照查漏。1. 先把数据类型这张地图铺开1.1 五种基础类型各自扛什么业务Redis 对外暴露的基础数据类型有五种String、Hash、List、Set、ZSet。名字都很朴素但每一种背后对应的是完全不同的数据结构实现和适用场景选型的时候脑子里要有一张对照表而不是拿到什么就用 String 硬塞。String 是唯一能直接跟键值对划等号的类型value 可以是文本、数字、二进制序列上限 512MB。它适合缓存单值、计数器、分布式锁、限流令牌这类场景。Hash 是一个 key 下面挂多个 field-value天然适合存对象比如用户信息、商品属性。List 是有序可重复的双向链表适合做队列、栈、最新列表。Set 是无序不重复集合适合去重、交集并集运算。ZSet 在 Set 基础上给每个成员配了一个 score按分数排序排行榜、延时队列、带权重的排序场景基本都靠它。这里有个常见误区很多人觉得「反正都是存数据用 String 存 JSON 最省事」。短期看确实省事但一旦业务需要对某个字段单独读写、需要按分数排序、需要做去重计数String 就彻底不够用了只能把整个 JSON 读出来改完再写回去并发一高就是典型的读改写覆盖问题。所以选型的第一步不是看数据长什么样而是看「你要对它做什么操作」。我在实际项目里见过一个典型反例某团队用 String 存用户的在线状态列表每次上下线都要读取整个列表、修改、写回用户量一上来就频繁出现状态丢失。后来换成 SetSADD/SREM 一步到位问题直接消失。这不是 Redis 的问题是类型选错了。1.2 底层编码才是性能的分水岭同样一条 HSET 命令在小 Hash 上和在百万级 Hash 上耗时能差出一个数量级。原因就在于 Redis 为每种类型准备了多套底层编码会根据数据规模和元素大小自动切换。String 有三种编码int值是 Long 范围内的整数且没超过长度限制时、embstr短字符串44 字节以内、raw长字符串。int 和 embstr 都是一次内存分配raw 是两次所以短小的字符串在内存和访问效率上都更优。这个 44 字节的界限来自 redisObject 结构加 SDS 头的开销实际用的时候不用刻意卡但知道它存在有助于理解为什么「短 key 短 value」是被反复强调的优化点。Hash 在小规模时用 listpack7.0 之前是 ziplist存储本质是一块连续内存遍历快、省指针开销超过阈值就转成 hashtable支持 O(1) 查找但每个键值对都要额外指针。Set 在元素全是整数且数量不多时用 intset有序数组加二分查找元素是短字符串时会用 listpack7.2 起支持否则用 hashtable。ZSet 在小规模时也是 listpack大了转 skiplist 加 dict 的组合。List 从 3.2 起统一用 quicklist也就是「链表套 listpack」的结构兼顾了内存连续性和插入性能。Stream 底层是 rax 前缀树这个后面单独说。理解编码的意义在于你能预判数据规模变化时性能拐点在哪。比如一个 Hash 存了 200 个字段每个 value 都是短字符串它还是 listpack操作很快某天字段涨到 600 个瞬间转成 hashtable内存占用可能直接涨 50% 以上这时候就该考虑拆分 key 了。1.3 阈值配置什么时候该动什么时候别动控制编码转换的配置项就那么几个都在 redis.conf 里配置项默认值作用hash-max-listpack-entries128Hash 字段数上限超过转 hashtablehash-max-listpack-value64Hash 单个 value 字节上限list-max-listpack-size-2负数表示按大小8KB限制每个 listpackset-max-intset-entries512intset 元素数上限set-max-listpack-entries128Set 用 listpack 的元素数上限zset-max-listpack-entries128ZSet 元素数上限zset-max-listpack-value64ZSet 成员字节上限注意不同大版本这两组参数的命名不一样7.0 之前叫 hash-max-ziplist-entries直接照抄老博客的配置会启动报错改配置前先用 redis-cli config get 确认当前版本支持的键名。调不调我的经验是默认值对 90% 的场景都够用别乱调。把阈值调大确实能省内存但 listpack 是连续内存元素一多每次插入删除都要 memmove大 value 场景下反而更慢。真正需要调的场景通常是「元素少但每个都比较大」比如存配置项的 Hash字段就十几个但每个 value 有几百字节这时候把 hash-max-listpack-value 调到 256 反而更合适。2. String看着最简单坑却最多2.1 核心命令和它们真正的用途基础读写就是 SET/GET但 SET 带了一堆可选参数很多人只用过最裸的那个SET user:1001:name 张三 SET user:1001:token abc123 EX 3600 NX SET counter 100 INCR counter GETSET user:1001:name 李四 MSET k1 v1 k2 v2 k3 v3 MGET k1 k2 k3EX 是秒级过期PX 是毫秒级NX 表示 key 不存在才设置XX 表示存在才设置GET 表示返回旧值。这几个参数组合起来就能实现最简单的分布式锁SET lock:order:123 uuid NX PX 30000。注意必须是 NX 加 PX 一起用分两步先 SETNX 再 EXPIRE 的老写法在中间崩溃时会留下永不过期的锁这个坑我踩过不止一次。计数器类命令 INCR/DECR/INCRBY/DECRBY/INCRBYFLOAT 是原子的这一点非常关键。很多业务需要「先读再判断再加」用 GET 加 SET 在并发下必然出错用 INCR 就没有这个问题。字符串拼接用 APPEND取子串用 GETRANGE覆盖指定位置用 SETRANGE求长度用 STRLEN这些命令在日志截断、二进制协议解析里偶尔用得上。还有一组容易被忽视的SETBIT/GETBIT/BITCOUNT/BITOP虽然是 String 的命令但操作的是位实际属于 Bitmap 的用法后面第 7 节单独展开。2.2 过期时间设了却不清问题出在哪有一类问题几乎每个用 Redis 的人都遇到过明明设了过期时间内存却没降下来。这通常不是 Redis 的 bug而是理解偏差。第一过期删除是「惰性删除加定期删除」组合不是到点立刻删。惰性删除指访问时才判断是否过期定期删除指每 100ms 随机抽样一批 key 检查。所以一个过期 key 如果一直没人访问可能要在内存里多躺一会儿最多几分钟级别正常情况下影响不大。第二用 SET 覆盖一个带过期时间的 key如果不显式带 EX 参数过期时间会被清掉。比如SET token abc EX 60之后又来一句SET token def这个 key 就永不过期了。这是我最想强调的一条线上排查内存泄漏时先看有没有这种覆盖写法。第三RENAME 一个 key过期时间会跟着走但用 SET 重建就没了。另外 PERSIST 可以主动去掉过期时间TTL 返回 -1 表示没设过期-2 表示 key 根本不存在这两个返回值别搞混。第四主从环境下从库不会主动淘汰过期 key它等主库发 DEL 命令同步过来。所以在从库上读一个逻辑上已过期的 key行为在不同版本有差异做读写分离的时候要留意这一点。2.3 大 key 是怎么把主线程堵死的String 最大的雷就是大 key。一个 10MB 的 valueGET 一次要序列化 10MB走网络要传 10MB删除时如果直接用 DEL释放内存的动作是同步的主线程会卡住。而 Redis 单线程处理命令卡住就是全站卡住。判断标准业界比较通用的是String 类型 value 超过 10KB 就要警惕超过 1MB 基本可以定性为大 key集合类型元素数超过 5000 也算。查大 key 用redis-cli --bigkeys它会抽样扫描并给出每种类型的最大 key但注意这个命令走 SCAN 实现生产环境低峰期跑别在高峰期执行。删除大 key 的正确姿势是用 UNLINK 代替 DELUNLINK 会把内存释放放到后台线程不阻塞主线程。如果版本太老不支持 UNLINK就只能分批处理比如把大 Hash 拆成多个小 Hash或者用 HSCAN 一点点删。预防比治理重要。我现在的习惯是凡是往 Redis 写 value先想一下这个数据理论上最大能多大。图片、大 JSON、文件内容这些一律不进 Redis最多存个引用地址。这个约定看着简单能挡掉八成内存事故。3. Hash对象存储的正确姿势3.1 常用命令速查Hash 的命令以 H 开头用起来像操作一个字典HSET user:1001 name 张三 age 28 city 杭州 HGET user:1001 name HMGET user:1001 name age HGETALL user:1001 HKEYS user:1001 HVALS user:1001 HLEN user:1001 HEXISTS user:1001 age HDEL user:1001 city HINCRBY user:1001 age 1 HSCAN user:1001 0 COUNT 100HGETALL 是最容易出问题的命令它一次性返回所有字段。字段少的时候无所谓字段上千个的时候返回的数据量加上命令执行时间都会很明显而且这些数据全都要经过网络。生产环境我一般要求禁用 HGETALL改用 HMGET 指定字段或者 HSCAN 分批取。HINCRBY 和 String 的 INCRBY 一样是原子操作做字段级计数很方便比如统计某个用户的各类行为次数一个 Hash 全装下比开一堆 key 省内存也省管理成本。3.2 存对象Hash 还是序列化字符串这是选型高频问题。我的判断标准就三条判断维度倾向 Hash倾向 String(JSON)是否需要单独读写某几个字段是否字段数量是否稳定是无所谓是否需要整体作为一个值参与排序否是数据规模中等字段几十到几百极小的对象或超大对象是否要设整个对象过期无所谓需要Hash 只能对整个 key 设过期Hash 的优势是字段级操作改一个字段不用读全量写全量并发下不会互相覆盖。劣势是不能对单个字段设过期时间过期只能作用于整个 key另外如果字段极多HGETALL 会成为负担。String 存 JSON 的优势是简单、通用、一次网络往返拿全量适合读多写少、整体消费的场景。劣势是改一个字段要反序列化整个对象金额、库存这种字段在并发修改时极易出错。还有一种折中方案是 Hash 存主体字段额外用 String 存需要单独过期或参与排序的字段。这个方案会增加一致性维护成本非必要不建议除非确实有强需求。3.3 渐进式 rehash 与内存抖动Hash 从 listpack 转成 hashtable 之后随着元素增删负载因子会变化Redis 需要在后台扩容或缩容这个动作叫 rehash。为了不阻塞主线程它采用的是渐进式 rehash同时保留新旧两张哈希表每次操作顺带迁移一部分桶直到迁移完成。这个机制对使用者的影响是rehash 期间内存会短暂翻倍。如果你有一个几百万元素的 Hash 正在扩容内存监控上会看到一个明显的尖峰容量规划时要把这部分余量算进去。缩容同理元素大量删除后也会触发。另外 rehash 期间查询要查两张表性能会有轻微下降但幅度通常在可接受范围。真正需要注意的是别让单个 Hash 无限制增长比如把全站用户的某个属性塞进一个 Hash这种设计迟早出问题。合理做法是按业务维度分片比如 user:1001:profile、user:1001:settings 分开存。4. List队列、栈和那点阻塞的事4.1 命令与左右之分List 是双向链表命令都带 L 或 R 前缀表示操作哪一端LPUSH queue task1 task2 task3 RPUSH queue task4 LRANGE queue 0 -1 LPOP queue RPOP queue LLEN queue LINDEX queue 0 LINSERT queue BEFORE task2 task1.5 LREM queue 1 task1 LTRIM queue 0 99这里最容易搞混的是方向。LPUSH 是往左头部插RPUSH 往右尾部插LPOP 从左边弹RPOP 从右边弹。所以「左进右出」是队列「左进左出」是栈。写代码时一定要在注释里标清楚我在 Code Review 里见过太多因为方向搞反导致的消费顺序错乱。LTRIM 是个很实用的命令常用来做「只保留最近 N 条」的场景。比如存用户最近 100 条操作记录每次 LPUSH 之后执行LTRIM key 0 99列表长度就永远控制在 100。这个组合比手动 LREM 干净得多。4.2 拿 List 当消息队列的真实体验List 确实能做简易队列生产者 LPUSH消费者 BRPOP。BRPOP 是阻塞版本列表为空时会挂起连接等待直到有数据或超时避免了消费者空转轮询。但必须说清楚它的局限List 做队列没有 ACK 机制。消费者 BRPOP 拿到数据的那一刻这条数据就从 Redis 里没了如果消费者处理到一半崩了这条消息就永久丢失。所以它只适合对可靠性要求不高的场景比如发个站内通知、刷新个缓存丢了可以容忍或者有兜底。需要 ACK、需要消费组、需要消息回溯的场景应该用 Stream这个在第 7 节展开。我见过的选型错误里「用 List 做订单消息队列」排得上前三出了问题很难补救。另一个细节是多消费者场景下的公平性。多个消费者同时 BRPOP 同一个 keyRedis 会按先来后到分配但没有任何负载均衡保证。如果某个消费者处理慢它可能一直抢不到或者抢到了一直在忙。这个在消费能力不均的集群里会造成长尾。4.3 阻塞命令的超时与连接管理BRPOP 的语法是BRPOP key timeouttimeout 单位是秒写 0 表示永久阻塞。这里有两个实操要点第一阻塞期间这条连接是被占用的不能复用来执行别的命令。如果你的连接池配置得很小又开了很多阻塞消费者很容易把连接池耗尽。所以阻塞消费者应该用独立的连接不要和普通业务命令混用一个池。第二客户端要有超时兜底。写 0 永久阻塞看着省事但网络抖动导致连接假死时客户端可能永远感知不到。我一般写 1 到 5 秒超时后客户端主动重试顺便还能做健康检查。还有一个坑是 BRPOP 的返回值格式它返回的是「key 加 value」的两元素数组不是单个 value。新手经常直接拿返回值当数据用结果拿到的是 key 名排查半天。5. Set去重与集合运算的主场5.1 命令与集合运算Set 的命令围绕增删改查和集合运算展开SADD tags:article:1 redis cache database SREM tags:article:1 cache SMEMBERS tags:article:1 SISMEMBER tags:article:1 redis SCARD tags:article:1 SPOP tags:article:1 SRANDMEMBER tags:article:1 2 SINTER tags:article:1 tags:article:2 SUNION tags:article:1 tags:article:2 SDIFF tags:article:1 tags:article:2 SINTERSTORE result tags:article:1 tags:article:2SMEMBERS 和 HGETALL 一样是全量返回集合大了就是灾难。查成员是否存在用 SISMEMBER统计数量用 SCARD需要遍历用 SSCAN 分批。这几个命令该用哪个取决于你到底要「判断存在」还是「拿全部」。集合运算 SINTER/SUNION/SDIFF 是 Set 最有价值的地方。社交场景里的「共同关注」就是 SINTER推荐场景里的「你可能认识」可以用 SDIFF 排除已关注的人。但要注意这些运算的时间复杂度跟集合大小线性相关两个十万人的集合做交集主线程会被占住几百毫秒这种操作要么放从库要么提前算好存起来。带 STORE 后缀的版本会把结果存到新 key适合离线计算。不过 SINTERSTORE 在大集合上同样是阻塞操作别在高峰期跑。5.2 抽奖、去重、共同好友三个实战去重统计最典型的是文章阅读去重。用户看文章时 SADD article:1:users user:1001判断是否已读用 SISMEMBER统计阅读人数用 SCARD。相比每次都往数据库写一条记录内存占用小、速度快日终再落库汇总。抽奖SRANDMEMBER 是「随机取但不删除」SPOP 是「随机取并删除」。抽奖场景用 SPOP 最合适抽中的人自动从集合移除天然防止重复中奖。如果要支持「一人抽一次但奖品保留池子」那就用 SRANDMEMBER 配合一个额外的中奖记录集合。共同好友SINTER 一步到位。但真实社交场景里好友列表可能上万直接 SINTER 会很慢工程上的做法是先用小的集合做交集或者定期离线计算。这一点要说透Redis 的集合运算适合中小规模实时计算大规模的关系计算还是得靠专门的图数据库或离线任务。5.3 SPOP 和 SRANDMEMBER 的区别别记反这两个命令长得像功能差一个「删不删」。SPOP 取出后元素从集合消失SRANDMEMBER 取出后集合不变。做抽奖用 SPOP做「随机推荐几个标签」用 SRANDMEMBER。另外 SRANDMEMBER 带 count 参数时如果 count 是正数返回不重复的 count 个元素不足则返回全部如果是负数返回的元素可能重复数量是 count 的绝对值。这个负数用法在「随机生成测试数据」时挺好用但别忘了它可能有重复。6. ZSet排行榜的唯一正解6.1 命令体系ZSet 在 Set 基础上给每个成员加了 score命令分几类ZADD rank 100 user:1 200 user:2 150 user:3 ZSCORE rank user:2 ZINCRBY rank 50 user:1 ZRANGE rank 0 -1 WITHSCORES ZREVRANGE rank 0 9 WITHSCORES ZRANK rank user:2 ZREVRANK rank user:2 ZRANGEBYSCORE rank 100 200 ZCOUNT rank 100 200 ZREM rank user:3 ZREMRANGEBYRANK rank 0 0 ZREMRANGEBYSCORE rank -inf 50ZRANGE 按分数升序ZREVRANGE 按降序排行榜用后者。ZRANK 查的是升序排名从 0 开始ZREVRANK 是降序排名。取 Top N 就是 ZREVRANGE key 0 N-1取某人的名次就是 ZREVRANK两个命令配合几乎覆盖所有榜单需求。ZINCRBY 是原子自增做积分、热度、票数统计非常合适。相比「读出来加一再写回去」它没有并发问题这是 ZSet 相比自己维护排序数组的核心优势。6.2 排行榜的三个实现细节同分怎么排ZSet 在 score 相同时按成员字典序排列。这个默认行为在很多场景下不够用比如希望「同分时先达成的排前面」。解决办法是把时间戳编进 score比如score 分数 * 10^10 (最大值 - 时间戳)这样分数优先、时间次之。代价是 score 变成大整数要注意精度double 只有 53 位有效数字别超过这个范围。分页怎么做ZREVRANGE 的 start 和 stop 是下标做分页没问题。但深分页比如第 1000 页时 ZSet 的随机访问特性决定了它比数组慢一些。如果榜单只需要前 100 名完全可以只维护 100 个元素用 ZREMRANGEBYRANK 定期裁剪ZREMRANGEBYRANK rank 0 -101就能保留最后 100 个。这个技巧能显著降低内存和查询耗时。榜单怎么刷新日榜、周榜这种周期性榜单常见做法是 key 带上日期后缀比如 rank:20240101。历史榜单设个过期时间自动清理新榜单自然形成。不要把所有周期数据塞进一个 key 用 field 区分那样既不好查也不好删。6.3 用 ZSet 做延时队列延时队列的原理是把「到期执行时间戳」当 score消费者轮询ZRANGEBYSCORE key 0 now LIMIT 0 10拿到期任务取到后先 ZREM 再执行。ZREM 返回 1 表示抢到了返回 0 说明被别人抢走了靠这个实现简单的互斥。这个方案能用但有明显短板轮询有延迟、没有重试机制、没有 ACK、任务堆积时 ZRANGEBYSCORE 的扫描成本会上升。真要做可靠的延时任务Stream 加消费组或者专门的任务队列更合适。ZSet 方案适合「对延迟不敏感、丢了也能接受」的轻量场景比如定时刷新缓存、延迟发通知。7. 四个特殊类型用对了省一台机器7.1 Bitmap一个 bit 一个状态Bitmap 严格说不是独立类型底层还是 String但用法完全是另一套。它把字符串当成位数组每一位存一个 0/1 状态特别适合「用户签到」「活跃标记」「布隆过滤器底层」这类场景。SETBIT sign:202401:user:1001 15 1 GETBIT sign:202401:user:1001 15 BITCOUNT sign:202401:user:1001 BITCOUNT sign:202401:user:1001 0 3 BITOP AND result bitmap1 bitmap2一个用户一个月的签到状态最多 31 位只占 4 字节。一百万用户一个月的签到数据理论上也就 4MB 左右按 key 分开存会有额外开销实际会大一些但依然很省。换成每天一个 key 存布尔值内存开销是几十倍差距。BITCOUNT 统计 1 的个数就是本月签到天数。BITOP 支持 AND/OR/XOR/NOT两个 Bitmap 做与运算能得到「同时满足两个条件」的人群做用户分群很实用。使用注意Bitmap 的偏移量是稀疏的如果用户 ID 是十位数直接拿 ID 当偏移量会撑出一个巨大的稀疏 Bitmap中间全是 0内存并没有省下来。正确做法是把 ID 映射成连续的较小整数再当偏移量。7.2 HyperLogLog12KB 数一亿个不重复统计 UV 是个经典难题用 Set 存用户 ID一千万用户就是几百 MB。HyperLogLog 用概率算法固定 12KB 就能估算基数标准误差 0.81%非常适合「不需要精确值」的去重计数场景。PFADD uv:page:1 user:1001 user:1002 PFCOUNT uv:page:1 PFMERGE uv:total uv:page:1 uv:page:2PFADD 添加元素PFCOUNT 返回估算值PFMERGE 可以把多个 HLL 合并。合并这个特性很关键意味着你可以按小时、按天分桶统计最后合并出总量不用维护一个大 key。局限也要清楚它不返回具体元素只能给数量有误差不能用于计费、库存这种必须精确的场景单个 HLL 只支持一种去重维度想同时按城市和设备统计就得建多份。另外 12KB 是固定开销元素很少的时候反而不如直接用 Set。7.3 GEO地理位置存查一步到位GEO 底层是 ZSet把经纬度编码成 score支持范围查询和距离计算GEOADD stores 120.15 30.28 store:1 120.16 30.29 store:2 GEOPOS stores store:1 GEODIST stores store:1 store:2 km GEOSEARCH stores FROMLONLAT 120.15 30.28 BYRADIUS 5 km ASC做「附近的门店」「附近的人」这类需求GEO 是最省事的方案。GEOSEARCH 支持按半径或矩形范围查询还能按距离排序、限制数量一步到位。老版本用的是 GEORADIUS新版本推荐 GEOSEARCH功能更全。精度方面GEO 的误差在 0.5% 左右做门店导航足够做精密测量就不合适了。另外 GEO 只支持二维平面距离海拔信息它不处理。7.4 Stream终于有个像样的消息队列Stream 是 5.0 引入的类型专为消息队列设计支持消费组、ACK、消息回溯解决了 List 做队列的所有短板。XADD orders * orderId 1001 amount 99 XLEN orders XRANGE orders - XGROUP CREATE orders group1 0 XREADGROUP GROUP group1 consumer1 COUNT 10 STREAMS orders XACK orders group1 1600000000000-0 XPENDING orders group1 XTRIM orders MAXLEN 10000XADD 里的*表示让 Redis 自动生成消息 ID格式是「毫秒时间戳-序号」。XGROUP CREATE 建消费组XREADGROUP 从组里读表示读新消息。读完处理完要 XACK 确认没确认的会留在 pending 列表里可以用 XPENDING 查看、用 XCLAIM 转移给其他消费者处理这就是完整的可靠性闭环。XTRIM 用于裁剪控制 Stream 长度。不加限制的 Stream 会一直增长这是新手最容易忘的一步。我一般用 MAXLEN 近似裁剪加~符号性能比精确裁剪好很多。Stream 相比专业消息中间件优势是轻量、和 Redis 一起部署劣势是持久化和堆积能力有限别指望它扛几千万条的积压。中等规模、对延迟敏感的异步任务Stream 是很合适的选择。8. key 级通用命令与过期管理8.1 SCAN 替代 KEYS 的正确姿势KEYS 命令按模式匹配所有 key听起来很方便但它是一次性全量遍历时间复杂度 O(N)key 数量上百万时会直接把主线程卡死几秒。生产环境应该直接从权限层面禁用这个命令。替代品是 SCAN它用游标分批遍历每次只返回一小部分不阻塞主线程SCAN 0 MATCH user:* COUNT 100 SCAN 12345 MATCH user:* COUNT 100用法是第一次传游标 0返回结果里带着下一次的游标一直循环到游标变回 0 为止。COUNT 只是「提示」不是硬性限制Redis 会根据自己的编码情况返回接近这个数量的元素可能多也可能少。SCAN 的保证是从遍历开始到结束一直存在的元素一定会被返回至少一次。反过来遍历期间新增的元素可能返回也可能不返回删除的元素可能返回也可能不返回同一个元素可能返回多次。所以基于 SCAN 做业务处理时必须幂等。MATCH 是在返回结果上做过滤不是先匹配再取。这意味着如果匹配率很低你可能要扫描很多轮才能凑够你要的数据。遇到这种场景应该用更精确的 key 设计而不是靠 MATCH 硬筛。8.2 过期策略与内存淘汰的区别这两个概念经常被混淆。过期策略处理的是「单个 key 到期了怎么删」淘汰策略处理的是「内存满了删谁」。过期删除采用惰性加定期两种方式配合前面讲过。淘汰策略由 maxmemory-policy 决定常见的有 noeviction不淘汰写入报错、allkeys-lru所有 key 里淘汰最久未用的、volatile-lru只淘汰设了过期时间的、allkeys-lfu淘汰访问频率最低的、volatile-ttl优先淘汰剩余时间短的。选哪个纯缓存场景用 allkeys-lru 或 allkeys-lfu 都行lfu 对新数据更友好。如果 Redis 里既有缓存又有不能丢的数据那就必须用 volatile 系列并且保证不丢的数据不设过期时间这样它们不会被淘汰。但这么做有风险如果所有可淘汰的 key 都被清完了内存还是不够写入照样失败。所以更稳妥的做法是缓存和数据分实例部署别混在一起。提示maxmemory 别设成等于机器内存要留出 fork 做持久化时的额外开销一般设物理内存的 60% 到 75% 比较稳妥。8.3 TYPE 和 OBJECT ENCODING排查问题的第一步遇到性能问题我第一件事是看这个 key 到底是什么类型、什么编码TYPE user:1001 OBJECT ENCODING user:1001 MEMORY USAGE user:1001 TTL user:1001 EXISTS user:1001TYPE 返回类型名OBJECT ENCODING 返回底层编码比如 listpack、hashtable、skiplist、intset。这两个信息结合起来基本能判断出这个 key 的数据规模和内存特征。MEMORY USAGE 给出估算的字节数排查大 key 时比 --bigkeys 更精准因为它可以针对单个 key 查询。TTL 前面说了-1 表示永不过期-2 表示不存在。EXISTS 返回 0 或 1。这几个命令组合起来就是一次最小的诊断动作花不了几毫秒但能省下大量猜测时间。9. 命令使用中的常见问题与排查实录9.1 高频问题速查表现象可能原因排查与解决内存持续上涨不回落过期时间被 SET 覆盖丢失大 key 未删TTL 抽查关键 keyUNLINK 删大 key检查覆盖写法命令偶发变慢到几百毫秒大 key 全量操作集合运算KEYS/FLUSHALLSLOWLOG GET 查慢日志改分批命令禁用危险命令写入报 OOMmaxmemory 打满且策略为 noeviction调淘汰策略清理无用数据扩容主从延迟突然变大大 key 删除、批量写、集合运算同步用 UNLINK拆分大 key错峰操作SCAN 拿到的数据重复或缺失SCAN 的弱一致性保证业务侧做幂等按最终一致设计排行榜同分排序不符合预期未把时间戳编进 score用 score 分段拼接注意 double 精度List 队列丢消息无 ACK 机制消费者崩溃改用 Stream 消费组加 XACKHash 内存突然涨一截编码从 listpack 转 hashtableOBJECT ENCODING 确认拆分 key 或调整阈值9.2 几条私藏的操作习惯第一条所有写命令都带过期时间除非有明确理由。缓存类数据不设过期迟早变成内存黑洞。我在项目里推行的约定是缓存 key 必须带 TTL代码里写死一个默认值需要覆盖的再显式指定。第二条key 命名统一规范。我们用的是「业务:对象:ID:属性」的格式比如 order:detail:1001、user:1001:profile。好处是 SCAN 的时候好筛、看监控的时候好认、出问题的时候好定位。冒号分隔还有个额外好处部分客户端工具会按冒号自动分组成树状结构可视化查看很方便。第三条批量操作控制大小。MGET 一次取 100 个 key 和取 10000 个 key网络和内存表现完全不同。我的经验值是单次批量控制在 100 到 500 之间超了就分批。同样pipeline 也是这个道理不是批得越大越好。第四条把 OBJECT ENCODING 和 MEMORY USAGE 加进固定排查流程。遇到任何「这个 key 怎么这么大」「这个操作怎么这么慢」的疑问先跑这两条命令比凭经验猜靠谱得多。编码是 listpack 还是 hashtable直接决定了这个 key 的性能特征。第五条新业务上线前做一次量级估算。预估一下 key 的数量、平均大小、峰值增长算一算内存够不够、会不会触发编码转换。这一步花十分钟能避免上线后半夜被告警叫醒。我见过太多项目栽在「没想到数据量会这么大」上Redis 本身很稳问题几乎都出在容量规划。最后分享一个我一直在用的小技巧本地起一个 Redis 实例把 Redis 的命令当字典用遇到不确定的命令直接HELP 命令名Redis 会把语法、复杂度、版本要求全列出来。比翻文档快而且不会记错版本差异。至于数据类型选型我的判断顺序是固定的先看要不要排序要排序就 ZSet再看要不要去重要就去 Set 或 HLL然后看是不是对象是就 Hash剩下需要按下标访问或做队列的才考虑 List其余全用 String。这个顺序走下来九成场景不会选错。
阅读完成 · 觉得有帮助?
咨询建站