news 2026/6/12 15:22:58

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 分散"

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/12 15:22:57

分布式锁 5 种实现

为什么需要分布式锁&#xff1f;单机应用&#xff1a;synchronized / ReentrantLock ← JVM 内锁 分布式应用&#xff1a;多 JVM 实例&#xff0c;synchronized 不够用&#xff01;← 需要分布式锁MySQL 分布式锁&#xff08;最朴素&#xff09;-- 用唯一索引实现分布式锁 CRE…

作者头像 李华
网站建设 2026/6/12 15:22:36

为什么全球设备商都选 Metrix 国际物联网卡?

在智能硬件全球化量产出海的行业趋势下&#xff0c;设备厂商的核心竞争优势&#xff0c;早已不局限于终端产品本身的性能与品质&#xff0c;更取决于全球化部署效率、跨区域运维稳定性、长期合规风控能力与综合运营成本控制。多数出海设备故障、项目延期、售后纠纷&#xff0c;…

作者头像 李华
网站建设 2026/6/12 15:15:53

深入解析MCF5301x:高度集成SoC在VoIP与POS系统中的核心架构与实战设计

1. 项目概述与核心价值在嵌入式系统开发领域&#xff0c;选对一颗“心脏”——微处理器&#xff0c;往往决定了整个项目的成败。尤其是在那些对实时性、安全性和集成度要求都极高的应用场景里&#xff0c;比如我们日常接触的智能POS机、企业级IP电话或者网络语音网关&#xff0…

作者头像 李华
网站建设 2026/6/12 15:06:51

终极抢票神器DamaiHelper:10分钟轻松搞定演唱会门票

终极抢票神器DamaiHelper&#xff1a;10分钟轻松搞定演唱会门票 【免费下载链接】damaihelper 支持大麦网&#xff0c;淘票票、缤玩岛等多个平台&#xff0c;演唱会演出抢票脚本 项目地址: https://gitcode.com/gh_mirrors/dam/damaihelper 还在为抢不到心仪的演唱会门票…

作者头像 李华