一位组织内部的同学分享了他面试 拼多多 服务端研发工程师的面经,除了项目拷打主要还是Redis方面更多,问题我帮大家整理在下面了:
缓存穿透怎么解决?
那存储的key是什么?
那如果我是一名攻击者,我每次都随机生成不同的key去查询,那每一次都是穿透的,你怎么解决?
缓存击穿怎么解决?
缓存雪崩怎么解决?
redis怎么设置过期时间?除了set+ex外,还有其他命令去设置过期时间吗?
缓存击穿是有大量热Key并发访问,那redis中如何检测热key?
redis对过期key的删除策略有哪些?
redis中ZSet底层是用什么数据结构来实现的?
面经详解
缓存穿透怎么解决?
缓存穿透指查询数据库中不存在的数据,导致请求穿透缓存层直击数据库
解决方案:
1. 布隆过滤器(Bloom Filter)
原理:基于哈希算法的概率型数据结构,快速判断某个Key是否可能存在于数据库。
实现步骤: 系统启动时加载所有有效Key到布隆过滤器
请求先经过布隆过滤器判断:
若返回「不存在」则直接拦截请求
若返回「可能存在」则继续查询缓存/数据库
代码示例(Guava实现):
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(),
1000000, // 预期数据量
0.01 // 误判率
);
// 初始化加载数据
allKeys.forEach(bloomFilter::put);
// 请求拦截
if (!bloomFilter.mightContain(key)) return null;
2. 缓存空对象
原理:对数据库查询结果为空的Key,在缓存中存储特殊标记(如NULL或空对象)。 实现要点:
设置较短的过期时间(如5分钟)
对空对象进行类型标识(避免与正常数据混淆)
public Object getData(String key) {
Object value = redis.get(key);
if (value != null) {
if (value instanceof EmptyObject) return null; // 空对象标识
return value;
}
Object dbData = db.query(key);
if (dbData == null) {
redis.setex(key, 300, new EmptyObject()); // 空对象缓存
return null;
}
redis.setex(key, 3600, dbData);
return dbData;
}
3. 请求参数校验
原理:在业务层增加合法性校验,拦截非法请求。
典型场景:
商品ID必须为数字且范围校验
用户ID符合特定格式(如UUID)
请求参数包含必填字段
那存储的key是什么?
一、缓存空对象场景的Key设计 1. 空对象标识符
格式:原始Key + 特殊后缀
示例:user:12345:empty
作用:明确标识空对象,避免与正常数据混淆
2. 短TTL设计
策略:设置较短过期时间(如5分钟)
代码示例:
SET user:12345:empty "NULL" EX 300 # 设置5分钟过期
二、布隆过滤器场景的Key设计
1. 原始业务ID
格式:直接使用数据库主键或唯一标识
示例:商品ID product:67890、用户ID user:12345
作用:与数据库字段直接对应,确保过滤准确性
2. 分片存储优化
策略:按业务类型分片存储
示例:
商品过滤器Key:bloom:product
用户过滤器Key:bloom:user
优势:降低单个布隆过滤器的误判率
三、互斥锁场景的Key设计
1. 分布式锁Key
格式:业务前缀 + 原始Key + 锁标识
示例:lock:product:67890
代码示例(Redisson):
RLock lock = redissonClient.getLock("lock:product:" + productId);
lock.lock(10, TimeUnit.SECONDS); // 设置10秒自动释放
2. 锁粒度控制
原则:按最小业务单元加锁
示例:对库存扣减使用 lock:stock:sku_123
避免使用全局锁(如global_lock)导致性能瓶颈
那如果我是一名攻击者,我每次都随机生成不同的key去查询,那每一次都是穿透的,你怎么解决?
当攻击者每次生成不同的随机Key(如UUID、负数ID等),传统方案如缓存空对象、布隆过滤器会失效,因为:
缓存空对象:导致内存被大量无效Key占据,可能引发OOM
布隆过滤器:无法预判未知的随机Key,且动态更新存在延迟
参数校验:仅适用于有明显规则的业务场景
解决方案:
1. 动态布隆过滤器增强
原理:实时将新发现的无效Key加入布隆过滤器
实现步骤:
使用支持动态扩容的布隆过滤器(如RedisBloom)
当数据库查询结果为空时,触发异步线程将Key加入布隆过滤器
后续相同Key的请求被直接拦截
// 伪代码示例
if (!bloomFilter.mightContain(key)) {{
return "非法请求";
}} else if (redis.get(key) == null) {{
executor.submit(() -> {{
Object dbData = db.query(key);
if (dbData == null) {{
bloomFilter.add(key); // 动态更新过滤器
redis.setex(key, 5, "NULL"); // 短期空对象
}}
}});
}}
优点:自适应防御重复攻击
缺点:首次随机Key仍会穿透,需配合其他方案
2. 请求参数规则校验
防御策略:
格式校验:检查ID是否为数字/UUID格式(正则匹配)
范围校验:如商品ID必须>0且<10^18
长度限制:如关键词搜索不得超过50字符
代码示例:
// 校验商品ID合法性
if (!key.matches("^\\d{1,18}$")) {{
throw new IllegalArgumentException("非法参数");
}}
3. 高频随机Key熔断限流
实现方式:
接口级限流:通过Sentinel对/query接口设置QPS阈值(如1000次/秒)
IP级限流:Nginx限制单个IP的访问频率(如100次/分钟)
降级策略:触发限流时返回默认值(如"系统繁忙")
4. 智能空对象缓存优化
改进方案:
压缩存储:使用特殊标记替代完整空对象(如SET key "NULL" EX 5)
内存控制:
设置最大空对象数量(如Redis的maxmemory-policy allkeys-lfu)
空对象TTL分级(高频Key 5秒,低频Key 60秒)
缓存穿透怎么解决?
缓存击穿指高并发热点Key失效时,大量请求直击数据库,导致:
数据库瞬时QPS飙升(可能导致宕机)
缓存层失去流量缓冲作用
系统可用性严重下降
解决方案:
1. 互斥锁(分布式锁)方案
原理:通过分布式锁控制缓存重建并发度,确保只有一个线程执行数据库查询和缓存重建 实现方式:
public Object getDataWithLock(String key) {
Object data = redis.get(key);
if(data == null) {
String lockKey = "lock:" + key;
if (redis.set(lockKey, "1", "NX", "EX", 10)) { // SETNX原子操作
try {
data = db.query(key); // 查数据库
redis.setex(key, 300, data); // 写入缓存
} finally {
redis.del(lockKey); // 释放锁
}
} else {
try {
Thread.sleep(50); // 等待重试
return getDataWithLock(key); // 递归重试
} catch (InterruptedException e) { ... }
}
}
return data;
}
优点:强一致性保障,适用于金融交易等场景
缺点:锁竞争影响吞吐量,需合理设置锁过期时间
2. 逻辑过期(异步重建)方案
原理:在缓存数据中内置逻辑过期字段,异步更新数据
执行流程:
判断逻辑过期时间是否到达
获取分布式锁触发异步更新线程
其他线程继续返回旧数据
- 热点数据永不过期方案
实现方式:
物理永不过期:redis.set(key, value)
配合后台定时任务更新(如每5分钟刷新) 适用场景: 电商首页推荐商品,新闻热点排行榜
缓存雪崩怎么解决?
缓存雪崩指在短时间内大量缓存Key集中失效或Redis集群宕机,导致所有请求穿透到数据库,可能引发:
数据库瞬时压力激增(QPS超过承载阈值)
系统级联故障(服务雪崩)
业务响应时间飙升(用户体验恶化)
解决方案:
1. 缓存失效时间随机化
原理:为每个Key设置基础TTL+随机偏移量,分散失效时间
int baseTTL = 3600; // 基础过期时间1小时
int randomOffset = new Random().nextInt(600); // 0-10分钟随机值
redis.setex(key, baseTTL + randomOffset, value);
效果:避免大量Key同时失效
2. 热点数据永不过期+异步更新
实现方式: 物理不设置过期时间:redis.set(key, value)
通过定时任务(如Quartz)或监听binlog异步更新数据
适用场景:电商商品详情页、新闻头条等
3. 多级缓存架构 架构设计:
graph TD
A[客户端] --> B{{本地缓存 L1}}
B -->|命中| A
B -->|未命中| C{{分布式缓存 L2}}
C -->|命中| A
C -->|未命中| D[数据库]
策略:
L1使用Caffeine/Guava Cache(TTL 30秒)
L2使用Redis集群(TTL 10分钟)
redis怎么设置过期时间?除了set+ex外,还有其他命令去设置过期时间吗?
一、基础命令设置方式
1. 秒级过期(EXPIRE)
EXPIRE key 60 # 设置60秒后过期
TTL key # 查看剩余秒数(返回-2表示Key不存在)
特点:最常用指令,适合大多数业务场景。
2. 毫秒级过期(PEXPIRE)
PEXPIRE key 15000 # 设置15000毫秒(15秒)后过期
PTTL key # 查看剩余毫秒数
适用场景:高精度时间控制需求(如分布式锁)。
3. 绝对时间戳过期(EXPIREAT/PEXPIREAT)
EXPIREAT key 1760000000 # 设置Unix秒级时间戳过期
PEXPIREAT key 1760000000000 # 设置Unix毫秒级时间戳过期
适用场景:定时任务触发场景,如活动截止时间。
4. SET扩展命令
SET key value EX 60 # 写入数据并设置60秒过期
SETEX key 60 value # 字符串专用命令(效果同上)
特点:原子性操作,适合写入时需设置过期时间的场景。
二、进阶设置方案
1. 批量设置过期时间
# 通过Shell脚本批量设置(示例)
redis-cli KEYS "prefix:*" | xargs -I{} redis-cli EXPIRE {} 300
注意:生产环境慎用KEYS *,推荐使用SCAN迭代。
2. 动态过期策略
-- Lua脚本实现动态过期时间(示例)
local defaultExpire = 60
redis.call('SET', KEYS[1], ARGV[1])
redis.call('EXPIRE', KEYS[1], defaultExpire)
用途:统一管理默认过期时间,减少代码冗余
缓存击穿是有大量热Key并发访问,那redis中如何检测热key?
内置命令工具
1. hotkeys 命令(Redis 4.0+)
原理:基于 LFU 算法统计 Key 访问频率
使用方式:
redis-cli --hotkeys # 输出按访问频率排序的热 Key
优点:原生支持,无需代码改造
缺点:
数据量大时扫描慢(全量遍历)
仅统计内存中的 Key(不包含已过期 Key
2. monitor 命令
原理:实时捕获所有 Redis 操作日志
使用方式:
redis-cli monitor > redis.log # 输出到日志文件
分析工具:
使用 redis-faina 等工具解析日志:
cat redis.log | ./redis-faina.py # 生成热 Key 报告
优点:实时性高
缺点:
高并发场景可能引发性能问题(内存/CPU 暴涨)
需额外处理日志存储和分析
redis对过期key的删除策略有哪些?
一、被动删除(惰性删除)
原理:当客户端访问某个 Key 时,Redis 会先检查该 Key 是否过期。若已过期则立即删除,未过期则返回数据。
优点:
CPU 资源友好,仅在访问时触发删除操作
避免非热点数据的额外检查开销
缺点:
内存不友好,长期不被访问的过期Key会堆积
极端情况可能导致内存泄漏
二、主动删除(定期删除)
原理:Redis 周期性(默认每秒10次)随机采样检查设置了过期时间的 Key,删除已过期的 Key。
优点:
弥补惰性删除的内存管理缺陷
通过采样平衡 CPU 与内存占用
缺点:
仍存在未被抽样的过期Key残留
高强度清理可能影响服务性能
三、强制删除(内存淘汰策略)
触发条件:当内存达到 maxmemory 限制时,根据策略强制淘汰 Key。
常见策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| volatile-lru | 淘汰最近最少使用的带过期Key | 需要热点数据保留 |
| volatile-ttl | 淘汰剩余存活时间最短的Key | 快速清理短期数据 |
| volatile-random | 随机淘汰带过期Key | 内存压力大时快速释放 |
| allkeys-lru | 淘汰所有Key中的最近最少使用 | 非核心数据缓存 |
Redis 实际采用的是 惰性删除 + 定期删除 + 内存淘汰 的复合策略:
日常通过惰性删除处理访问热点数据
定期删除清理部分过期数据缓解内存压力
内存不足时触发强制淘汰保障服务可用性
redis中ZSet底层是用什么数据结构来实现的
Redis 的 ZSet 根据数据量和元素大小,动态选择以下两种数据结构:
压缩列表(ziplist)
使用条件(需同时满足): 元素数量 ≤ zset_max_ziplist_entries(默认 128)
每个元素大小 ≤ zset_max_ziplist_value(默认 64 字节)
存储方式:
每个元素由两个连续的 entry 组成:[member, score]
按 score 升序 排列,实现紧凑内存存储
跳表(skiplist) + 哈希表(dict)
触发条件:突破上述任一限制
组合结构:
跳表(zskiplist):按 score 排序,支持快速范围查询(时间复杂度 O(logN))
哈希表(dict):存储 member → score 的映射,实现 O(1) 时间查询单个元素分值
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。