news 2026/9/18 17:45:15

Redis 16个核心使用场景:从缓存穿透到分布式锁,一篇讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 16个核心使用场景:从缓存穿透到分布式锁,一篇讲透

接手过一个老项目,登录态存在内存 Map 里,排行榜每天半夜用定时任务全量算,浏览量直接 update 数据库,UV 统计更是想都不敢想。后来我把它们全部迁到 Redis,一周改完,单机稳稳扛住了日常流量。Redis 本身不难,难的是很多人不知道它到底能在哪些场景里当“万能零件”。这篇就围绕 16 个常见使用场景来展开,从场景、数据结构、核心命令到坑位,一次说清楚。刚入门的朋友可以用来建立全局认知,准备面试的朋友也可以拿来做快速复盘。

1. 开跑之前:把环境一次配好,下载、Docker 主从、序列化一起说清

1.1 下载与 Windows 安装到底怎么选

很多人第一关就卡在“Redis 怎么下载”。这里要明确一点:Redis 官网没有官方 Windows 版本,Linux 系统都是直接 apt 或 yum 装,Windows 上想跑官方版基本要靠 WSL2 或 Docker Desktop。如果电脑实在不方便装 Docker,也可以找第三方移植包,比如 tporadowski/redis 项目维护的 Windows 编译版,装完把 redis-server.exe 跑起来,再开一个 redis-cli 敲 ping,能回 PONG 就算成功了。

日常开发我强烈建议配一个可视化客户端。网上搜“redis desktop manager”出来的版本很乱,早期开源版社区已经停止维护,官方现在的产品叫 Redis Insight。这个工具确实值得装:它不只是看 key,还能看内存分析、慢日志、命令耗时,排查线上问题比 redis-cli 盲敲高效得多。我见过太多同事装了个老掉牙的 RDM,连 cluster 模式都连不上,白折腾半小时。

1.2 用 Docker 一次性搭出主从

很多教程单机玩得很溜,一上主从就懵。这里我给一个无脑复制的 docker-compose 配置,直接跑起来就能看到主从关系:

version: '3' services: redis-master: image: redis:7 container_name: redis-master command: ["redis-server", "--port", "6379"] ports: - "6379:6379" redis-slave: image: redis:7 container_name: redis-slave command: ["redis-server", "--port", "6380", "--replicaof", "redis-master", "6379"] depends_on: - redis-master ports: - "6380:6380"

启动docker compose up -d后,进 slave 执行INFO replication,看到role:slavemaster_link_status:up就代表主从同步正常。注意 Redis 5 之后slaveof命令改名叫replicaof,老博客里那些--slaveof启动参数虽然兼容,但新项目没必要再用。

搭主从不是为了装样子,后面聊缓存雪崩、读写分离都要靠它兜底。我有一次压测时主库 CPU 飙满,从库一点事没有,就是因为写操作全打在主库上;后来把排行榜读流量切到从库,主库压力直接降了一半。

1.3 序列化:为什么你存进去的对象全是乱码

这里必须提一个 Java 后端最容易踩的坑:默认的 JdkSerializationRedisSerializer 会把对象序列化成一串\xAC\xED\x00\x05t\x00开头的二进制,在客户端里看全是乱码,而且占据的内存比 JSON 大好几倍。这不是 Redis 的问题,是序列化策略没配对。

我一般推荐用 GenericJackson2JsonRedisSerializer,或者直接用 StringRedisTemplate 手动把对象转成 JSON 字符串再塞进去。前者省事但要保证对象有无参构造,后者对性能敏感场景更友好。曾有个项目上线后发现 key 值全部多了一对双引号,排查了半天,原因就是 Jackson 序列化策略没配好导致 value 被二次转义。序列化这块,宁可在环境准备阶段多花十分钟,也别等上线了再收拾。

2. 16 个场景到底怎么记:一张速查表搞定数据结构选型

2.1 场景与数据结构映射

这 16 个场景是我在实际项目和面试题里反复看到的组合,先给你一张表,后面再逐个讲原理和坑:

序号使用场景核心数据结构核心命令一句话说明
1缓存StringGET / SET / EXPIRE最经典用法,注意穿透击穿雪崩
2分布式锁StringSET NX EX / Lua注意锁超时和误删问题
3计数器/浏览量StringINCR / DECR原子自增,天然并发安全
4排行榜ZSetZADD / ZREVRANGE有序集合按分数排序
5会话共享StringSET EX / GET多机登录态统一存储
6消息队列List / StreamLPUSH / BRPOP / XADD轻量队列,Stream 更可靠
7延迟任务ZSetZADD / ZRANGEBYSCOREscore 存执行时间戳
8UV 去重统计HyperLogLogPFADD / PFCOUNT亿级去重只要 12KB
9签到/在线状态BitmapSETBIT / BITCOUNT位运算省空间
10附近的人GEOGEOADD / GEOSEARCH基于经纬度距离查询
11限流StringINCR + EXPIRE固定窗口限流最简单
12发布订阅Pub/SubPUBLISH / SUBSCRIBE实时通知,不持久化
13购物车HashHSET / HINCRBY / DEL字段粒度操作方便
14抽奖SetSADD / SRANDMEMBER / SPOP随机性和去重结合
15共同关注SetSINTER / SINTERSTORE集合交集运算
16布隆过滤器扩展类型BF.ADD / BF.EXISTS防缓存穿透的过滤器

看到这张表你会发现一个规律:Redis 场景选型本质上就是“数据结构选型”。多数字面看似复杂的业务场景,底层都能映射到五种基础类型加三种扩展类型上。你不需要背命令,把每种结构的特性记清楚,场景自然能对上号。

2.2 选型背后的底层逻辑

为什么偏偏是 Redis 来完成这些事?说白了就三点:内存快、数据结构丰富、单线程命令执行天然串行。

内存快不用多说,单线程这个特性值得一提。既然是单线程执行命令,多个客户端同时 INCR 的时候不存在并发竞争,所以它敢保证原子性。很多人纠结“单线程会不会浪费 CPU”,实际上 Redis 的瓶颈几乎从来不在 CPU,而在网络 IO、内存大小、fork 子进程这些地方。

选择哪个数据结构,本质上是看你要解决的是“映射”“顺序”还是“集合关系”。String 解决单值映射,Hash 解决对象和字段级操作,List 解决先后顺序,Set 解决去重和交集并集,ZSet 解决带权重的排序,GEO 解决地理位置,Bitmap 解决位级标记。把这张映射关系刻在脑子里,面试问“这个场景用什么”,你就能答出个所以然。

3. 缓存、分布式锁、计数器:三个高频场景,也是三个大坑

3.1 缓存穿透不是“穿透缓存”,而是“打穿到数据库”

第一个场景就是缓存。流程大家都熟:先查 Redis,没有就查数据库,再把结果写回 Redis 并设置过期时间。但“缓存穿透”这个坑,很多人是在线上被打挂之后才真正理解的——当查询一个 key 在 Redis 和数据库里都不存在时,每次请求都会直接落到数据库,缓存形同虚设。恶意攻击者随便构造几个不存在的 ID 就能把库压垮。

应对穿透的常规方案有两类:一是把“空值也缓存”,给一个很短的过期时间比如 60 秒,这样同一个不存在 key 的重复请求会打到 Redis;二是用第 16 个要讲的布隆过滤器,在查缓存之前先判断这个 key 是否可能存在,不存在就直接返回,连 Redis 都不查。更严谨的做法是两者结合,入口做 bloom 判断,穿透到缓存层的漏网之鱼用空值兜底。

缓存击穿和缓存雪崩也顺带说清楚。击穿是单个热点 key 在过期瞬间被大量请求打到数据库,解决办法是“逻辑过期”或者“互斥锁重建缓存”。雪崩是一大批 key 在同一时刻过期,导致数据库瞬间接住所有请求,解决办法很简单:过期时间加一个随机值,比如SET key value EX (300 + random(60)),把雪崩变成“细水长流”。集群高可用和主从切换则是更高的兜底保障。

3.2 分布式锁的正确姿势:SETNX 到 Lua 的演进

第二个场景,面试必问的分布式锁。老一点的写法是SETNX lock_key unique_value,成功返回 1 就拿到锁。但这个命令没有过期时间,万一业务代码异常没释放锁,这把锁就永远解不开,死锁了。更糟的是早期有人再用EXPIRE lock_key 10补设过期时间,两步操作不原子,setnx 之后 expire 之前进程崩溃,照样死锁。

所以现在正确的写法是一条命令搞定:

SET lock:order:1001 uuid_value EX 30 NX

EX 指定过期时间,NX 保证不存在才设置。释放锁的时候也要小心,很多人直接DEL lock_key,会误删别人的锁——比如线程 A 的锁因为超时自动释放了,线程 B 拿到同 key 的锁,此时 A 恰好执行完去 DEL,把 B 的锁删了。所以必须先比较 value 再删,用 Lua 脚本保证两条命令的原子性:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

生产环境我更推荐用 Redisson,它内部封装了看门狗定时续期,默认 30 秒锁超时但业务没跑完会自动延长锁,避免“锁过期但业务还没执行完”这种尴尬引发并发问题。分布式锁没有银弹,你要清楚自己的场景是什么:短事务用原生 SET NX EX 完全够了,长任务和复杂重入需求直接上 Redisson。

3.3 计数器的原子性:INCR 背后为什么不会丢

第三个场景是计数器。文章浏览量、视频播放量、商品库存扣减,都能用 INCR 解决。INCR 是原子操作,多个请求同时进来,Redis 内部是排队执行的,不存在“读取-加一-写回”的中间态,所以不会像数据库 update 那样因为并发导致少算。

一个很常见的应用是“今天的热度值”:

INCR article:view:1001 EXPIRE article:view:1001 86400

要注意的是,业务上经常会遇到“先查后写”的逻辑,比如判断当前浏览量大于 1000 再做什么操作。这类操作不是原子的,如果并发高,还是需要用 Lua 脚本把判断和自增合并,或者用 WATCH/MULTI 事务。另外一个坑是计数器的 key 不能无限增长,有些系统一个 key 存总浏览量存了三年,最终变成一个大 key,操作耗时变长。建议按天分桶,比如article:view:1001:20240101,定期做归档合并。

4. 排行榜、UV、签到、会话共享:存量业务最值得改造的四个点

4.1 排行榜:ZSet 为什么比 MySQL 的 ORDER BY 强

第四个场景是排行榜,搞活动、社区热帖、游戏排名都会用到。ZSet 的每个成员带一个 score,Redis 内部按 score 天然排好序。查 Top10:

ZADD leaderboard:2024 1000 user:1 998 user:2 1230 user:3 ZREVRANGE leaderboard:2024 0 9 WITHSCORES

一次命令返回排行数据,时间复杂度 O(log(N) + M),比 MySQL 里ORDER BY score LIMIT 10高效得多,而且省掉了数据库连接开销。更妙的是修改分数,ZINCRBY能在一个命令里给用户加分,不会像数据库那样要先查再用新值 update,存在并发覆盖风险。

排行榜还有个经典难题:同分时怎么排序?比如两个用户都是 1000 分,谁先到谁在前。方案是用组合分数:score = 业务分 * 10^N + (截止时间戳 - 当前时间戳),把时间作为排序权重压到小数部分。实际项目中这样做会有精度问题,更稳妥的做法是只取业务分为 score,如果业务要求严格按时间排序,就在分数上做巧妙的偏移,或者干脆再存一份时间戳的 ZSet 辅助排序。

4.2 UV 统计:HyperLogLog 为什么省内存省到离谱

第八个场景是 UV 去重统计。如果你没有用过 HyperLogLog,我觉得它是 Redis 最被低估的类型。统计一个页面今天的独立访客数,误杀标准误差 0.81%,但占用空间永远只有 12KB 左右。什么概念?如果你用 Set 存 100 万个用户 ID,一个用户 ID 按 8 个字节算就要 8MB,HyperLogLog 只要 12KB,差了接近 700 倍。

PFADD uv:page:home user_1001 user_1002 user_1003 PFCOUNT uv:page:home PFMERGE uv:month uv:20240101 uv:20240102

PFADD 添加访客 ID,PFCOUNT 直接返回去重后的结果,PFMERGE 还能把多天的 UV 合并成月 UV。缺点是无法精确到具体是哪个用户,所以它只适合“只要数字、不要明细”的场景。有人问“那我要看实时 UV 怎么办”,HyperLogLog 的统计虽然不是精确值,但天级别、活动级别的误差完全能接受,而且响应速度是毫秒级。

4.3 Bitmap 签到:一个用户一年只花 46 字节

第九个场景是签到和在线状态。用 Bitmap 存储每个用户一年 365 天的签到记录:key 为sign:user:1001:2024,offset 用天数表示,某天签到了就把对应的 bit 置 1。

SETBIT sign:user:1001:2024 30 1 BITCOUNT sign:user:1001:2024

一年只需要 46 字节,一个用户要查连续签到天数,用BITFIELD一次取回一个月的位数据再逐个判断。这里有个容易踩的细节:setbit 的 offset 从 0 开始,所以第 1 天对应 offset 0,而不是 1;月份维度建议 key 里带年月,否则一个 key 无限涨下去又变成大 key。在线状态也同理,比如online:20240101这个 key 的 bit 位置代表用户 ID,BITCOUNT 一下就是当前在线人数,比在数据库里反复 UPDATE 在线时间表轻量得多。

4.4 会话共享:登录态从进程内存搬到 Redis

第五个场景是会话共享,多实例部署时非常关键。以前单机部署,登录态放 Tomcat 内存里没问题;上了多实例之后,负载均衡调度到不同机器,用户可能每点一个页面就要重新登录一次。解决办法是把 Session 抽出来放到 Redis:SET session:token userId EX 7200,每次请求拿 token 换 userId。

这个方案比后端自己写内存并发安全的 Session 简单得多,也比 “sticky session 粘住同一台机器” 更优雅,应用实例可以随时上下线、扩容缩容,会话不丢。要注意 key 的过期时间要和服务端空闲超时对齐,并且 token 值建议用 UUID 而不是自增 ID,防止遍历会话。如果你做的是纯前后端分离项目,用 JWT 也可以,但 JWT 无法主动吊销,需要做黑名单,Redis 恰好又能当这个黑名单存储。这么一描述你会发现:分布式系统里会话统一交给 Redis,既能主动控制过期,又能全局访问,属于性价比极高的改造。

5. 队列三兄弟:消息队列、延迟任务、发布订阅,别选错模型

5.1 List 做消息队列:LPUSH + BRPOP 为什么不那么可靠

第六个场景是消息队列。很多人第一次用 Redis 做队列就是 List 的 LPUSH + BRPOP。生产者LPUSH queue:task task_1001,消费者BRPOP queue:task 0,0 表示永远阻塞等待。BRPOP 是阻塞读,比轮询好很多,不会空转耗 CPU。

这个方案能扛住几十万的吞吐,但有两个硬伤。第一,没有 ack 机制,消费者从 List 里 BRPOP 出消息后崩溃,这条消息就彻底丢了,无法回到队列。第二,重复消费不好控制,同一个任务可能被不同消费者弹出。所以 List 队列适合“丢了也没关系”的场景,比如发送验证码、非关键日志异步处理。如果你的业务丢不起消息,就要上 Stream。

5.2 Stream:消费者组和消息确认让队列更可靠

Redis 5.0 引入的 Stream 就是冲着“可靠队列”来的,它提供了消费者组(Consumer Group)和 XACK 确认机制。生产者:

XADD order_events * event order.created order_id 1001

消费者从组里读消息之后,Redis 会记录这条消息的投递状态,处理完要 XACK:

XREADGROUP GROUP order_group consumer_1 COUNT 10 STREAMS order_events > XACK order_events order_group 1700000000000-0

如果消费者处理失败没有确认,消息会进入 Pending 列表,之后可以用 XAUTOCLAIM 把它转移给其他消费者重新处理。这套机制看起来已经接近专业 MQ 了,但要注意:Stream 的消息依然存在内存里,Redis 重启没开 AOF 或 RDB 持久化,消息照样会丢。你要真在核心链路用 Stream,持久化配置必须开,而且最好评估一下消息堆积量会不会把内存打爆。轻量可靠请它出场,重量级高可靠性还是交给 RabbitMQ、Kafka 这些专门组件。

5.3 延迟任务:ZSet 的 score 就是你的定时器

第七个场景是延迟任务:订单超时未支付自动关闭、定时提醒、30 分钟未确认自动退款。这类需求没必要上 MQ 的延迟队列插件,用 ZSet 就能做。

原理很简单:score 存期望触发时间的时间戳,value 存任务 ID。生产者:

ZADD delay:order 1735689600 order_1001

消费者每隔一段时间(比如 1 秒)扫描一次:

ZRANGEBYSCORE delay:order 0 1735689600 LIMIT 0 100

拿到到期任务后,先 ZREM 从集合里移除,再执行真正的业务逻辑。这里有个关键细节:必须用 Lua 脚本把“取任务”和“删任务”做成原子操作,否则两个消费者会拿到同一个任务重复执行。轮询时间也不宜设太短,Redis 被每秒一次的空查询打满没有意义,一般 1 秒到 5 秒都行,取决于业务对延迟的容忍度。

如果任务量很大、还要求定时精度非常高,这种轮询方案就不合适了。但在“过期关单”“定时提醒”这类分钟级延迟容忍场景里,它是最简单可靠的实现,我甚至用它替换过一个重型延迟中间件,效果很好。

5.4 发布订阅:实时通知模型别和消息队列混为一谈

第十二个场景是发布订阅,特点是“广播”而不是“抢消息”。客户端 SUBSCRIBE 订阅 channel,服务端 PUBLISH 发一条消息,所有订阅者都会立刻收到。

SUBSCRIBE order_notify PUBLISH order_notify "order_1001 paid"

它和消息队列的本质区别是:发布订阅没有消息堆积,客户端不在线消息就直接没了;消息队列里消息会被保存、等消费者上线来取。所以 Pub/Sub 适合实时性场景,比如站内通知、直播弹幕、配置中心推送。一旦消息重要性高,或者对“离线消息补推”有要求,就必须回到 Stream 或专业 MQ。这个选择做错很致命,我见过有人把秒杀消息发给 Pub/Sub,结果消费者一崩,整个秒杀结果全丢了。

6. 附近的人、限流、购物车、抽奖、共同关注:这些花式玩法你迟早用得上

6.1 附近的人:GEO 把经纬度算距离变成内置操作

第十个场景是“附近的人”“附近的门店”。GEO 类型在 Redis 内部是用 ZSet 实现的,分数就是 geohash 编码的距离值,所以它天然支持“按距离排序”。示例:

GEOADD user_location 116.397128 39.916527 user_1001 121.473701 31.230416 user_1002 GEOSEARCH user_location FROMMEMBER user_1001 BYRADIUS 10 km ASC COUNT 20

这一条命令就返回以 user_1001 为圆心、半径 10 公里内最近的 20 个人。老版本的 GEORADIUS 也能做,但 GEOSEARCH 是 6.2 之后更推荐的命令,支持按矩形范围搜索,语义也更清楚。

注意事项:写入时经纬度顺序是“经度 纬度”,不要写成纬度在前,否则坐标直接跑偏到另一个地方;另外距离计算是球面距离,使用的是地球半径近似值,几十公里内误差可忽略。地图“附近”这类需求,只要数据量在百万级别以内,Redis 完全够用,不用上 MongoDB 或者 PostGIS 那么大阵仗。

6.2 限流:INCR + EXPIRE 一分钟一个 key

第十一个场景是限流。最经典的固定窗口限流:用户 ID 做 key,每个窗口周期设置一个过期时间。用户每请求一次就 INCR,如果计数超过阈值就拒绝。

set limiter:user:1001 1 EX 60 NX INCR limiter:user:1001

判断逻辑可以拆两步,但要防并发,最好是 Lua 脚本把 INCR、判断、EXPIRE 合并。固定窗口有个明显的边界问题:第 59 秒和第 61 秒各允许了 100 个请求,中间跨过一个窗口,两个窗口加起来可能有 200 个请求通过。要更平滑可以用滑动窗口或者令牌桶,Redis 里用 ZSet 记录每个请求的时间戳,每次请求前 ZREMRANGEBYSCORE 删掉窗口外的记录,再统计窗口内的数量。这套方案精度好一些,但每个用户都要维护一个 ZSet,单个 key 内成员数量太大时占内存,所以生产环境针对普通用户更推荐固定窗口,只有核心 VIP 才用精密的滑动窗口。如果并发非常大且不想在 Redis 里存太多 key,那就上 Guava 本地限流或者网关统一限流,别死磕 Redis。

6.3 购物车:Hash 是天然的“字段级购物车”

第十三个场景是购物车,用 Hash 再适合不过:key 是用户 ID,field 是商品 SKU,value 是数量。加购是 HSET,增加数量是 HINCRBY,修改数量、删除商品、查询全部都是字段级的原子操作:

HSET cart:user:1001 sku_1001 1 HINCRBY cart:user:1001 sku_1001 1 HDEL cart:user:1001 sku_1001 HGETALL cart:user:1001

这种设计比 MySQL 里的 cart 表省了太多麻烦,不用每次读写都维护数据库连接。不过要注意:Redis 里只适合存购物车结构和数量,商品标题、价格这些详情不应该同步存进 Hash,否则价格变动要同步改所有用户购物车,非常痛苦。主流做法是购物车模块价格只在结账时读取商品服务的最新价格,Redis 里的 Hash 只当“列出车里有啥、各买了几件”的快照。

6.4 抽奖和共同关注:也一个 Set 全搞定

第十四个场景是抽奖,Set 自带“去重”和“随机”两个特性。活动开始时把所有参与用户 SADD 进集合,开奖时 SRANDMEMBER 随机取 N 个不重复的用户(允许重复中奖就用 SRANDMEMBER,抽完即弃用 SPOP,直接弹出从集合移除)。

SADD draw:activity_1001 user_1001 user_1002 SRANDMEMBER draw:activity_1001 3 SPOP draw:activity_1001 3

第十五场景是社交关系里的“共同关注”。用户关注列表是天然的去重集合,用 Set 存user:1001:follow = [user_2001, user_2002, ...],查看两个人互相关注了谁、共同关注了谁,一条 SINTER 就出来:

SINTER user:1001:follow user:1002:follow SISMEMBER user:1001:follow user_2001

如果你是“你可能认识的人”这类推荐场景,还可以把“好友的好友”做并集后减去自己的好友,也能用 Set 的 SADD、SUNION、SDIFF 组合完成。这种方案在百万好友量级下依然快得惊人,比关系型数据库的连表查询优雅很多。

7. 布隆过滤器:把缓存穿透拦在 Redis 之前

7.1 原理:说“一定不在很容易”

第十六个场景是布隆过滤器。它回答的是一个概率问题:一个元素是否存在。注意这里的表述,结果是“一定不存在”和“可能存在”,而不是“一定存在”,因为多个元素经过多个哈希函数映射到同一个位上,会产生假阳性。但反过来,只要有一个位是 0,就说明这个元素从来没有被添加过,所以它判断“不存在”是绝对准确的。

把这个特性用到缓存穿透上:系统启动时把所有存在的商品 ID 加载进布隆过滤器,然后对每个查询请求先问布隆“这个 ID 存在吗”,它回答不存在,就直接返回“无数据”,根本不碰 Redis 和数据库。只有它回答“可能存在”时才透传到缓存和数据库,数据库查不到也不会让它击穿,因为恶意构造的随机 ID 大概率会被过滤器拦截。

7.2 三种落地方式,按团队情况选

第一种是使用 Redis 自带的 Bloom 模块,装 RedisStack 镜像就能用BF.ADDBF.EXISTS。这是最省事的方式,不过要注意官方模块与 Redis 版本兼容问题,生产环境如果是自己编译的 Redis,要重新把模块编译进去。

第二种是 Java 端直接用 Guava 的 BloomFilter,同步数据和判断先在本地做,然后只把“可能存在”的请求放行。这种方式不用改 Redis 版本,但本地内存里保存的过滤器副本和 Redis 里的数据可能出现同步偏差,需要定期重建。

第三种是自己在 Redis 里通过 Setbit 模拟位数组,用多个字符串 key 拼一个超大位图,配合 Lua 做判断。这种适合临时应急,性能不如原生模块,但胜在环境干净。实际选型时,如果只是防止缓存穿透,我更推荐第一种,前提是你们能接受 RedisStack 模块;如果 Redis 环境不能动,就用 Guava 本地过滤器冷启动时重建一次,也能解决大部分攻击流量。

还有一个常被忽略的点:布隆过滤器不是只能防穿透,它也可以做“已读消息过滤”“爬虫 URL 去重”“黑名单拦截”。原理都一样,就是判断“见没见过”,一鱼多吃。

8. 面试官最爱问的 Redis 追问,以及我的掉坑复盘

8.1 面试高频追问,答好这几条就稳了

顺着这 16 个场景,面试官通常还会深入追问几类问题。第一类是“Redis 为什么这么快”,答案不是简单一句“内存”,而要说三层:内存操作、单线程避免了线程切换和锁竞争、IO 多路复用使得网络读写不会阻塞命令执行。第二类是“key 过期策略是什么”,主动定期删除加惰性删除,定期删除是每 100ms 随机抽一批 key 检查过期,惰性删除是每次访问 key 时判断有没有过期。如果有大量 key 同时到期又抽不完,内存里会产生过期 key 积累,所以过期时间加随机值在这里又能帮上忙。

第三类是持久化选型,RDB 适合做备份和快速恢复,AOF 数据更全但文件大、恢复慢,混合持久化是折中方案。核心业务宁可让性能稍微下降一点也要开 AOF,我线上一般用appendfsync everysec。第四类是缓存和数据库一致性,最实用的策略是 Cache Aside:读先读缓存,读不到再读数据库并回填;写的时候“先更新数据库,再删缓存”。删除缓存失败时要考虑补偿,比如延时双删或者监听 binlog 同步失效。第五类是大 key 问题,一条命令操作大量数据会把单线程的 Redis 卡住,排查慢日志,发现大 key 可以拆分为多个小 key 或者用 SCAN 渐进遍历。

8.2 我的三个掉坑复盘

第一个坑是用 List 做核心消息队列。当时图它简单,上线后消费者服务发版重启,积压在 List 里的消费到一半的消息全部没有确认,发版期间从库同步正常,但消息本身丢了一批。后来换成了 Stream,靠 XACK 确认机制,重启之后能用 XAUTOCLAIM 继续消费,再没丢过。

第二个坑是分布式锁误删。早期我写锁用的是SETNX + EXPIRE两步,高并发下出现过一个请求拿到锁但还没执行完锁就过期了,另一个请求拿到锁,前面的请求执行完 DEL 把后面的锁删了,导致两段临界区同时执行。后来改成 Redisson + 看门狗续期,以及 Lua 脚本比较 value 再删,才彻底解决。

第三个坑是热点 key 压垮单节点。一个爆款商品的详情页缓存 key 被大量请求同时命中,虽然 Redis 扛住了缓存读取,但所有请求都挤在同一台从库上,最终把从库的网卡打爆。后来加了本地进程缓存做二级缓存,热点 key 在应用内存里直接返回,只有本地不存在时再走 Redis;同时热点 key 的过期时间不设置固定值,用逻辑过期加异步刷新,请求永远不走数据库重建,只让一个后台任务去建立新缓存。

这些都是我真实吞过苦水的地方。Redis 看起来只是一个 key-value 数据库,但每一条命令背后都有取舍,每一个场景都有对应的数据结构和边界条件。把这 16 个常见使用场景吃透,不管是在业务里选型还是在面试里对答,你都会从容很多。最后再分享一个我自己的习惯:每次代码里要引入一个 Redis 用法之前,先问自己三个问题——用哪种结构能最准确表达数据关系?命令是否原子,要不要 Lua 脚本兜底?key 的过期时间和超时策略是什么?想清楚了再动手,线上事故能少一大半。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 17:45:02

iOS App完整上架流水线:从Swift开发到App Store审核

1. 这不是又一本“Swift语法速查手册”,而是一条能真正跑通的iOS开发流水线你点开这个标题,大概率不是想再看一遍“var和let有什么区别”或者“闭包怎么写才不循环引用”。我干这行十一年,带过三十多个从零起步的学员,亲手帮他们把…

作者头像 李华
网站建设 2026/9/18 17:44:46

oh-my-hermes:React Native 中 Hermes 引擎的配置调优与优化实践

1. 从"引擎能用"到"引擎好用":oh-my-hermes到底解决什么问题先说个我自己的真实经历。去年上半年,我们团队把一个中度体量的 React Native 应用升级到 0.72,随之而来的是 Hermes 从"可选引擎"变成了默认引擎。…

作者头像 李华
网站建设 2026/9/18 17:39:46

STM32启动流程详解:从向量表到main函数发生了什么

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 17:38:30

MCP Server 生产级开发:错误处理、流式进度与部署实践

说实话,过去半年我身边几乎所有做 AI 应用的人都在聊 MCP。Cursor 里挂 MCP 服务器、Claude Desktop 里配 MCP、本地部署的 Ollama/DeepSeek 也想通过 MCP 把工具调用能力接出来。但有个很现实的问题:用别人写好的 MCP server 很简单,自己动手…

作者头像 李华
网站建设 2026/9/18 17:38:21

Vue keep-alive 下 activated 钩子的正确用法与状态同步策略

简介:本资源是一份聚焦 Vue.js 实际开发痛点的深度实践指南,面向中初级前端开发者,专门解决「页面返回时因重复请求导致用户操作状态丢失」这一高频问题。通过详解 keep-alive 缓存机制与 activated 生命周期钩子的协同用法,结…

作者头像 李华
网站建设 2026/9/18 17:37:15

力扣题解高效使用法:按题型拆解与模板化刷题之道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华