
如果你手头有个项目准备做缓存或者正在背 Redis 面试题大概率绕不开一个问题Redis 的数据结构到底有哪几种、每个结构常用的命令有哪些、以及真正干活的时候该怎么选。市面上讲 Redis 数据类型的文章很多但大多只停留在这是 String、这是 Hash的层面看完能记住命令遇到实际问题还是懵。我自己的体会是Redis 这件事光记住命令没用关键是搞清楚每种结构的底层组织和适用边界——也就是为什么这个场景该用 ZSet 而不是 List为什么 Hash 能省内存为什么有人把 String 用成了大 key 然后半夜被同事叫起来处理告警。这篇文章不打算给你讲一堆云里雾里的原理而是从实际开发视角把 Redis 的数据结构和常见命令完整过一遍每个数据结构都会带上命令示例、适用场景和踩坑提醒尽量做到看完就能用。适合刚上手 Redis 的初学者也适合准备面试想系统整理一遍的开发者以及那些 Redis 已经用了一阵子、但总觉得选型不太对劲儿的人。1. 先打破一个误解Redis 的数据结构分两层很多人在学 Redis 时被数据结构三个字搞迷糊了。面试官问Redis 有哪些数据结构你背出 String、Hash、List、Set、ZSet 五个就完事了但实际往深了问一句ZSet 底层是什么结构你就开始卡壳。这里得先说清楚一个事实Redis 对外提供的五种基本数据类型和内部底层的存储编码结构是两码事。1.1 对外数据类型与底层编码的区别简单来说String、List、Hash、Set、ZSet 是你在命令行里敲命令时看到的抽象类型而 Redis 在内存里真正用哪种数据结构去存储这些键是内部分配的。Redis 会根据你的 value 大小、元素数量、数据类型自动选择最省内存、性能又不差的编码方式。我举个例子。同样是存一个 Hash 键如果这个 Hash 里的字段很少、值也很短Redis 会用压缩列表Redis 7.0 之后改成了 listpack来存如果字段数量多了或者值变大到超过阈值它就会自动升级成哈希表。这个转换过程对我们使用者是透明的但理解它很重要因为很多为什么 Redis 内存这么省或者为什么我的操作突然变慢的问题都跟这个编码切换有关。再比如 Set。当集合里的元素全是整数且数量不大时Redis 用整数集合intset存储一旦出现非整数元素或者元素数量超过配置阈值就切到哈希表。这就是为什么论坛里那个10万个用户 ID 的去重集合占内存很小的帖子总能火——因为全是整数 ID走了 intset。1.2 编码结构变化的触发条件与配置每种类型对应的底层编码切换Redis 都给了我们配置项去控制阈值。拿全局配置来说hash-max-listpack-entries、set-max-intset-entries、zset-max-listpack-entries这几个参数就是各家 Redis 运维经常调整的阀门。我以 ZSet 为例说明这个切换逻辑。Redis 7.0 之前ZSet 的元素数量小于zset-max-ziplist-entries默认 128且每个元素长度小于zset-max-ziplist-value默认 64 字节时用 ziplist 存超过之后就换成跳表加哈希表的组合结构。这个自动升级是不可逆的元素涨上去了删掉一些元素也不会自动降级回 ziplist。所以真要抠内存得在设计阶段就控制好单 key 的规模。聊到这儿可能有人会问我搞清楚底层编码结构有什么用用处很大。第一面试时别人问你ZSet 底层为什么用跳表你能从编码切换说起而不是硬背跳表查询快、插入快。第二实际开发时如果你发现某个 Hash 或 ZSet 键出现了明显的内存膨胀往往就是元素数量和大小超了阈值触发了编码切换。这时候合理的做法不是删数据而是调整对应配置或者对 key 做拆分。2. 五种基本数据结构命令与典型场景逐个拆解现在进入正题把五种基本数据结构的常用命令过一遍。我不打算把 Redis 官方文档里的命令全部罗列出来那样跟查文档没区别。我会挑出日常开发中用得最频繁、面试最容易问到的命令每个结构都配上一个真实场景来讲。2.1 String缓存、计数器与分布式锁的基石String 是 Redis 里最基础、用得最多的类型。它的 value 最大能存 512MB不仅能存字符串还能存数字、二进制数据、JSON 序列化结果。命令核心就这几个方向读写、增减、过期、批量。先看最基础的命令命令作用示例SET key value设置键值对SET user:1:name zhangsanGET key获取值GET user:1:nameSETNX key value不存在时才设置返回 1 或 0SETNX lock:order 1SETEX key seconds value设置值并指定过期时间SETEX session:token 3600 abc123MSET / MGET批量设置/获取MSET a 1 b 2INCR / DECR自增/自减 1INCR views:article:1001INCRBY / DECRBY指定步长增减INCRBY user:1:score 10INCRBYFLOAT浮点数增减INCRBYFLOAT cart:1:amount 3.5APPEND key value在字符串末尾追加APPEND log:error another errorSTRLEN key获取字符串长度STRLEN user:1:bio很多初学者会忽略INCR系列命令的原子性。这个指令在单线程的 Redis 里天然就是原子的不需要加锁所以做计数器非常合适。比如文章阅读量、商品库存扣减、用户积分变动直接用INCRBY就行比先 GET 再 SET 再考虑并发好太多。但这里有个特别容易踩坑的点很多人把 Java 对象序列化成 JSON 字符串然后整个丢进 String 里。一个一二 kB 的 JSON 还好如果是把一个大对象的全量字段都塞进去读写频繁时网络开销和内存占用都很大。我在项目里见过有人把用户的基本信息、订单列表、收藏夹全部塞进一个 String key 里美其名曰缓存大对象结果这个 key 成了 big key访问一次慢一次。String 适合存简单值复杂对象还是得考虑 Hash。关于 String 做分布式锁也是在面试里高频出现的点。早期有人用SETNX加锁、DEL释放锁但会碰到一个经典问题线程 A 拿到锁后执行时间太长锁过期了线程 B 又拿到锁A 执行完把 B 的锁给删了。正确的做法是加锁时用SET lock uniqueValue NX PX 30000给每次加锁生成唯一标识释放锁时用 Lua 脚本先比对标识再删除保证只能删自己的锁。这个我在第 5 章还会展开讲。2.2 Hash对象存储的集大成者Hash 在 Redis 里对应的是一个 string 字段和 string 值之间的映射简单理解就是一个小型的存储对象。它特别适合存结构化数据也就是那些有一堆字段、但每个字段的值又不大的场景。常用命令如下命令作用示例HSET key field value添加字段HSET user:1001 name zhangsan age 25HGET key field获取单个字段HGET user:1001 nameHMSET key f1 v1 f2 v2批量添加字段HMSET user:1001 name zhangsan age 25HMGET key f1 f2批量获取字段HMGET user:1001 name ageHGETALL key获取全部字段和值HGETALL user:1001HINCRBY key field n字段值自增 nHINCRBY user:1001 score 5HDEL key field删除字段HDEL user:1001 ageHEXISTS key field判断字段是否存在HEXISTS user:1001 nameHKEYS / HVALS获取所有字段名或值HKEYS user:1001HLEN key获取字段数量HLEN user:1001用 Hash 存储对象最大的好处是可以部分更新。比如用户信息里用户改了头像你只需要HSET user:1001 avatar new_url其他字段不用像 String 那样整存整取。这一点在流量稍大的接口里差异非常明显——不用每次都想哪些字段变了序列化还是反序列化这类琐碎问题。不过 Hash 也不是没有毛病。如果字段数量特别多或者单个 value 特别大它内部从 listpack 切换到哈希表之后内存消耗会上升一截。另外HGETALL这个命令要慎用尤其是字段很多还频繁调用的时候。它会把整个 Hash 的所有字段和值全部取出来一个 Hash 几十个字段还好几百个字段还每来一次请求就HGETALL一次响应时间和带宽都扛不住。处理方式一般是改成HMGET只取需要的字段比如列表页只需要昵称和头像那就只取这两个。实际项目里购物车的存储是我最常用的 Hash 场景。一个cart:1001键下field是商品 SKU IDvalue是加入购物车的数量。加购就是HINCRBY cart:1001 sku:1234 1修改数量同理删一项就是HDEL。这比用 String 拼接一个 JSON 数组或者用 List 存一堆重复商品 ID都要优雅得多。2.3 List队列、栈与最新列表List 在 Redis 里是一个按照插入顺序排序的字符串列表底层用 quicklist 实现。它既能当队列用也能当栈用还能用来做最新的多少条这种列表类功能。命令我按功能分三组给你捋方向命令作用插入LPUSH key value [value...]/RPUSH从左侧/右侧插入弹出LPOP key/RPOP从左侧/右侧弹出元素范围LRANGE key start stop获取指定范围元素查询LINDEX key index/LLEN key按下标取元素 / 获取长度修剪LTRIM key start stop只保留指定范围删除LREM key count value删除指定数量的匹配元素阻塞BLPOP key timeout/BRPOP key timeout阻塞式弹出超时返回 nil我用 List 做最新消息列表时最喜欢LPUSH LTRIM组合。比如一个帖子下面的最新 10 条评论每次新评论LPUSH post:1001:comments comment_content然后LTRIM post:1001:comments 0 9天然就把列表控制在 10 条以内。这比先查总数据量再分页要省事得多。BLPOP和BRPOP是 List 用来做消息队列的核心。它们的语义是如果列表里有数据直接弹出没有数据就阻塞等待直到超时或者有数据进来。很多人拿这个实现了一个轻量级的任务队列生产端LPUSH消费端BRPOP。但你要意识到这不是一个可靠的消息队列消息被弹出去之后如果消费端崩溃这条消息就永远丢失了。Redis 官方后来的 Stream 类型就是来补这个短板的第 3 章会细讲。还有一个容易忽略的问题List 的LRANGE如果 start 给 0、stop 给 -1那就是把所有元素都取出来。List 里的元素如果攒到十万条你这么一取内存和带宽直接就爆了。所以列表类数据至少要配合LTRIM控制长度或者限制LRANGE的范围别图省事一把梭。2.4 Set去重、标签与集合运算Set 是一个无序去重的字符串集合。去重是它最直观的用途但很多人忽略了它真正厉害的地方是集合运算——交集、并集、差集这些在社交类、推荐类场景里都是常用的底牌。常用命令命令作用示例SADD key member [member...]添加元素SADD user:1001:tags java redisSREM key member移除元素SREM user:1001:tags javaSMEMBERS key获取全部元素SMEMBERS user:1001:tagsSISMEMBER key member判断元素是否存在SISMEMBER user:1001:tags redisSCARD key获取元素数量SCARD user:1001:tagsSPOP key [count]随机弹出元素SPOP prize:pool 1SRANDMEMBER key [count]随机获取元素但不弹出SRANDMEMBER queue 3SINTER key [key...]集合取交集SINTER user:1001:tags user:1002:tagsSUNION key [key...]集合取并集SUNION tag:java tag:redisSDIFF key [key...]集合取差集SDIFF user:1001:follows user:1002:follows注意SMEMBERS这个命令和 List 的LRANGE 0 -1一样属于全量拉取型命令Set 元素多的时候性能会打折扣。在实际开发里如果只是想判断某个人是否在某集合里请用SISMEMBER一次 O(1)。如果想取多个集合交集的结果SINTER是交由 Redis 服务端算完再返回的网络传输量会小很多。Set 在抽奖场景的用法也很经典把用户 ID 全部SADD进一个 set开奖时用SPOP随机弹出中奖者弹出来的用户自动从集合移除天然避免了一个用户重复中奖。如果是显示 3 个候选礼物这类需要随机展示但又不移除的场景用SRANDMEMBER。2.5 ZSet排行榜和延时队列的天然选择ZSet 全称是 Sorted Set有序集合。它在 Set 的基础上给每个元素附加了一个 double 类型的 scoreRedis 按照 score 从小到大排序。正因为这个排序特性它成了业务层做排行榜、延时队列的首选结构。常用命令命令作用示例ZADD key score member添加元素并设置分数ZADD leaderboard 100 user:1001ZRANGE key start stop [WITHSCORES]按下标范围获取元素ZRANGE leaderboard 0 9 WITHSCORESZREVRANGE key start stop按分数从高到低获取ZREVRANGE leaderboard 0 9 WITHSCORESZRANGEBYSCORE key min max按分数范围获取ZRANGEBYSCORE leaderboard 80 100ZSCORE key member获取某元素分数ZSCORE leaderboard user:1001ZRANK key member/ZREVRANK获取正序/倒序排名ZREVRANK leaderboard user:1001ZINCRBY key increment member为某元素加分ZINCRBY leaderboard 10 user:1001ZREM key member删除元素ZREM leaderboard user:1001ZCARD key获取元素总数ZCARD leaderboardZREMRANGEBYSCORE key min max按分数范围删除ZREMRANGEBYSCORE pending:queue 0 1637212800排行榜的需求每个产品都有ZSet 几乎就是为它设计的。你要做实时热榜就给每篇文章维护一个post:hot:rank键每次文章获得一个赞或一条评论就ZINCRBY post:hot:rank 1 article:1001展示时直接ZREVRANGE post:hot:rank 0 19取前 20 名。一个命令顶一堆 SQL 运算。ZSet 做延时队列的思路可能很多新手还没接触过。原理是把任务的执行时间戳当作 score放进一个 ZSet 里然后轮询时用ZRANGEBYSCORE pending:task 0 当前时间戳取出所有到期的任务处理完了ZREM删掉。这个方案在任务量不大、可靠性要求不苛刻的场景下很灵活很多公司用它在内存里做简单的定时任务调度省去引入 MQ 的成本。ZSet 的底层实现是很多面试题的重点值得多说一句。它内部同时维护了哈希表和跳表哈希表负责按 member 快速找 score跳表负责按 score 范围高效遍历。跳表这种数据结构相比平衡树实现起来更简单区间查找性能也很不错所以 Redis 的作者选了它。如果你在面试中被问到这一点把这个双结构配合的逻辑讲清楚比单纯背ZSet 用跳表要有说服力得多。3. 进阶数据结构Bitmap、HyperLogLog、Geo 与 Stream只掌握五种基本类型其实还只是 Redis 数据结构的入门。真正让 Redis 在业务里显得灵活到不讲道理的是那几种基于基本类型扩展出来的高级结构。它们各自解决一类特定问题用好了能省下大量内存和代码。3.1 Bitmap用 bit 做统计的极致省内存方案Bitmap 说到底是 String 类型的一种位图操作模式Redis 把字符串当成一个 bit 数组来用。通过SETBIT和GETBIT直接操作某个二进制位上的是 0 还是 1可以用极小的内存存大量的布尔状态。命令作用示例SETBIT key offset value设置某一位的值0 或 1SETBIT sign:2024:user:1001 5 1GETBIT key offset获取某一位的值GETBIT sign:2024:user:1001 5BITCOUNT key [start end]统计值为 1 的位数BITCOUNT sign:2024:user:1001BITOP op destkey key...对多个 bitmap 做运算BITOP AND dest:key key1 key2签到打卡是最经典的 bitmap 场景。一年 365 天用一个 String 类型 key365 bit ≈ 46 字节就能存下一个人全年的签到记录。查他第 50 天有没有签到就是GETBIT sign:2024:user:1001 49看他这个月签到了几天就用BITCOUNT配合[start end]按字节范围统计。如果用 MySQL 存一个人一年就得 365 行用 bitmap 存内存几乎可以忽略不计。Bit 位的顺序要注意一个细节offset 是从 0 开始计的所以第 1 天对应的是 offset 0第 50 天对应的是 offset 49。代码里换算清楚再传参很多人在边界值上栽过跟头。BITOP命令还能做用户在线状态聚合比如把所有在线用户的 bitmap 做BITOP OR结果为 1 的位置就是在线用户集合。这个思路在分析型系统里很常用但要注意BITOP是重量级批量操作一次处理太大范围的数据也会占服务端 CPU别在业务高峰盲目调用。3.2 HyperLogLog基数统计不存原始值HyperLogLog 最大的价值是用很小的固定内存约 12KB统计一个集合里不重复元素的数量误差在 0.81% 以内。它不存储元素本身只存元素的哈希近似值所以统计完就拿不到原始成员列表了。命令作用示例PFADD key element [element...]添加元素用于基数统计PFADD uv:2024-12-25 user:1001 user:1002PFCOUNT key [key...]估算基数去重后总量PFCOUNT uv:2024-12-25PFMERGE destkey sourcekey...合并多个 HyperLogLogPFMERGE uv:week u1 u2 u3UV 统计、页面访问去重、搜索关键词去重等场景HyperLogLog 是首选。我在之前一个项目里要统计每天的活跃用户数如果直接用一个 Set 去存 user_id一天百万级用户一个 key 就是几十 MB几十天下来内存压力非常大。换成 HyperLogLog每天一个 key无论用户量多大每个 key 只占 12KB 左右成本几乎为零。但要明确它适合的场景只需要知道不重复的数量不需要知道具体是哪些人不重复。如果你还要查看当天活跃用户的具体名单HyperLogLog 做不了那会儿还是得老老实实上 Set 或者布隆过滤器。这三种方案的取舍表我放在这里方案内存占用是否返回元素精确度适合场景Set高能精确小规模需明细的去重集合Bitmap低能需二次解析精确用户 ID 连续或可映射的统计HyperLogLog极低否近似0.81%大规模 UV 统计类3.3 Geo地理位置与附近的人Geo 类型从 Redis 3.2 开始提供底层是用 ZSet 实现的只是把经纬度通过一种编码算法转成了 score。因此 Geo 键本质上是 ZSet你甚至能对 Geo 键直接调用部分 ZSet 命令只是不太建议这么混用。命令作用示例GEOADD key longitude latitude member添加位置GEOADD shops 121.47 31.23 shopAGEOPOS key member获取成员坐标GEOPOS shops shopAGEODIST key m1 m2 [unit]计算两点距离GEODIST shops shopA shopB kmGEORADIUS key lng lat radius unit按经纬度查附近GEORADIUS shops 121.47 31.23 5 kmGEORADIUSBYMEMBER key member radius unit按成员查附近GEORADIUSBYMEMBER shops shopA 5 km附近的门店附近的司机这类功能直接用GEORADIUS就能查询。我在一个外送项目里用它做过按用户位置查找三公里内可用骑手一个命令返回候选骑手列表比在关系型数据库里做经纬度范围计算要快一个数量级。要留意的是GEORADIUS在 Redis 6.2 之后官方更推荐用GEOSEARCH作为替代GEORADIUS也还支持只是语义上GEOSEARCH更清晰。Geo 的精度受编码算法限制最短距离有大概 0.5% 的误差日常的附近的人搜索完全没有问题但如果你的业务对距离精度要到米级还是得用更专业的地理计算方案。3.4 Stream轻量消息队列与消费者组Stream 是 Redis 5.0 引入的类型定位就是实现一个不依赖外部 MQ 的消息队列。它支持消息 ID、消息持久化、消费者组、消费确认弥补了 List 做队列时消息弹出即丢失、不支持多消费者组的缺陷。命令作用示例XADD key id field value添加消息*表示自动生成 IDXADD order:events * type created orderId 123XLEN key获取消息数量XLEN order:eventsXREAD COUNT n STREAMS key id从指定位置读取消息XREAD COUNT 10 STREAMS order:events 0XREADGROUP GROUP group consumer消费者组读取XREADGROUP GROUP workers c1 COUNT 1 STREAMS order:events XACK key group id确认某条消息已消费XACK order:events workers 1690000000000-0XTRIM key MAXLEN n裁剪历史消息XTRIM order:events MAXLEN 1000Stream 的消费者组模型我建议你要么不用一旦要用就得先理解Pending Entries ListPEL待确认消息列表。消费者组里的每个消费者读走一条消息后消息会进入这个消费者的 PEL处理完确认XACK后才算完。如果消费者处理到一半崩溃了消息还留在 PEL 里换一个消费者用XCLAIM可以重新认领这些消息。这套机制让消息至少被处理一次配合幂等消费逻辑很多内部系统真就能靠 Stream 撑住。当然Stream 不是 RabbitMQ 或 Kafka 的替代品。它缺少消息积压回放策略、分区扩展能力也弱适合公司内部消息量不大、不想多运维一套中间件的场景。用在这里的前提是业务简单、你能接受一条消息至少处理一次的语义而不是严格的正好一次。4. 从安装到可视化开发环境搭建与常用工具讲完命令和数据结构说点工程相关的内容。毕竟命令敲得再熟环境没装好、工具选得难受开发体验也是白搭。这一章聊安装、容器化部署和可视化客户端。4.1 安装方式与 Docker 部署Redis 在 Windows 上没有官方正式版官方只提供 Linux 版本。Windows 上跑 Redis 的方案历来是社区维护的分支或者是通过 WSL、Docker 跑 Linux 容器。如果你公司有条件我建议直接上 Linux 或者 Docker少踩 Windows 分支的版本坑。Docker 部署 Redis 是现在最省心的方式我这里给一份开发环境用的最小配置docker run -d \ --name redis-dev \ -p 6379:6379 \ -v /data/redis:/data \ redis:7.2-alpine \ redis-server --appendonly yes这个命令启动了一个带持久化的 Redisappendonly yes表示开启 AOF 持久化。-v /data/redis:/data是把容器里的数据目录映射到宿主机这样容器删了数据也不会丢。对于主从架构可以再起一个容器挂--slaveof参数指向主库但这只是开发验证用生产环境还是建议用配置文件和编排工具管理。装完之后第一件事是给 Redis 设密码。直接在启动参数里加--requirepass your_password或者进配置文件把requirepass打开。很多人自己在开发环境装了 Redis 不去设密码结果端口暴露出去被扫描器接管当成矿机了这种事见过不止一次。4.2 可视化客户端的选择与对比命令行用顺手之后偶尔还是想要一个图形化界面看看 key 的分布、扫描大 key、看内存占用。市面上的 Redis 可视化客户端挺多我把自己实际用过的几个整理成表格工具平台特点适合人群Redis Desktop ManagerRDMWin / Mac / Linux老牌工具功能全面新版已收费习惯老牌工具的团队Another Redis Desktop ManagerARDMWin / Mac / Linux免费开源界面现代化性能不错不想付费的日常开发RedisInsightWin / Mac / LinuxRedis 官方出品支持命令分析、内存分析深度排查问题的开发者TablePlus / NavicatWin / Mac综合数据库管理工具附带 Redis 支持同时管 MySQL 等数据库的人这里面我自己的日常主力是 Another Redis Desktop Manager因为免费、跨平台树形展示 key 的时候还能看到每个 key 的类型和 TTL。RedisInsight 的官方内存分析功能在排查 big key 时特别有用建议和 ARDM 两个都装。关于可视化 client 有两点提醒。第一不要在图形界面上对整个库执行KEYS *。生产环境的 key 一旦几十万上百万KEYS *会阻塞 Redis 主线程线上就出大事了。真要扫 key 用SCAN命令或者直接上 RedisInsight 的扫描功能它内部就是游标式的。第二可视化客户端看起来能编辑数据但它只是把命令包装成界面有些批量操作会把整条命令发过去脑子要时刻清楚背后是哪个命令。4.3 连接工具与命令行筑基工具再多redis-cli这个官方命令行还是要会用。它除了执行命令还内置了几个很实用的运维参数redis-cli -h 127.0.0.1 -p 6379 -a your_password redis-cli --bigkeys redis-cli --scan --pattern user:*--bigkeys会遍历全库找出各个类型里最大的 key上线前检查大 key 必备。--scan --pattern user:*是按通配符游标式地扫描 key代替KEYS user:*。5. 这些坑我踩过命令误用与缓存治理经验最后一部分我把这些年实际遇到的问题做一次系统化的复盘。命令本身不复杂坑通常出在用错了命令没考虑并发没考虑缓存失效这三类问题上。5.1 命令误用从全量扫描到 RedisTemplate 类型报错先说你每天最容易踩的坑——KEYS *。它会把所有 key 遍历一遍在 key 数量很大时严重阻塞 Redis 单线程。换成SCAN命令游标式获取每次返回少量 key不阻塞主线程。我在排查线上问题时只用redis-cli --scan --pattern绝不用KEYS。再一个是SMEMBERS和HGETALL这类全量命令。很多初学者做缓存时习惯把整个集合取出来到程序里再慢慢判断。数据量小的时候没事几万条甚至几十万条数据时每一次全量获取都会造成网络阻塞和内存尖刺。对应换成SISMEMBER、HMGET或者分批SCAN。有段时间我老被一个报错困扰java.lang.RuntimeException: java.lang.Integer cannot be cast to java.lang.String或者ERR value is not an integer or out of range。后来排查发现是 RedisTemplate 的序列化配置问题。如果 key 和 value 用了不同的序列化器存进去和读出来对不上就会出现类型错乱。比如你用StringRedisTemplate存了字符串42再用普通RedisTemplate默认 JdkSerializationRedisSerializer 序列化 value去读读出来的字节流反序列化后并不是 Integer就报错了。解决方法是统一用StringRedisTemplate或者手动配置RedisTemplateString, Object的 key 和 value 序列化器让它们都是 StringRedisSerializer。5.2 缓存穿透、击穿与雪崩的治理思路这三个词在 Redis 缓存面试里几乎是必问的实际项目里也确实值得花心思设计。我把三者的区别和应对方案整理成一张表问题现象常见场景解决思路缓存穿透查询一个不存在的 key每次打到 DB恶意攻击用随机 ID 查询空值缓存 / 布隆过滤器拦截 / 参数校验缓存击穿热点 key 过期瞬间大量请求打到 DB微博热搜 key 刚好到了过期时间互斥锁重建缓存 / 逻辑过期不淘汰缓存雪崩大量 key 同时过期DB 压力暴增缓存设置了相同的固定过期时间过期时间加随机值 / 多级缓存 / 限流降级缓存穿透最常见的处理是空值缓存查询结果为空时也往 Redis 里写一个空值并设置较短的过期时间例如 60 秒。这样同一个恶意请求短时间内的第二次查询就会被缓存挡住。更彻底的做法是加布隆过滤器把存在的 ID 预加载到布隆过滤器里过滤器判断不存在就直接返回但还是有一个过滤器误判率的问题需要权衡。缓存击穿针对的是单个热点 key。我讲一下互斥锁的思路当缓存失效后不是所有请求都去查 DB而是先尝试获取一个分布式锁让拿到锁的线程去查数据库并回填缓存其他线程短暂自旋等待重试。逻辑上可以实现但要小心死锁和超时——一定要给锁加过期时间回填缓存的逻辑要幂等。缓存雪崩我见过最典型的就发生在秒杀活动上运营把所有商品设置了同一天的同一个过期时间点零点一到几万个缓存 key 同时失效数据库瞬间被打挂。处理办法是我一直坚持的每个 key 的过期时间额外加一个随机值比如baseExpire Random(0, 300)秒让失效时间均匀散开。5.3 分布式锁的正确姿势分布式锁在热词里热度一直很高说明大家确实都在用也确实容易用错。我再补一个稍微极端一点的场景一个服务实例拿到了锁结果它在执行任务时发生了长时间的 GC 停顿超过了锁的过期时间锁被 Redis 自动释放。另一个实例立刻拿到了锁开始干活。前一个实例 GC 恢复后不知道锁已经没了两个实例同时执行同一个任务业务上产生了重复数据。这个问题的根子是锁的过期时间和任务执行时间没法准确预估。行业里的解决方案主要是 Redisson 的看门狗机制默认给锁设置 30 秒过期然后每隔一段时间自动续期只要持有锁的线程还活着锁就不会过期。任务执行完主动释放锁看门狗线程也跟着停掉。如果你不想引入 Redisson自己用 Redis 写一套也不是不行但要记住几个底线加锁时必须用SET key uniqueValue NX PX 30000其中 uniqueValue 是每条线程唯一的随机值。释放锁必须用 Lua 脚本先比对 uniqueValue 再删除不能直接DEL。加锁和设置过期时间必须原子完成不能用SETNX之后再EXPIRE否则 SETNX 成功但 EXPIRE 失败锁就会永久不释放。if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这个脚本是我线上用了很久的删除锁方案。把这套原理吃透面试时从不能只删自己的锁讲到看门狗续期基本就算过关了。5.4 大 Key 与热 Key 的处理经验最后说一个我现在做缓存设计时一定会提前预防的问题——大 key。一个 String 类型的 value 超过 1MB或者一个 List / Set / ZSet 里元素数量超过几万都算大 key。大 key 的危害主要体现在读写成本高、删除卡顿、数据迁移耗时、内存分布不均。处理大 Key 没有银弹通常就是拆和换。拆是把一个大的 Hash 拆成多个小 Hash比如把用户的不同维度信息拆成user:1001:base、user:1001:ext换是换一种数据结构比如把大 Set 换成多个小 Set或者用 HyperLogLog 做统计类数据。热 Key 则是另一个维度的难题——某个 key 访问频率特别高导致单台 Redis 实例 CPU 打满。应急方案是给热 key 做多级缓存先在本地 JVM 用 Caffeine 缓存一层还可以考虑对 key 做副本比如把hot:product:1在集群里复制成hot:product:1#0、#1、#2等把读流量分散到不同分片。当然这是治标治本还是要看业务能不能把访问模型削峰。个人在实际项目里的体会是数据结构选型这件事宁可多花点时间想清楚也不要等出了故障再回来改。去 Google 或者看 GitHub 上开源项目里 Redis 的用法你会发现那些写得好的代码基本都有一个共性根据读频率、写频率、数据量、一致性要求四个维度去选结构而不是看着哪个命令顺手就用哪个。同一个需求用 ZSet 还是 List用 Hash 还是 String可能性能差异就是十倍百倍。另外一个小技巧维护一个内部的 Redis 命令速查表挺管用。每当你遇到一个好像可以解决当前问题但没把握的命令不要只在命令行里试花五分钟把你用的场景和命令写进团队文档下次别人遇到同样的问题就不用再踩一遍你的坑了。Redis 的设计整体是很直接的最难的从来不是命令而是你在动手之前有没有把自己要解决的数据结构问题想清楚。