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 请求瞬间打 MySQLDB 连接池爆了1.2 穿透Penetration问题查询一个不存在的 key每次都打到数据库恶意攻击 / 业务 bug ↓ 查询 user_id -1不存在 ↓ Redis 没这个 key → 查 MySQL ↓ MySQL 也没这个 user → 返回 null ↓ ⚠️ 但每次都查 MySQL**没有缓存保护** ↓ ⚠️ 攻击者用 100w 个不存在的 user_id 查 ↓ MySQL 被 100w 次无效查询打爆项目场景27 家分行的客户敏感数据查询被恶意传 100w 个不存在的身份证号外部数据采集外部传 100w 个不存在的客户号100w 次无效查询打爆 MySQL1.3 击穿Breakdown问题1 个热点 key 过期瞬间大量请求打到数据库双 11 大促 / 春晚红包 / 明星离婚 ↓ 某个热点商品的缓存 key 过期 ↓ ⚠️ 100w 个用户同时查这个商品 ↓ 100w 个请求同时打 MySQL ↓ MySQL 扛不住系统雪崩项目场景春节红包雨某个热门红包的库存 key 过期央行降息某个热门理财产品的详情 key 过期100w 用户同时查 1 个 keyDB 被打爆二、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 / Resilience4jSentinelResource(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 }方案 4Redis 集群 高可用根本上解决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 RBloomFilterLong orderBloomFilter; PostConstruct public void init() { orderBloomFilter redissonClient.getBloomFilter(order:bloom); // 预期 1 亿数据误判率 1% orderBloomFilter.tryInit(100_000_000L, 0.01); // 启动时把数据库所有 ID 加载到布隆过滤器 ListLong 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 ResultOrder 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 CacheDataT { private T data; private Long expireTime; // 逻辑过期时间 } // 写入时只设逻辑过期不设 Redis 过期 public void setWithLogicalExpire(String key, Object value, long expireSeconds) { long expireTime System.currentTimeMillis() expireSeconds * 1000; CacheDataObject cacheData new CacheData(); cacheData.setData(value); cacheData.setExpireTime(expireTime); redisTemplate.opsForValue().set(key, cacheData); // 不设 Redis 过期 } // 查询时检查逻辑过期 public Order queryOrder(Long orderId) { String key order: orderId; CacheDataOrder cacheData (CacheDataOrder) 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 ListLong 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 / Cluster3.1 主从复制Master-Slave┌─────────┐ 异步复制 ┌─────────┐ │ Master │ ───────────→ │ Slave 1 │ ← 读 │ (读写) │ └─────────┘ └─────────┘ ───────────→ ┌─────────┐ 异步复制 │ Slave 2 │ ← 读 └─────────┘特点1 个 Master N 个 SlaveMaster 写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 方案 1Cache 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 再删除缓存❌ 先删除缓存再更新 DBA 删缓存 → 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 阻塞 Redisredis-cli del user:all会卡 5 秒集群模式 slot 迁移卡住网络带宽打满解决// ❌ 错误1 个 key 存所有 redisTemplate.opsForValue().set(user:all, allUsers); // ✅ 正确拆成多个小 key for (int i 0; i 100; i) { ListUser 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); // 方案 3Slot 分散Cluster 模式下用 hashtag 强制同 slot redisTemplate.opsForValue().get({product:hot}123); // 同一 slot六、面试官追问应对追问Redis 3 大问题怎么解决雪崩、穿透、击穿对应不同场景雪崩大量 key 同时过期过期时间加随机值 多级缓存 熔断降级穿透查询不存在的 key空值缓存 布隆过滤器 参数校验击穿1 个热点 key 过期分布式锁 热点 key 永不过期 预热老哥 mpvs 项目用布隆过滤器 多级缓存 分布式锁3 套组合挡住了 50w 无效查询和 100w 热点查询。追问 2Redis 集群模式怎么选3 种集群模式主从1 主 N 从简单但 Master 挂了要手动切换金融项目慎用Sentinel主从 Sentinel 集群3-5 节点Master 挂了自动切换MOVA 用这个Cluster数据分片16384 slot 至少 3 主 3 从mpvs 用这个16 主 16 从数据量 50G 用 Sentinel 50G 用 Cluster。追问 3Redis 和 MySQL 双写一致性怎么保证4 种方案Cache Aside最常用先更新 DB再删除缓存最终一致延迟双删先删缓存 → 更新 DB → 延迟 500ms → 再删缓存避免并发读旧值Binlog 异步同步用 Canal 订阅 MySQL Binlog异步删除缓存mpvs 用这个分布式锁写 DB 写缓存都加锁强一致性能低追问 4Redis 雪崩怎么发生的怎么防止发生原因大量 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 对应位置设为 12.查询算 hash 值任何一位是 0 → 一定不存在全是 1 → 可能存在有 1% 误判优点100w 个不存在 key 查询99% 在布隆过滤器挡住不查 DB。**项目用 Redisson 的 RBloomFilter**预加载 10 亿订单 ID 到布隆过滤器误判率 1%。追问 6Redis 大 Key 怎么发现怎么解决发现用redis-cli --bigkeys扫描memory usage命令看 key 大小。项目大 Key 处理1.拆分27 家分行的客户数据拆成 27 个 key每个 100MB2.异步删除用unlink替代del不阻塞 Redis3.压缩用MessagePack / Protobuf替代 JSON压缩 3 倍效果原来 1 个 1G 大 key → 拆成 10 个 100M 小 key删除时间从 5s 降到 500ms。追问 7Redis 主从复制原理全量复制 增量复制全量复制Slave 第一次连 Master1.Slave 发送PSYNC命令2.Master 执行BGSAVE生成 RDB3.Master 把 RDB 发送给 Slave4.Slave 加载 RDB5.同步过程中 Master 写的命令缓存在 replication buffer增量复制Slave 重连 Master1.Master 维护repl_backlog 缓冲区2.Slave 重连时发送PSYNC offset3.Master 从 offset 位置开始发送增量命令用Redis Sentinel 异步复制金融项目允许秒级数据丢失。七、记忆口诀雪崩随机值 多级缓存 熔断穿透空值缓存 布隆过滤器 参数校验击穿分布式锁 永不过期 预热双写一致Cache Aside 延迟双删 Binlog 同步集群50G 以下 Sentinel50G 以上 Cluster大 Key拆 压 异步删热 Key本地缓存 多副本 slot 分散