Redis 面试问答
1. 为什么要 Redis?
MySQL 数据在磁盘上,高并发读热点时 QPS 容易顶不住。Redis 把数据放内存,读写是微秒级,一般当加速层挂在 MySQL 前面——扛热点读、分担压力,不是拿来替代主库的。
2. 常见应用场景
| 场景 | 结构 | 说明 |
|---|---|---|
| 数据缓存 | String / Hash | 热点先查 Redis,miss 再查 DB 回填 |
| Session / 登录态 | String + TTL | 多实例共享登录信息 |
| 计数 | String + INCR | 点赞、浏览量,原子递增 |
| 排行榜 | ZSet | 按 score 排序取 Top N |
| 分布式锁 | String + SET NX EX | 防重复执行、扣库存互斥 |
| 限流 | INCR + EXPIRE | 同一用户每分钟最多 N 次 |
缓存是最常见的用法。要做可靠消息队列还是 RabbitMQ / Kafka,Redis 的 List / Stream 只适合量小、能丢几条的场景。
3. 什么样的数据适合放缓存?
不是所有数据都值得进 Redis,大致看四条:
- 读多写少:商品详情、用户昵称头像——一天读几万次,改几次
- 热点明显:爆款 SKU、活动页配置——少数 key 扛大部分流量
- 能容忍短暂旧数据:类目树、系统配置——晚几秒同步问题不大
- 体积可控:单条 KB 级,总量算得出,内存装得下
反过来,账户余额、库存扣减这种强一致的得靠 DB;订单流水、操作日志写多读少,塞缓存命中率低;半年没人看的冷数据占了内存也白占;个性化推荐每人结果不同,缓存 key 会爆炸。
拿电商商品详情举例:同一个 productId 被大量重复访问,内容不常改,查 Redis 命中直接返回,miss 再查 MySQL 回填,TTL 设 30 分钟就行。用户改昵称后,先更新 DB,再删 user:{id} 缓存,下次读自动回填新值。
4. 为什么快?
根本原因是内存,比磁盘快几个数量级。
另外几个点面试也会问:单线程执行命令——读写串行,不用加锁、不用切线程,Redis 6 之后网络 IO 可以多线程,但命令还是单线程跑;IO 多路复用——一个线程监听大量连接,谁有数据处理谁;专用数据结构——String、Hash、ZSet 按场景优化过,不是一张通用表包打天下。
5. 五种类型
| 类型 | 典型用法 |
|---|---|
| String | 缓存 JSON、计数、分布式锁 |
| Hash | 一个对象多个字段,比如用户 profile |
| List | 简单队列、最新消息 |
| Set | 去重、共同好友、标签 |
| ZSet | 排行榜,按 score 排序 |
和 Memcached 比:Memcached 只有 String、不能持久化;Redis 类型多、能落盘、集群方案成熟,新项目基本选 Redis。
6. 持久化
数据在内存里,重启就没了。RDB 是隔一段时间拍快照,恢复快,但两次快照之间的写入可能丢。AOF 是记每条写命令,像写日记,丢得少,文件大、恢复慢。生产里常见做法是混合:快照打底,日志补最近的增量。
7. 缓存穿透、击穿、雪崩
用缓存绕不开这三个问题。
穿透是有人查 DB 里根本不存在的 id,缓存永远 miss,请求全打到 DB。简单做法是在 DB 也查不到时,往 Redis 存一个 null,TTL 设短一点,同一个恶意 id 再来就直接挡掉。合法 id 很多、伪造请求又多的话,可以在前面加布隆过滤器——一种很省内存的「可能存在名单」,把库里有的 id 预先放进去;它说「不存在」就一定不存在,直接返回;说「可能存在」再去查 Redis 和 DB(有极低概率误报)。布隆过滤器不是 Redis 内置类型,要用得装 RedisBloom 模块,或者在应用层自己维护。小项目缓存空值通常就够。
击穿是热点 key 到期被 Redis 删掉了,同一瞬间大量请求一起 miss、打穿 DB。可以加互斥锁,过期后只允许一个线程回源查 DB、重建缓存,其余等着或重试。还有一种叫逻辑过期的做法:key 本身不设 TTL,在 value 里自己存过期时间,比如 { "expireAt": 1710003600, "data": {...} }。读的时候如果业务上判断已过期,仍然先把旧 data 返回给用户,同时另起线程异步去 DB 拉新的——key 不会在 Redis 里突然消失,就不会出现全员同时 miss 的情况。代价是实现复杂些,过期窗口内用户可能看到稍旧的数据。
雪崩是大量 key 同一时刻过期,或者 Redis 整机挂了,DB 被冲垮。给 TTL 加随机偏移错开过期时间,配合集群和限流降级。
8. 缓存和 DB 怎么一致?
最常用的是 Cache Aside:读的时候先查缓存,没有就查 DB 再回填;写的时候先更新 DB,再删缓存——注意是删不是改,直接改缓存容易在并发下乱序。这只能做到最终一致,删完缓存到下次读之间可能还有旧值,强一致还是得靠 DB。
9. 分布式锁
单机 lock 关键字在多实例部署时管不了别的机器,需要有个大家都能访问的地方抢锁,Redis 是常见选择。
用在哪: 定时任务三台机器都配了凌晨 2 点跑报表,加锁后只有一台真正跑;用户连点两次支付,第二次拿不到锁直接拒绝;同一订单的状态流转只允许一个流程在处理。秒杀扣库存高并发时,有时用 Redis Lua 原子扣减比锁更轻;锁更适合「整段业务同一时刻只允许一个进来」的场景。QPS 极高、或者金融账务强一致的,锁不是首选。
怎么做: 加锁一条命令:
SET lock:stock:10086 <随机UUID> NX EX 30
NX 表示 key 不存在才设成功,抢到了;设不成功说明别人占着。EX 30 是 30 秒自动过期,进程崩溃没释放也不会死锁。value 用 UUID,释放时才知道删的是不是自己的锁。
业务跑完释放锁,不能先 GET 再 DEL——中间锁可能过期被别人抢走,你再 DEL 就删了别人的。得用 Lua 把「比对 value + 删除」合成一步:
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
业务跑得比 TTL 长,就起个看门狗线程定期续期。另外锁是互斥不是排队,拿不到就失败或重试;主从切换极端情况下锁可能丢,要求绝对可靠得上 ZooKeeper,或者业务本身做幂等。
10. 主从、Sentinel、Cluster
单机 Redis 扛读压力或者想要备份,会上主从:一主多从,写走主,主异步把数据推给从,从可以分担读。复制是异步的,主不等从确认就返回,所以性能好,但从可能落后——刚写主库立刻读从库,可能还是旧值。主挂了纯主从得人工把某个从升为主,生产一般会继续加 Sentinel。
Sentinel 是独立部署的监控进程,通常起奇数个(比如 3 个),盯着主从节点的健康;主挂了自动投票选一个从升为新主,再通知客户端改连新地址。它解决的是「主节点故障怎么自动切换」,不管数据分片,也不解决单机内存不够。
数据量再大、一台机器的内存就不够用了,得上 Cluster,把数据分到多台机器上存——每台只存一部分,加起来才是完整的数据。
Cluster 怎么决定某个 key 存哪台机器?它把 key 空间切成 16384 个槽(slot),可以理解成 16384 个编号格子,0 号到 16383 号。每个 key 根据名字算出一个数,对 16384 取模,落到某个格子上,这个格子分配给哪台机器,key 就在哪台机器上。比如 0~5000 号槽给 A 机器,5001~10000 给 B 机器。
客户端连任意一台都能问,如果连错了机器,对方会告诉你「这个 key 不在我这,去另一台」,这就是 MOVED。扩容缩容要搬槽的时候,搬迁过程中会临时回 ASK,意思是「这数据正在搬,你先去找目标机器,但路由表还没完全更新」。
有个限制:一次操作如果要动多个 key(比如 MGET、事务),这些 key 必须在同一个槽里,否则 Redis 拒绝执行。想让两个 key 进同一个槽,可以用 hash tag——key 名里加 {},只有花括号里的部分参与算槽号。比如 user:1001:profile 和 user:1001:orders 可能落在不同机器,改成 {1001}:profile 和 {1001}:orders,花括号里都是 1001,就会算到同一个槽。
Cluster 里每个负责一部分数据的主节点也可以有自己的从节点,某台主挂了,它的从能顶上来,和前面说的主从 failover 是一个思路,只是多了一层「先分片、再高可用」。
实际选型很简单:数据一台内存够、怕主挂,主从 + Sentinel;数据超单机、要扩容,上 Cluster;开发小项目单机够用。Cluster 运维和客户端都麻烦,没到这个量级不必硬上。
11. 单线程、大 key、热 key
前面说了命令是单线程执行的,一个慢操作会堵住后面所有请求。KEYS * 扫全库、过长的 Lua 脚本都是雷区,遍历用 SCAN 分批来。
删 key 也要注意。DEL 是同步删,key 摘掉的同时主线程当场释放内存,删完才返回;UNLINK 是异步删,key 立刻从字典里摘掉、客户端已经查不到了,真正 free 内存交给后台线程慢慢做。一个 Hash 存了百万字段、或者 String 塞了几 MB JSON,用 DEL 可能卡主线程几百毫秒,这时用 UNLINK,主线程微秒级就返回了。小 key 两者差别不大。
大 key 本身就要从设计上拆开存,删的时候用 UNLINK,迁移也慢,都是同一个问题。热 key 是少数 key 扛了绝大部分流量,比如明星商品,可以在前面加本地缓存,或者多副本分散读;即使用了 Cluster,同一个 key 也只在一个节点上,分片挡不住单 key 过热。
12. 过期与内存淘汰
key 设了 TTL 也不会精确到毫秒删除。有人访问时发现过期了才删,这叫惰性删除;后台还会随机抽一批 key 检查过期,叫定期删除。所以过期 key 可能多存活一小会儿。
内存设了 maxmemory 上限之后满了,就要走淘汰策略。缓存场景常用 allkeys-lru(淘汰最久没用的)或 allkeys-lfu(淘汰访问最少的)。
13. Pipeline、事务、Lua
Pipeline 是客户端一口气发多条命令,减少网络往返,但不保证原子,只是打包传输。MULTI / EXEC 让多条命令连续执行、中间不被插队,但不支持回滚。需要「读-改-写」一体原子的时候,用 Lua 脚本,Redis 把整段脚本当一个命令执行。
14. Pub/Sub(发布订阅)
发布者往频道(channel)发消息,订阅了这个频道的客户端都会收到,彼此不知道对方是谁——典型的发布订阅模型。Redis 用 PUBLISH / SUBSCRIBE 实现。
缺点是消息不持久化:订阅者不在线就直接丢了,也没有 ack 确认,所以不可靠。要可靠队列用 Stream,或者 RabbitMQ / Kafka。
更多推荐




所有评论(0)