一、缓存分层体系(标准三层架构,高并发必备)

1. L1 本地进程缓存(Caffeine,替代Guava Cache)

核心定位

离应用最近,无网络IO,单机百万QPS,拦截90%常规读请求,大幅减轻Redis压力。

适用数据

  • 极少变更基础静态数据:商品基础信息、字典、配置、标签
  • 短期热点数据,允许1~3s数据不一致

Caffeine核心优势

  1. 基于Window TinyLFU淘汰算法,比LRU命中率高30%
  2. 支持同步/异步加载、自动过期、写入刷新、最大容量限制
  3. 无锁设计,并发读写性能远超Guava

基础配置示例

// 初始化本地缓存:最大10万条,写入后5秒过期
LoadingCache<Long, GoodsDTO> localCache = Caffeine.newBuilder()
        .maximumSize(100000)
        .expireAfterWrite(5, TimeUnit.SECONDS)
        .recordStats() // 统计命中率、失效、加载耗时
        .build(id -> loadFromRedis(id));

短板

单机隔离,多实例数据不一致;重启丢失;不适合强一致场景;内存占用可控。

2. L2 分布式缓存(Redis Cluster,核心存储层)

定位

跨实例共享数据,支撑集群全局缓存,承载所有热点查询,统一数据视图。

适用

所有需要多服务共享的数据:库存、用户会话、订单快照、活动数据、排行榜

集群架构选型

  1. Redis Cluster(分片集群,无中心,水平扩容,生产主流)
  2. 主从+哨兵(读多写少、数据量小场景,扩容受限)

读写分离优化

主节点负责写入,从节点分担读请求,降低主节点CPU/网卡负载。

3. L3 数据库(最终兜底)

仅缓存全部失效、穿透、熔断降级时访问,正常流量下DB请求量极低。

完整读取流程

客户端请求 → 查L1本地缓存
命中 → 直接返回
未命中 → 查询L2 Redis
Redis命中 → 回填本地缓存,返回数据
Redis未命中 → 查询DB → 回填Redis+本地缓存 → 返回

二、缓存三大致命问题:根因+全套解决方案

2.1 缓存穿透:查询不存在的数据,请求直达DB

根因

  1. 恶意请求传入非法ID(负数、超大ID、随机乱ID)
  2. 业务空数据未缓存,每次查询都访问数据库
  3. 爬虫批量遍历不存在主键,压垮DB

解决方案(按推荐优先级排序)

方案1:布隆过滤器(最优,拦截无效ID)

原理:预加载所有有效业务主键到过滤器,查询前先判断;过滤器判定不存在直接返回空,不访问Redis/DB。

  • 实现:RedissonBloomFilter、Google Guava布隆过滤器
  • 生产落地:
    1. 项目启动/定时任务全量同步有效商品ID、用户ID到过滤器
    2. 新增数据同步写入布隆过滤器
    3. 过滤器存在误判率(可控,调整容量降低误判)
  • 缺点:数据删除困难,适合只增不减数据;重启需重新加载。
方案2:缓存空值/占位符

查询DB无数据时,存入Redis特殊标识(如""NULL_PLACEHOLDER),设置短过期时间(3~10s),短期内拦截重复穿透。

// DB无数据处理
if(goods == null){
    redisTemplate.opsForValue().set("goods:10086", "NULL", 5, TimeUnit.SECONDS);
    return null;
}

缺点:恶意随机ID会产生大量无效key,占用Redis内存。

方案3:参数校验+网关限流

网关层拦截非法参数:ID小于0、超长字符串、特殊符号;对单IP高频空查询进行限流。

2.2 缓存击穿:热点Key过期瞬间,并发流量全部打DB

根因

爆款商品、首页活动等百万QPS热点Key统一过期,过期瞬间大量请求绕过缓存访问数据库。

4套落地方案

方案1:逻辑永不过期(首选,无锁无等待)

  • 不设置Redis过期时间,key永久存在
  • value中增加expireTime逻辑过期字段
  • 请求时先判断逻辑时间,未过期直接返回;过期则异步线程刷新缓存,当前请求直接返回旧数据
    优势:无锁、无阻塞、吞吐量最高;适合允许短暂旧数据的读多写少场景。
// Redis存储结构示例
{
  "data": {商品详情},
  "logicExpire": 1789000000
}

方案2:互斥锁(强实时场景,保证数据最新)

热点key过期后,仅允许1个线程查询DB更新缓存,其余线程等待重试。
使用Redisson分布式锁实现,避免大量并发击穿DB。
伪流程:

  1. 查询Redis,发现逻辑过期
  2. 尝试获取刷新锁
  3. 获取成功:查DB更新缓存,释放锁
  4. 获取失败:短暂sleep后重试读取缓存

方案3:定时主动预热刷新

后台定时任务(如每30分钟)主动刷新全量热点Key,从根源避免key过期真空期。
适合活动商品、首页固定资源。

方案4:过期时间随机偏移

基础过期时间 + 随机浮动值(如基础30min,随机±300s),避免大量key同一时刻失效,仅作为辅助方案,无法解决单热点key击穿。

2.3 缓存雪崩:大批量key同时过期,DB瞬间流量洪峰

根因

  1. 批量缓存设置相同过期时间,零点/整点大批量失效
  2. Redis集群宕机、主从切换、节点故障,整体缓存不可用

分两类问题分开解决

第一类:批量key同时过期雪崩

  1. 过期时间随机偏移:expire = 30*60 + new Random().nextInt(300)
  2. 分层过期:基础配置12h,商品数据30min,活动数据10min,错开生命周期
  3. 逻辑永不过期策略替代物理过期

第二类:Redis整体宕机雪崩(高可用兜底)

  1. Redis Cluster集群+主从复制,每个slot至少一主一从,故障自动切换
  2. 多级本地缓存L1兜底:Redis挂掉后,流量全部走本地缓存,保护DB
  3. 熔断降级:Sentinel监控Redis异常,触发降级,返回静态兜底数据
  4. 限流:网关/应用层限制DB最大并发查询数,防止压垮数据库

三、缓存更新一致性策略(读写并发脏数据根治)

生产环境99%业务使用 Cache Aside 旁路缓存,分为读流程、写流程。

3.1 标准读流程

  1. 查询缓存,命中直接返回
  2. 未命中 → 查询DB
  3. 将DB数据写入Redis缓存,返回结果

3.2 写流程3种方案对比

方案1:先更新DB,再删除缓存(基础版,大部分场景够用)

流程:更新数据库 → 删除对应Redis缓存
问题:极低概率读写并发脏数据
场景:线程A更新DB,未删缓存;线程B此时读缓存拿到旧数据,写入缓存,导致缓存长期脏数据。

方案2:延迟双删(工业级,解决并发脏读,推荐)

完整流程:

  1. 更新数据库
  2. 立即删除Redis缓存
  3. 延迟1~3s再次删除缓存(通过线程池/MQ异步执行)
    原理:延迟删除覆盖并发读写入旧缓存的窗口,彻底解决脏数据。
    延迟时间设置:大于业务单次查询DB+回填缓存最大耗时。

方案3:先删缓存,再更新DB(不推荐)

并发场景极易出现脏数据,禁止使用。

3.3 强一致性场景补充

金融、订单账务不建议依赖缓存做数据计算;缓存仅作查询加速,核心计算逻辑走DB事务。
若要求缓存与DB实时一致,使用Canal监听数据库binlog,自动同步更新缓存,解耦业务代码。

其他缓存模式(极少使用)

  1. Read/Write Through:读写全部经过缓存,缓存同步写DB,代码侵入高,性能差
  2. Write Back 写回:缓存异步刷DB,数据丢失风险高,仅大数据离线场景使用

四、热点Key & 大Key深度优化(超高并发核心瓶颈)

4.1 热点Key问题

单key承受数万~百万QPS,绑定Redis单一节点,CPU/网卡打满,集群其他节点空闲。

优化方案

  1. 缓存副本分片(最优)
    同一个热点商品拆分多份缓存key:goods:100_0goods:100_1goods:100_2
    查询时随机选取其中一个key读取,分摊流量到不同slot节点。
    更新时同步删除所有副本key。

  2. L1本地缓存拦截
    热点数据存入Caffeine本地缓存,90%流量不经过Redis,从源头减少Redis压力。

  3. Redis集群热点slot迁移
    手动迁移热点slot到多台物理机器,分散节点压力。

4.2 大Key优化(value超过10KB即为大key)

危害:

  • 网络传输耗时高,单次rt暴涨
  • 删除大key阻塞Redis主线程,造成服务卡顿
  • 集群迁移slot超时失败

拆分方案

  1. 对象Hash拆分:商品详情大JSON拆分为hash结构,hget单个字段,不用一次性读取全部数据
hset goods:100 name "手机" price 2999 stock 1000
# 按需查询,避免全量拉取
hget goods:100 price
  1. 列表分页拆分:长list拆分多个短list,分页存储
  2. 压缩序列化:JSON使用Snappy压缩后存入Redis,降低体积
  3. 异步分片删除:不直接del大key,使用hdel/lpop分批删除,不阻塞主线程

五、Redis精细化性能调优

5.1 网络层优化

  1. 使用Pipeline批量操作,合并多次IO往返(mget、hmget批量查询)
  2. 禁用频繁单条get/set循环,批量接口统一聚合
  3. 客户端连接池合理配置,避免频繁创建销毁连接
  4. 客户端使用长连接,减少TCP握手开销

5.2 内存调优

  1. 内存淘汰策略:生产固定 allkeys-lru
maxmemory-policy allkeys-lru
  1. 关闭持久化RDB/AOF不必要刷盘:读多写少场景降低刷盘频率
  2. 控制key过期删除策略:惰性删除+定期主动删除结合,避免集中清理

5.3 数据结构选型优化(极大节省内存、提升速度)

业务场景 推荐结构 禁用
对象存储 Hash String存完整JSON大key
排行榜、积分 ZSet 多次sort排序
计数器、库存 String原子incr/decr Lua复杂脚本循环计算
去重统计 HyperLogLog Set存海量ID
短列表分页 List 超大List不分片
布尔标签 BitMap 单个String存储

5.4 Lua脚本规范

  1. 热点库存扣减、限流逻辑使用Lua原子执行,减少多次网络交互
  2. Lua脚本避免循环、复杂计算,缩短执行时间,防止阻塞Redis主线程

六、多级缓存协同完整流程(本地Caffeine + Redis + 延迟双删)

读流程

  1. 根据商品id查询Caffeine本地缓存
  2. 命中 → 直接返回,结束
  3. 未命中 → 查询Redis
  4. Redis命中:回填Caffeine,返回数据
  5. Redis未命中:查询DB,写入Redis,回填本地缓存,返回

更新写流程(延迟双删,保证一致性)

  1. 开启事务更新MySQL数据库,提交事务
  2. 同步删除Redis缓存key
  3. 同步删除本机Caffeine缓存
  4. 异步线程延迟2s,再次删除Redis缓存(覆盖并发脏读)
  5. 通知集群其他实例刷新本地缓存(可选,MQ广播失效通知)

优势

  • 本地缓存拦截绝大多数读,Redis QPS大幅下降
  • 延迟双删解决读写并发脏数据
  • 本地缓存可作为Redis宕机降级兜底

七、缓存预热、过期、淘汰策略设计

7.1 缓存预热(上线/大促前必备)

场景:系统重启、活动上线、零点大促,缓存为空,瞬间流量全部击穿DB
预热方案:

  1. 项目启动预热:@PostConstruct全量加载基础字典、热门商品到本地+Redis
  2. 定时任务预热:凌晨低峰同步更新全量热点数据
  3. 大促手动预热接口:运营后台触发批量写入热点商品缓存
  4. 流量分层预热:灰度放量,逐步填充缓存,避免瞬时洪峰

7.2 过期时间设计规范

  1. 静态配置、字典:逻辑永不过期,定时任务刷新
  2. 商品基础信息:10~30min + 随机偏移
  3. 临时活动数据:5~10min
  4. 空值缓存:3~5s(极短,避免占用内存)
  5. 用户会话:30min,支持续期

7.3 缓存淘汰策略

  1. L1本地Caffeine:基于容量淘汰,Window TinyLFU,设置最大容量限制,防止OOM
  2. L2 Redis:allkeys-lru,内存达到阈值自动淘汰最少访问key

八、缓存降级、限流、熔断防护体系

1. 熔断(Sentinel)

监控Redis调用异常比例、响应超时,触发熔断后不再访问Redis,直接走降级逻辑:

  • 降级方案1:读取本地缓存旧数据
  • 降级方案2:返回静态默认兜底页面/空数据
  • 降级方案3:直接提示“活动火爆,请稍后重试”

2. 分布式限流(Redis Lua滑动窗口)

针对穿透、恶意爬虫,限制单IP/单用户每秒查询缓存次数,超限直接拦截,不访问缓存与DB。

3. 本地限流(RateLimiter)

单机控制缓存最大并发请求数,防止单机流量打爆Redis。

九、分业务场景缓存落地模板

场景1:商品详情(读极高,写少)

  1. 分层:Caffeine本地缓存 + Redis逻辑永不过期热点副本
  2. 防击穿:逻辑过期,异步刷新
  3. 更新:延迟双删,定时预热
  4. 防穿透:布隆过滤器拦截无效商品ID

场景2:秒杀库存(读写双高,热点极强)

  1. 热点拆分:库存key多副本分片
  2. 缓存仅做库存预减校验,真实扣减落DB
  3. MQ异步下单削峰,减少缓存并发更新
  4. 本地缓存不存储真实库存,避免多实例不一致

场景3:系统配置字典(几乎不更新)

  1. 仅本地Caffeine缓存,Redis做全局同步存储
  2. 逻辑永久有效,后台修改后广播清除所有实例本地缓存

场景4:用户订单快照(读多,按月分冷热)

  1. 近3个月热单存入Redis Hash;历史冷单不缓存,直接查DB
  2. 分页查询,禁止一次性加载完整订单列表大key

更多推荐