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 分散"

相关推荐
AOwhisky3 小时前
Redis 学习笔记(第一期):概述、安装配置与核心理论
运维·数据库·redis·笔记·学习·云计算
AOwhisky3 小时前
Redis 学习笔记(第四期):高可用与集群(哨兵 + Cluster + 容器化)
linux·运维·数据库·redis·笔记·学习·缓存
IT策士6 小时前
Redis 从入门到精通:事务与 Lua 脚本
redis·junit·lua
胡小禾10 小时前
Redis哨兵模式下主从同步的偏差
数据库·redis·缓存
zzqssliu10 小时前
Taocarts接口限流实操:基于Redis实现API防刷与流量管控
数据库·redis·缓存
啦啦啦啦啦zzzz10 小时前
redis的持久化操作和主从复制与集群的关系及其应用
数据库·redis
IT策士11 小时前
Redis 从入门到精通:分片之道 —— Redis Cluster
数据库·redis·缓存
AOwhisky11 小时前
学习自测与解析:Redis系列第一期与第二期核心知识点详解
运维·数据库·redis·学习·云计算
Java爱好狂.12 小时前
阿里1658页2026最新Java面试题总结(含答案)
数据库·redis·程序员·java面试·java面试题·java编程·java八股文