高并发数据访问场景下的并发控制与缓存优化(缓存优化)
一、缓存分层体系(标准三层架构,高并发必备)
1. L1 本地进程缓存(Caffeine,替代Guava Cache)
核心定位
离应用最近,无网络IO,单机百万QPS,拦截90%常规读请求,大幅减轻Redis压力。
适用数据
- 极少变更基础静态数据:商品基础信息、字典、配置、标签
- 短期热点数据,允许1~3s数据不一致
Caffeine核心优势
- 基于Window TinyLFU淘汰算法,比LRU命中率高30%
- 支持同步/异步加载、自动过期、写入刷新、最大容量限制
- 无锁设计,并发读写性能远超Guava
基础配置示例
// 初始化本地缓存:最大10万条,写入后5秒过期
LoadingCache<Long, GoodsDTO> localCache = Caffeine.newBuilder()
.maximumSize(100000)
.expireAfterWrite(5, TimeUnit.SECONDS)
.recordStats() // 统计命中率、失效、加载耗时
.build(id -> loadFromRedis(id));
短板
单机隔离,多实例数据不一致;重启丢失;不适合强一致场景;内存占用可控。
2. L2 分布式缓存(Redis Cluster,核心存储层)
定位
跨实例共享数据,支撑集群全局缓存,承载所有热点查询,统一数据视图。
适用
所有需要多服务共享的数据:库存、用户会话、订单快照、活动数据、排行榜
集群架构选型
- Redis Cluster(分片集群,无中心,水平扩容,生产主流)
- 主从+哨兵(读多写少、数据量小场景,扩容受限)
读写分离优化
主节点负责写入,从节点分担读请求,降低主节点CPU/网卡负载。
3. L3 数据库(最终兜底)
仅缓存全部失效、穿透、熔断降级时访问,正常流量下DB请求量极低。
完整读取流程
客户端请求 → 查L1本地缓存
命中 → 直接返回
未命中 → 查询L2 Redis
Redis命中 → 回填本地缓存,返回数据
Redis未命中 → 查询DB → 回填Redis+本地缓存 → 返回
二、缓存三大致命问题:根因+全套解决方案
2.1 缓存穿透:查询不存在的数据,请求直达DB
根因
- 恶意请求传入非法ID(负数、超大ID、随机乱ID)
- 业务空数据未缓存,每次查询都访问数据库
- 爬虫批量遍历不存在主键,压垮DB
解决方案(按推荐优先级排序)
方案1:布隆过滤器(最优,拦截无效ID)
原理:预加载所有有效业务主键到过滤器,查询前先判断;过滤器判定不存在直接返回空,不访问Redis/DB。
- 实现:RedissonBloomFilter、Google Guava布隆过滤器
- 生产落地:
- 项目启动/定时任务全量同步有效商品ID、用户ID到过滤器
- 新增数据同步写入布隆过滤器
- 过滤器存在误判率(可控,调整容量降低误判)
- 缺点:数据删除困难,适合只增不减数据;重启需重新加载。
方案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。
伪流程:
- 查询Redis,发现逻辑过期
- 尝试获取刷新锁
- 获取成功:查DB更新缓存,释放锁
- 获取失败:短暂sleep后重试读取缓存
方案3:定时主动预热刷新
后台定时任务(如每30分钟)主动刷新全量热点Key,从根源避免key过期真空期。
适合活动商品、首页固定资源。
方案4:过期时间随机偏移
基础过期时间 + 随机浮动值(如基础30min,随机±300s),避免大量key同一时刻失效,仅作为辅助方案,无法解决单热点key击穿。
2.3 缓存雪崩:大批量key同时过期,DB瞬间流量洪峰
根因
- 批量缓存设置相同过期时间,零点/整点大批量失效
- Redis集群宕机、主从切换、节点故障,整体缓存不可用
分两类问题分开解决
第一类:批量key同时过期雪崩
- 过期时间随机偏移:
expire = 30*60 + new Random().nextInt(300) - 分层过期:基础配置12h,商品数据30min,活动数据10min,错开生命周期
- 逻辑永不过期策略替代物理过期
第二类:Redis整体宕机雪崩(高可用兜底)
- Redis Cluster集群+主从复制,每个slot至少一主一从,故障自动切换
- 多级本地缓存L1兜底:Redis挂掉后,流量全部走本地缓存,保护DB
- 熔断降级:Sentinel监控Redis异常,触发降级,返回静态兜底数据
- 限流:网关/应用层限制DB最大并发查询数,防止压垮数据库
三、缓存更新一致性策略(读写并发脏数据根治)
生产环境99%业务使用 Cache Aside 旁路缓存,分为读流程、写流程。
3.1 标准读流程
- 查询缓存,命中直接返回
- 未命中 → 查询DB
- 将DB数据写入Redis缓存,返回结果
3.2 写流程3种方案对比
方案1:先更新DB,再删除缓存(基础版,大部分场景够用)
流程:更新数据库 → 删除对应Redis缓存
问题:极低概率读写并发脏数据
场景:线程A更新DB,未删缓存;线程B此时读缓存拿到旧数据,写入缓存,导致缓存长期脏数据。
方案2:延迟双删(工业级,解决并发脏读,推荐)
完整流程:
- 更新数据库
- 立即删除Redis缓存
- 延迟1~3s再次删除缓存(通过线程池/MQ异步执行)
原理:延迟删除覆盖并发读写入旧缓存的窗口,彻底解决脏数据。
延迟时间设置:大于业务单次查询DB+回填缓存最大耗时。
方案3:先删缓存,再更新DB(不推荐)
并发场景极易出现脏数据,禁止使用。
3.3 强一致性场景补充
金融、订单账务不建议依赖缓存做数据计算;缓存仅作查询加速,核心计算逻辑走DB事务。
若要求缓存与DB实时一致,使用Canal监听数据库binlog,自动同步更新缓存,解耦业务代码。
其他缓存模式(极少使用)
- Read/Write Through:读写全部经过缓存,缓存同步写DB,代码侵入高,性能差
- Write Back 写回:缓存异步刷DB,数据丢失风险高,仅大数据离线场景使用
四、热点Key & 大Key深度优化(超高并发核心瓶颈)
4.1 热点Key问题
单key承受数万~百万QPS,绑定Redis单一节点,CPU/网卡打满,集群其他节点空闲。
优化方案
-
缓存副本分片(最优)
同一个热点商品拆分多份缓存key:goods:100_0、goods:100_1、goods:100_2
查询时随机选取其中一个key读取,分摊流量到不同slot节点。
更新时同步删除所有副本key。 -
L1本地缓存拦截
热点数据存入Caffeine本地缓存,90%流量不经过Redis,从源头减少Redis压力。 -
Redis集群热点slot迁移
手动迁移热点slot到多台物理机器,分散节点压力。
4.2 大Key优化(value超过10KB即为大key)
危害:
- 网络传输耗时高,单次rt暴涨
- 删除大key阻塞Redis主线程,造成服务卡顿
- 集群迁移slot超时失败
拆分方案
- 对象Hash拆分:商品详情大JSON拆分为hash结构,hget单个字段,不用一次性读取全部数据
hset goods:100 name "手机" price 2999 stock 1000
# 按需查询,避免全量拉取
hget goods:100 price
- 列表分页拆分:长list拆分多个短list,分页存储
- 压缩序列化:JSON使用Snappy压缩后存入Redis,降低体积
- 异步分片删除:不直接del大key,使用hdel/lpop分批删除,不阻塞主线程
五、Redis精细化性能调优
5.1 网络层优化
- 使用Pipeline批量操作,合并多次IO往返(mget、hmget批量查询)
- 禁用频繁单条get/set循环,批量接口统一聚合
- 客户端连接池合理配置,避免频繁创建销毁连接
- 客户端使用长连接,减少TCP握手开销
5.2 内存调优
- 内存淘汰策略:生产固定
allkeys-lru
maxmemory-policy allkeys-lru
- 关闭持久化RDB/AOF不必要刷盘:读多写少场景降低刷盘频率
- 控制key过期删除策略:惰性删除+定期主动删除结合,避免集中清理
5.3 数据结构选型优化(极大节省内存、提升速度)
| 业务场景 | 推荐结构 | 禁用 |
|---|---|---|
| 对象存储 | Hash | String存完整JSON大key |
| 排行榜、积分 | ZSet | 多次sort排序 |
| 计数器、库存 | String原子incr/decr | Lua复杂脚本循环计算 |
| 去重统计 | HyperLogLog | Set存海量ID |
| 短列表分页 | List | 超大List不分片 |
| 布尔标签 | BitMap | 单个String存储 |
5.4 Lua脚本规范
- 热点库存扣减、限流逻辑使用Lua原子执行,减少多次网络交互
- Lua脚本避免循环、复杂计算,缩短执行时间,防止阻塞Redis主线程
六、多级缓存协同完整流程(本地Caffeine + Redis + 延迟双删)
读流程
- 根据商品id查询Caffeine本地缓存
- 命中 → 直接返回,结束
- 未命中 → 查询Redis
- Redis命中:回填Caffeine,返回数据
- Redis未命中:查询DB,写入Redis,回填本地缓存,返回
更新写流程(延迟双删,保证一致性)
- 开启事务更新MySQL数据库,提交事务
- 同步删除Redis缓存key
- 同步删除本机Caffeine缓存
- 异步线程延迟2s,再次删除Redis缓存(覆盖并发脏读)
- 通知集群其他实例刷新本地缓存(可选,MQ广播失效通知)
优势
- 本地缓存拦截绝大多数读,Redis QPS大幅下降
- 延迟双删解决读写并发脏数据
- 本地缓存可作为Redis宕机降级兜底
七、缓存预热、过期、淘汰策略设计
7.1 缓存预热(上线/大促前必备)
场景:系统重启、活动上线、零点大促,缓存为空,瞬间流量全部击穿DB
预热方案:
- 项目启动预热:@PostConstruct全量加载基础字典、热门商品到本地+Redis
- 定时任务预热:凌晨低峰同步更新全量热点数据
- 大促手动预热接口:运营后台触发批量写入热点商品缓存
- 流量分层预热:灰度放量,逐步填充缓存,避免瞬时洪峰
7.2 过期时间设计规范
- 静态配置、字典:逻辑永不过期,定时任务刷新
- 商品基础信息:10~30min + 随机偏移
- 临时活动数据:5~10min
- 空值缓存:3~5s(极短,避免占用内存)
- 用户会话:30min,支持续期
7.3 缓存淘汰策略
- L1本地Caffeine:基于容量淘汰,Window TinyLFU,设置最大容量限制,防止OOM
- L2 Redis:allkeys-lru,内存达到阈值自动淘汰最少访问key
八、缓存降级、限流、熔断防护体系
1. 熔断(Sentinel)
监控Redis调用异常比例、响应超时,触发熔断后不再访问Redis,直接走降级逻辑:
- 降级方案1:读取本地缓存旧数据
- 降级方案2:返回静态默认兜底页面/空数据
- 降级方案3:直接提示“活动火爆,请稍后重试”
2. 分布式限流(Redis Lua滑动窗口)
针对穿透、恶意爬虫,限制单IP/单用户每秒查询缓存次数,超限直接拦截,不访问缓存与DB。
3. 本地限流(RateLimiter)
单机控制缓存最大并发请求数,防止单机流量打爆Redis。
九、分业务场景缓存落地模板
场景1:商品详情(读极高,写少)
- 分层:Caffeine本地缓存 + Redis逻辑永不过期热点副本
- 防击穿:逻辑过期,异步刷新
- 更新:延迟双删,定时预热
- 防穿透:布隆过滤器拦截无效商品ID
场景2:秒杀库存(读写双高,热点极强)
- 热点拆分:库存key多副本分片
- 缓存仅做库存预减校验,真实扣减落DB
- MQ异步下单削峰,减少缓存并发更新
- 本地缓存不存储真实库存,避免多实例不一致
场景3:系统配置字典(几乎不更新)
- 仅本地Caffeine缓存,Redis做全局同步存储
- 逻辑永久有效,后台修改后广播清除所有实例本地缓存
场景4:用户订单快照(读多,按月分冷热)
- 近3个月热单存入Redis Hash;历史冷单不缓存,直接查DB
- 分页查询,禁止一次性加载完整订单列表大key
更多推荐



所有评论(0)