最新PDD面经分享

343 阅读11分钟

一位组织内部的同学分享了他面试 拼多多 服务端研发工程师的面经,除了项目拷打主要还是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. 逻辑过期(异步重建)方案

原理:在缓存数据中内置逻辑过期字段,异步更新数据

执行流程:

判断逻辑过期时间是否到达

获取分布式锁触发异步更新线程

其他线程继续返回旧数据

  1. 热点数据永不过期方案

实现方式:

物理永不过期: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,备注:面试群。