Redis 3 大问题 + 5 大扩展问题

一、Redis 3 大经典问题(面试 100% 必考

1.1 雪崩(Avalanche

问题:大量 key 同一时间过期,导致所有请求打到数据库

复制代码
早上 9:00
  ↓
Redis 里 50w 个缓存 key 全部过期(设的同一时间,比如 1 小时)
  ↓
⚠️ 50w 个请求同时打 MySQL
  ↓
MySQL 扛不住,连接池耗尽,CPU 100%
  ↓
⚠️ 系统雪崩

项目场景

  • 报表 50w 个任务缓存,凌晨 0 点同时过期
  • 27 家分行的客户数据缓存,早 8 点同时过期
  • 50w+ 请求瞬间打 MySQL,DB 连接池爆了
1.2 穿透(Penetration

问题:查询一个不存在的 key,每次都打到数据库

复制代码
恶意攻击 / 业务 bug
  ↓
查询 user_id = -1(不存在)
  ↓
Redis 没这个 key → 查 MySQL
  ↓
MySQL 也没这个 user → 返回 null
  ↓
⚠️ 但每次都查 MySQL,**没有缓存保护**
  ↓
⚠️ 攻击者用 100w 个不存在的 user_id 查
  ↓
MySQL 被 100w 次无效查询打爆

项目场景

  • 27 家分行的客户敏感数据查询,被恶意传 100w 个不存在的身份证号
  • 外部数据采集,外部传 100w 个不存在的客户号
  • 100w 次无效查询打爆 MySQL
1.3 击穿(Breakdown

问题:1 个热点 key 过期,瞬间大量请求打到数据库

复制代码
双 11 大促 / 春晚红包 / 明星离婚
  ↓
某个热点商品的缓存 key 过期
  ↓
⚠️ 100w 个用户同时查这个商品
  ↓
100w 个请求同时打 MySQL
  ↓
MySQL 扛不住,系统雪崩

项目场景

  • 春节红包雨,某个热门红包的库存 key 过期
  • 央行降息,某个热门理财产品的详情 key 过期
  • 100w + 用户同时查 1 个 key,DB 被打爆

二、3 大问题的解决方案(5 套方案

2.1 雪崩的 4 种解决方案
方案 1:过期时间加随机值最常用
复制代码
// ❌ 错误:所有 key 同一时间过期
redisTemplate.opsForValue().set("report:2024", data, 1, TimeUnit.HOURS);

// ✅ 正确:过期时间加随机值(0-300 秒)
int baseExpire = 3600;  // 1 小时
int randomExpire = RandomUtil.randomInt(0, 300);  // 0-300 秒
redisTemplate.opsForValue().set("report:2024", data, 
    baseExpire + randomExpire, TimeUnit.SECONDS);

原理:50w 个 key 不会同时过期,分散到 0-300 秒

方案 2:多级缓存
复制代码
┌─────────────────────────────────────────┐
│ L1: Caffeine(本地缓存,1 秒过期)           │ ← JVM 内存
├─────────────────────────────────────────┤
│ L2: Redis(分布式缓存,1 小时过期)           │ ← 共享内存
├─────────────────────────────────────────┤
│ L3: MySQL(数据库,永久)                   │ ← 磁盘
└─────────────────────────────────────────┘

"项目用 Caffeine + Redis 多级缓存L1 缓存 1 秒过期,L2 缓存 1 小时过期避免 50w+ key 同时过期雪崩。"

方案 3:熔断降级Sentinel / Resilience4j
复制代码
@SentinelResource(value = "queryOrder", fallback = "queryOrderFallback")
public Order queryOrder(Long orderId) {
    // 查 Redis
    Order order = (Order) redisTemplate.opsForValue().get("order:" + orderId);
    if (order == null) {
        // 查 MySQL
        order = orderMapper.selectById(orderId);
        redisTemplate.opsForValue().set("order:" + orderId, order, 3600, TimeUnit.SECONDS);
    }
    return order;
}

// 熔断降级:Redis 挂了直接返回默认值
public Order queryOrderFallback(Long orderId, Throwable e) {
    log.warn("Redis 熔断降级, orderId={}", orderId, e);
    return orderMapper.selectById(orderId);  // 直接走 MySQL
}
方案 4:Redis 集群 + 高可用根本上解决
复制代码
Redis Sentinel(哨兵):主从自动切换
Redis Cluster(集群):数据分片 + 故障转移
2.2 穿透的 3 种解决方案
方案 1:空值缓存最常用
复制代码
// ❌ 错误:null 不缓存
public Order queryOrder(Long orderId) {
    Order order = (Order) redisTemplate.opsForValue().get("order:" + orderId);
    if (order == null) {
        order = orderMapper.selectById(orderId);
        if (order != null) {
            redisTemplate.opsForValue().set("order:" + orderId, order, 3600, TimeUnit.SECONDS);
        }
        // ⚠️ null 不缓存,导致每次都查 DB
    }
    return order;
}

// ✅ 正确:null 也缓存(5 分钟)
public Order queryOrder(Long orderId) {
    String key = "order:" + orderId;
    Order order = (Order) redisTemplate.opsForValue().get(key);
    if (order == null) {
        order = orderMapper.selectById(orderId);
        // 不管有没有都缓存
        redisTemplate.opsForValue().set(key, order == null ? "null" : order, 
            order == null ? 300 : 3600, TimeUnit.SECONDS);
    }
    // 空值返回
    return "null".equals(order) ? null : order;
}
方案 2:布隆过滤器
复制代码
@Component
public class BloomFilterService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    private RBloomFilter<Long> orderBloomFilter;
    
    @PostConstruct
    public void init() {
        orderBloomFilter = redissonClient.getBloomFilter("order:bloom");
        // 预期 1 亿数据,误判率 1%
        orderBloomFilter.tryInit(100_000_000L, 0.01);
        
        // 启动时把数据库所有 ID 加载到布隆过滤器
        List<Long> allOrderIds = orderMapper.selectAllIds();
        for (Long id : allOrderIds) {
            orderBloomFilter.add(id);
        }
    }
    
    public boolean mightContain(Long orderId) {
        return orderBloomFilter.contains(orderId);
    }
}

@Service
public class OrderService {
    
    @Autowired
    private BloomFilterService bloomFilterService;
    
    public Order queryOrder(Long orderId) {
        // 1. 先过布隆过滤器
        if (!bloomFilterService.mightContain(orderId)) {
            return null;  // 一定不存在,直接返回
        }
        
        // 2. 查 Redis
        Order order = (Order) redisTemplate.opsForValue().get("order:" + orderId);
        if (order == null) {
            // 3. 查 MySQL
            order = orderMapper.selectById(orderId);
            if (order != null) {
                redisTemplate.opsForValue().set("order:" + orderId, order, 3600, TimeUnit.SECONDS);
            }
        }
        return order;
    }
}

布隆过滤器原理

  • bitmap 存 hash 值
  • 查询时有 1% 误判率 (说有但实际没有),但绝对不漏报(说没有一定没有)
  • 100w 个不存在 key 的查询,99% 在布隆过滤器就被挡住
方案 3:参数校验 + 限流业务层
复制代码
@PostMapping("/order/query")
public Result<Order> queryOrder(@RequestBody @Valid OrderQueryRequest request) {
    // 1. 参数校验
    if (request.getOrderId() == null || request.getOrderId() < 0) {
        return Result.fail("参数非法");
    }
    
    // 2. 限流(同一 IP 每秒最多 10 次)
    if (!rateLimiter.tryAcquire("queryOrder:" + request.getUserId(), 10)) {
        return Result.fail("请求过快");
    }
    
    // 3. 正常查询
    return Result.ok(orderService.queryOrder(request.getOrderId()));
}
2.3 击穿的 3 种解决方案
方案 1:分布式锁最常用
复制代码
public Order queryOrder(Long orderId) {
    String key = "order:" + orderId;
    Order order = (Order) redisTemplate.opsForValue().get(key);
    if (order == null) {
        // ✅ 加分布式锁,只让 1 个请求查 DB
        String lockKey = "lock:order:" + orderId;
        try (RedisLock lock = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
            // ✅ Double Check:再次查 Redis
            order = (Order) redisTemplate.opsForValue().get(key);
            if (order == null) {
                // 查 DB
                order = orderMapper.selectById(orderId);
                if (order != null) {
                    redisTemplate.opsForValue().set(key, order, 3600, TimeUnit.SECONDS);
                }
            }
        }
    }
    return order;
}

"mpvs 项目用 Redis 分布式锁(SETNX + Lua 脚本) 解决热点 key 击穿,100w 并发查询 1 个热点 key,只让 1 个请求查 DB。"

方案 2:热点 key 永不过期逻辑过期
复制代码
// 缓存数据 + 逻辑过期时间
@Data
public class CacheData<T> {
    private T data;
    private Long expireTime;  // 逻辑过期时间
}

// 写入时只设逻辑过期,不设 Redis 过期
public void setWithLogicalExpire(String key, Object value, long expireSeconds) {
    long expireTime = System.currentTimeMillis() + expireSeconds * 1000;
    CacheData<Object> cacheData = new CacheData<>();
    cacheData.setData(value);
    cacheData.setExpireTime(expireTime);
    
    redisTemplate.opsForValue().set(key, cacheData);  // 不设 Redis 过期
}

// 查询时检查逻辑过期
public Order queryOrder(Long orderId) {
    String key = "order:" + orderId;
    CacheData<Order> cacheData = (CacheData<Order>) redisTemplate.opsForValue().get(key);
    
    if (cacheData == null) {
        // 缓存不存在,查 DB
        Order order = orderMapper.selectById(orderId);
        setWithLogicalExpire(key, order, 3600);
        return order;
    }
    
    if (cacheData.getExpireTime() < System.currentTimeMillis()) {
        // ⚠️ 逻辑过期了,异步刷新
        asyncRefreshCache(orderId, key);
    }
    
    return cacheData.getData();
}

@Async
public void asyncRefreshCache(Long orderId, String key) {
    // 异步查 DB + 刷新缓存
    Order order = orderMapper.selectById(orderId);
    setWithLogicalExpire(key, order, 3600);
}

优点:永远不会有"key 过期瞬间打 DB"的问题

方案 3:预热 + 永不过期
复制代码
// 项目启动时预热热点数据
@PostConstruct
public void preloadHotData() {
    log.info("开始预热热点数据...");
    
    // 查询所有热点 key
    List<Long> hotOrderIds = orderMapper.selectHotOrderIds();
    for (Long orderId : hotOrderIds) {
        Order order = orderMapper.selectById(orderId);
        redisTemplate.opsForValue().set("order:" + orderId, order);  // 永不过期
    }
    
    log.info("预热完成,共 {} 个热点 key", hotOrderIds.size());
}

三、Redis 集群模式(主从 / Sentinel / Cluster

3.1 主从复制(Master-Slave
复制代码
┌─────────┐    异步复制    ┌─────────┐
│ Master  │ ───────────→  │ Slave 1 │  ← 读
│ (读写)  │               └─────────┘
└─────────┘ ───────────→  ┌─────────┐
              异步复制     │ Slave 2 │  ← 读
                         └─────────┘

特点:

  • 1 个 Master + N 个 Slave
  • Master 写,Slave 读
  • 异步复制 (可能丢数据,金融项目慎用),数据量小(100w/天)使用
3.2 Sentinel(哨兵
复制代码
┌─────────┐               ┌──────────┐
│ Master  │ ← 监控 ────   │ Sentinel │ ← 自动故障转移
└─────────┘               │  集群     │
   ↑ 自动切换              └──────────┘
   │                            ↑
┌─────────┐                    │
│ Slave 1 │ ←─── 提升为 Master ─┘
└─────────┘

特点:

  • 在主从基础上加 Sentinel 集群(3-5 个节点)
  • Master 挂了自动选 Slave 升级为新 Master
  • 客户端通过 Sentinel 知道当前 Master
  • **项目常用 Sentinel 模式,**数据量中等(50w 任务)使用
3.3 Cluster(集群
复制代码
┌─────────┐  ┌─────────┐  ┌─────────┐
│ Master  │  │ Master  │  │ Master  │
│ Slot 0  │  │ Slot 1  │  │ Slot 2  │
│ -5460   │  │ -10922  │  │ -16383  │
└─────────┘  └─────────┘  └─────────┘
     ↑             ↑             ↑
┌─────────┐  ┌─────────┐  ┌─────────┐
│ Slave 1 │  │ Slave 1 │  │ Slave 1 │
└─────────┘  └─────────┘  └─────────┘

特点:

  • 数据分片(16384 个 slot)
  • 至少 3 主 3 从
  • 高可用 + 横向扩展
  • 项目常用 Cluster 模式(27 家分行数据分片),数据量大(10 亿+)使用

四、Redis 双写一致性( 4 种方案

4.1 4 种方案对比
方案 一致性 性能 复杂度
先更新 DB,再删除缓存 最终一致
延迟双删 强一致
基于 Binlog 异步同步 最终一致
分布式锁 强一致
4.2 方案 1:Cache Aside 模式最常用
复制代码
// 写操作
public void updateOrder(Order order) {
    // 1. 先更新 DB
    orderMapper.updateById(order);
    
    // 2. 再删除缓存
    redisTemplate.delete("order:" + order.getId());
}

// 读操作
public Order queryOrder(Long orderId) {
    String key = "order:" + orderId;
    Order order = (Order) redisTemplate.opsForValue().get(key);
    if (order == null) {
        order = orderMapper.selectById(orderId);
        if (order != null) {
            redisTemplate.opsForValue().set(key, order, 3600, TimeUnit.SECONDS);
        }
    }
    return order;
}

为什么是"先更新 DB 再删除缓存"?

  • ❌ "先删除缓存再更新 DB":A 删缓存 → B 读缓存(null)→ B 查 DB(旧值)→ B 写缓存(旧值)→ A 写 DB(新值)→ 缓存是旧值
  • ✅ "先更新 DB 再删除缓存":A 写 DB(新值)→ A 删缓存 → B 读缓存(null)→ B 查 DB(新值)→ B 写缓存(新值)→ 缓存最终是新值
4.3 方案 2:延迟双删
复制代码
public void updateOrder(Order order) {
    // 1. 先删除缓存
    redisTemplate.delete("order:" + order.getId());
    
    // 2. 更新 DB
    orderMapper.updateById(order);
    
    // 3. 延迟 500ms 再删除一次(异步)
    CompletableFuture.runAsync(() -> {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        redisTemplate.delete("order:" + order.getId());
    });
}

原理: 删除缓存 → 更新 DB → 延迟 500ms → 再删缓存。避免 B 在 A 更新 DB 期间读到旧 DB 值并写入缓存

4.4 方案 3:基于 Binlog 异步同步
复制代码
@Component
public class BinlogSyncConsumer {
    
    @Autowired
    private CanalClient canalClient;
    
    @PostConstruct
    public void start() {
        canalClient.subscribe("mpvs_order", message -> {
            // 1. 解析 Binlog
            for (CanalEntry entry : message.getEntries()) {
                if (entry.getEntryType() == EntryType.ROWDATA) {
                    RowChange rowChange = entry.getRowChange();
                    for (RowData rowData : rowChange.getRowDatasList()) {
                        // 2. 删除对应缓存
                        Long orderId = Long.parseLong(rowData.getAfterColumns(0).getValue());
                        redisTemplate.delete("order:" + orderId);
                    }
                }
            }
        });
    }
}

原理: 用 Canal 订阅 MySQL Binlog,异步删除缓存最终一致性高、零侵入

4.5 方案 4:分布式锁强一致
复制代码
public void updateOrder(Order order) {
    String lockKey = "lock:order:" + order.getId();
    try (RedisLock lock = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
        // 1. 写 DB
        orderMapper.updateById(order);
        // 2. 写缓存
        redisTemplate.opsForValue().set("order:" + order.getId(), order, 3600, TimeUnit.SECONDS);
    }
}

缺点:性能低 (所有写操作都要加锁)。小流量场景用

五、Redis 大 Key / 热 Key 问题

5.1 大 Key 问题

问题:1 个 key 存了 1G 数据

复制代码
key: user:all
value: [100w 个 user 的 JSON]  ← ⚠️ 1G

危害:

  • 删除 1 个大 key 阻塞 Redis(redis-cli del user:all 会卡 5 秒)
  • 集群模式 slot 迁移卡住
  • 网络带宽打满

解决:

复制代码
// ❌ 错误:1 个 key 存所有
redisTemplate.opsForValue().set("user:all", allUsers);

// ✅ 正确:拆成多个小 key
for (int i = 0; i < 100; i++) {
    List<User> batch = allUsers.subList(i * 10000, (i + 1) * 10000);
    redisTemplate.opsForValue().set("user:batch:" + i, JSON.toJSONString(batch));
}

"把 27 家分行的客户数据按分行编号拆成 27 个小 key避免大 Key 阻塞 Redis。"

5.2 热 Key 问题

问题:1 个 key 被 100w 并发访问

复制代码
key: product:hot:123
并发: 100w QPS
     ↓
单 Redis 节点扛不住
     ↓
⚠️ Redis CPU 100%

解决:

复制代码
// 方案 1:本地缓存 + Redis 二级缓存
@Cacheable(value = "CaffeineCache", key = "#productId")
public Product getProduct(Long productId) {
    return productMapper.selectById(productId);
}

// 方案 2:多副本 key
int replica = productId.hashCode() % 5;  // 5 个副本
redisTemplate.opsForValue().get("product:hot:" + productId + ":replica:" + replica);

// 方案 3:Slot 分散(Cluster 模式下用 hashtag 强制同 slot)
redisTemplate.opsForValue().get("{product:hot}123");  // 同一 slot

六、面试官追问应对

追问:Redis 3 大问题怎么解决?

"雪崩、穿透、击穿对应不同场景:

  • 雪崩 (大量 key 同时过期):过期时间加随机值 + 多级缓存 + 熔断降级
  • 穿透 (查询不存在的 key):空值缓存 + 布隆过滤器 + 参数校验
  • 击穿 (1 个热点 key 过期):分布式锁 + 热点 key 永不过期 + 预热

老哥 mpvs 项目用'布隆过滤器 + 多级缓存 + 分布式锁'3 套组合,挡住了 50w+ 无效查询和 100w+ 热点查询。"

追问 2:Redis 集群模式怎么选?

"3 种集群模式

  • 主从 :1 主 N 从,简单但 Master 挂了要手动切换(金融项目慎用)
  • Sentinel :主从 + Sentinel 集群(3-5 节点),Master 挂了自动切换MOVA 用这个
  • Cluster数据分片(16384 slot)+ 至少 3 主 3 从mpvs 用这个16 主 16 从

数据量 < 50G 用 Sentinel,> 50G 用 Cluster。"

追问 3:Redis 和 MySQL 双写一致性怎么保证?

"4 种方案

  • Cache Aside (最常用):先更新 DB,再删除缓存(最终一致)
  • 延迟双删:先删缓存 → 更新 DB → 延迟 500ms → 再删缓存(避免并发读旧值)
  • Binlog 异步同步 :用 Canal 订阅 MySQL Binlog,异步删除缓存mpvs 用这个
  • 分布式锁 :写 DB + 写缓存都加锁(强一致,性能低
追问 4:Redis 雪崩怎么发生的?怎么防止?

"发生原因:大量 key 同一时间过期,请求瞬间打 DB。

项目实战

1.过期时间加随机值(0-300 秒)--- 50w 个 key 不会同时过期

2.Caffeine + Redis 多级缓存 --- L1 缓存 1 秒过期,扛住 80% 请求

3.Sentinel 熔断降级 --- Redis 挂了直接返回 MySQL,不报错

效果 :50w+ key 同时过期场景下,QPS 只增加 200%(10w→30w)。"

追问 5:布隆过滤器原理?

"布隆过滤器用 bitmap + 多个 hash 函数

1.插入 :对 key 算 k 个 hash 值,bitmap 对应位置设为 1

2.查询 :算 hash 值,任何一位是 0 → 一定不存在全是 1 → 可能存在(有 1% 误判)

优点100w 个不存在 key 查询,99% 在布隆过滤器挡住不查 DB

**项目用 Redisson 的 RBloomFilter,**预加载 10 亿订单 ID 到布隆过滤器,误判率 1%。"

追问 6:Redis 大 Key 怎么发现?怎么解决?

"发现 :用 redis-cli --bigkeys 扫描,memory usage 命令看 key 大小。

项目大 Key 处理

1.拆分:27 家分行的客户数据拆成 27 个 key(每个 100MB)

2.异步删除 :用 unlink 替代 del不阻塞 Redis

3.压缩 :用 MessagePack / Protobuf 替代 JSON(压缩 3 倍

效果 :原来 1 个 1G 大 key → 拆成 10 个 100M 小 key,删除时间从 5s 降到 500ms。"

追问 7:Redis 主从复制原理?

"全量复制 + 增量复制

全量复制(Slave 第一次连 Master):

1.Slave 发送 PSYNC 命令

2.Master 执行 BGSAVE 生成 RDB

3.Master 把 RDB 发送给 Slave

4.Slave 加载 RDB

5.同步过程中 Master 写的命令,缓存在 replication buffer

增量复制(Slave 重连 Master):

1.Master 维护 repl_backlog 缓冲区

2.Slave 重连时发送 PSYNC offset

3.Master 从 offset 位置开始发送增量命令

Redis Sentinel + 异步复制(金融项目允许秒级数据丢失)。"

七、记忆口诀

"雪崩:随机值 + 多级缓存 + 熔断"

"穿透:空值缓存 + 布隆过滤器 + 参数校验"

"击穿:分布式锁 + 永不过期 + 预热"

"双写一致:Cache Aside + 延迟双删 + Binlog 同步"

"集群:50G 以下 Sentinel,50G 以上 Cluster"

"大 Key:拆 + 压 + 异步删"

"热 Key:本地缓存 + 多副本 + slot 分散"

相关推荐
流星白龙4 小时前
【Redis】2.Redis重大版本
数据库·redis·junit
流星白龙4 小时前
【Redis】7.Hash表
数据库·redis·哈希算法
流星白龙5 小时前
【Redis】4.基本全局命令
数据库·redis·缓存
流星白龙8 小时前
【Redis】3.Redis安装与命令行客户端
数据库·redis·缓存
Season45013 小时前
Redis命令 (generic即通用命令)
数据库·redis·bootstrap
我不想名字重复14 小时前
redis缓存和数据库数据保持一致
数据库·redis·缓存
流星白龙17 小时前
【Redis】6.计数其他命令
数据库·redis·缓存
流星白龙17 小时前
【Redis】8.List列表
redis
NWU_白杨17 小时前
三种常用的数据存储技术
数据库·redis·mysql·sqlite
我叫黑大帅17 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试