主题
Redis
本篇定位
覆盖数据类型、持久化、淘汰策略、缓存三大问题和分布式锁。
高频面试题
Q1:Redis 数据类型及场景?
我的答案:
结论:Redis 常用五种基本类型,再加三个高级结构,业务场景要对着数据形状选。
第一,String,最基础,字符串/数字都行。场景:计数器(INCR article:read:123)、分布式 session、缓存对象序列化后的 JSON、限速。最大 512MB,一般别塞这么大。
第二,Hash,field-value 的 map。场景:存对象,比如用户信息 user:1001 -> {name, age, email},比 String 存 JSON 好在能单独改某个字段,不用整个反序列化再写回。
第三,List,双向链表。场景:消息队列、最新列表、关注时间线。lpush/rpop 就是简单队列;注意它不是专业 MQ,没有 ack、没有持久化强保障。
第四,Set,无序去重集合。场景:标签、共同好友(sinter)、抽奖去重、UV。
第五,ZSet(Sorted Set),每个 member 带一个 score,按 score 排序。场景:排行榜(直播间热度、游戏排行)、延时队列(score 存时间戳)、带权重的任务队列。
高级结构三个:Bitmap,位操作,存签到、在线状态,一年签到才占 45 字节;HyperLogLog,基数估算,12KB 内存就能算上亿 UV,误差 0.81%,能容忍误差的统计场景非常香;Stream(Redis 5.0+),专业消息队列,支持消费组、ACK、 pending 列表,比 List 更像 MQ。
选型一句话:KV 计数用 String,对象用 Hash,队列用 List/Stream,去重交并用 Set,排行用 ZSet,海量统计用 HyperLogLog。
Q2:RDB 和 AOF 区别?
我的答案:
结论:RDB 是时间点快照,AOF 是追加写命令日志,生产一般两个都开,4.0 后还支持混合持久化。
RDB:在某个时刻把整个内存数据快照成一个 dump.rdb 二进制文件,靠 save(阻塞)或 bgsave(fork 子进程)触发。优点:文件小、恢复快、适合做备份和主从全量同步;缺点:两次快照之间宕机就会丢那一段时间的数据,fork 大实例时会短暂阻塞。
AOF:每写一条命令就追加到 appendonly.aof 文件(类似 MySQL binlog)。刷盘策略三种:always 每条都 fsync,最安全最慢;everysec 每秒 fsync,默认,最多丢 1 秒;no 交给 OS,最快最不安全。优点:数据安全,最多丢 1 秒;缺点:文件大、恢复慢。AOF 会重写(bgrewriteaof),把过期、覆盖的命令压缩成最小命令集,不然文件会无限膨胀。
Redis 4.0 开始支持混合持久化:AOF 重写时,把这一刻的数据用 RDB 格式写在 AOF 文件开头,之后的增量命令再用 AOF 文本格式追加。这样重启时先加载 RDB 部分(快),再 replay 后面的增量命令(丢得少),鱼和熊掌兼得,生产推荐开 aof-use-rdb-preamble yes。
实际选择:纯缓存、丢了能重建就关持久化;当数据库用就 RDB+AOF 都开,再定期把 RDB 拷到对象存储做异地备份。
Q3:缓存穿透/击穿/雪崩及解决?
我的答案:
结论:这三个都是缓存没挡住请求、压力直接打到 DB,但成因不同,方案也不同。
缓存穿透:查一个数据库里根本不存在的数据,每次都绕过缓存打到 DB。比如有人恶意刷 user?id=-1。方案:第一,对查不到的 key 也缓存一个空值(短 TTL,比如 60 秒);第二,前置布隆过滤器,把所有存在的 key 放 Bitmap 里,不存在的直接拒绝。注意空值缓存别放太久,不然真数据来了有短暂不一致。
缓存击穿:某个热点 key 突然过期,瞬间成千上万并发请求同时穿透到 DB。方案:第一,互斥锁,拿不到锁的线程 sleep 一下重试,只放一个线程去回源重建缓存(SETNX lock:key 抢锁,拿到的查 DB 写缓存,释放锁);第二,逻辑过期,value 里存一个逻辑过期时间,发现过期后异步开线程重建,当前请求先返回老数据,不等;第三,热点 key 提前预热、永不过期。
缓存雪崩:大量 key 同时过期,或者 Redis 整个节点挂了,请求全压到 DB。方案:第一,给 TTL 加随机抖动,比如基础 300 秒加 random(0~60) 秒,别让一堆 key 同一秒到期;第二,热点数据永不过期或后台刷新;第三,缓存层高可用(主从+哨兵/Cluster),DB 这边加限流、降级、熔断兜底,别让 DB 被打挂。
一句话区分:穿透是"查不存在的",击穿是"一个热点 key 过期",雪崩是"一大片 key 同时过期或缓存挂了"。
Q4:如何实现分布式锁?
我的答案:
结论:Redis 分布式锁最基本的命令是 SET key value NX PX 过期时间,要解决"互斥、防误删、可续期、原子释放"四个问题。
第一,加锁。SET lock:order:1001 <uuid> NX PX 30000。NX 意思是 key 不存在才设置成功,保证互斥;PX 30000 30 秒自动过期,防止持锁进程挂了死锁;value 必须是全局唯一标识(UUID+线程 id),不能随便写个固定值。
第二,释放锁。不能直接 DEL,否则可能把别人的锁删了——比如我 A 拿锁、执行超时、锁自动过期了、B 又拿到锁、A 醒过来执行完直接 DEL,把 B 的锁删了。正确做法:先判断 value 是不是自己的,再删,这两步要原子,用 Lua 脚本:
lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end第三,业务执行时间可能超过 30 秒怎么办?要看门狗(watchdog)自动续期:开一个后台线程,每隔过期时间的 1/3(比如 10 秒)检查自己还持不持锁,持就把过期时间重置回 30 秒。Redisson 客户端已经封装好了,直接用它的 RLock,别自己手写。
第四,集群问题。主从异步复制,主节点拿到锁还没同步给从就挂了,从升主后另一个客户端又能拿到同一把锁。严格场景用 Redlock(向 N 个独立主节点申请,多数成功才算拿到),但 Redlock 有争议(Martin Kleppmann 那篇文章),一般业务用 Redisson 单节点锁 + 主从就够,金融级强一致用 ZooKeeper/etcd 更稳。