系统架构设计(一):构建“客户端+L1/L2/L3”四级缓存防御体系
温馨提示:
本文最后更新于 2026年03月08日,已超过 18 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我。
一、引言
在亿级流量的高并发场景下,单一缓存策略往往独木难支。想象一下双十一零点爆发的流量洪峰,如果所有请求都直接穿透到数据库,系统将在毫秒级内崩溃。为了应对这种挑战,现代架构设计遵循一个核心原则:数据离用户越近越好,访问路径越短越快。基于此,一套“组合拳”应运而生:利用客户端缓存让终端设备分担压力,实现“零网络延迟”;同时在服务端构建三级缓存体系(本地内存 -> 分布式集群 -> 持久化存储),形成梯次防御。本文将深入剖析这两大模块的协同工作机制,通过完整的请求流转图解与热点 Key 分散等实战策略,带你构建一套“快、稳、省”的全链路缓存体系。
二、服务端三级缓存
服务端三级缓存并非指某种特定技术,而是一种分层架构思想。它通过不同速度、容量和成本的存储介质,平衡性能与资源。
1. 第一级:本地缓存
- 定位:应用进程内的“极速特区”。
- 实现组件:
Caffeine(推荐),Guava Cache,Ehcache(本地模式)。 - 核心特点:
- 速度:纳秒级,无网络开销,直接访问 JVM 内存。
- 容量:小,受限于单机内存,不适合存全量数据。
- 一致性痛点:集群环境下各节点数据隔离,需配合广播失效机制。
- 最佳缓存对象:
- 热点中的热点:如配置开关、字典表、秒杀活动的倒计时状态。
- 防穿透空值:缓存 DB 查询为空的记录(设置短 TTL),防止恶意请求打穿数据库。
- 对一致性不敏感的数据:允许秒级延迟的非核心业务数据。
2. 第二级:分布式缓存
- 定位:集群共享的“中央仓库”。
- 实现组件:
Redis(主流),Memcached。 - 核心特点:
- 速度:毫秒级,存在网络 RTT 开销,但远快于 DB。
- 容量:中等,可横向扩展集群。
- 一致性:天然保证集群内数据共享和一致。
- 最佳缓存对象:
- 核心业务热数据:用户 Session、购物车、库存快照、文章详情。
- 跨服务共享数据:微服务间频繁交互的公共数据。
- 复杂结构数据:利用 Redis 的 List/Set/ZSet 处理排行榜、消息队列等。
3. 第三级:数据库/近线存储
- 定位:数据的“最终防线”与“冷数据归档”。
- 说明:严格来说 DB 是持久层,但在缓存语境下,它是 L2 未命中后的兜底。部分架构也会将 ES 或 SSD 近线存储视为 L3。
- 最佳缓存对象:
- 全量数据与冷数据:所有业务的最终一致性来源。
- 强一致性要求数据:必须实时读取的最新状态(如账户余额,通常需加锁或特殊策略)。
三、客户端缓存
客户端缓存是将数据存储在用户设备(浏览器、App)或边缘节点,旨在减少网络请求,提升用户体验。
1. 核心实现方式
|
端类型 |
技术手段 |
适用场景 |
|
Web 端 |
(Cache-Control, ETag) |
静态资源、API 响应头控制 |
|
/ |
用户偏好、Token、小型配置 |
|
|
|
离线访问、PWA 应用、精细化的网络拦截 |
|
|
移动端 |
(图片/文件) |
图片缩略图、视频流、大文件下载 |
|
/ |
复杂的离线业务数据、消息列表 |
2. 适合缓存的数据
- 静态资源:CSS/JS/图片/字体(配合 CDN 效果最佳)。
- 弱网/离线数据:新闻详情、地图瓦片、商品详情页(允许短暂旧数据)。
- 个性化低频变更数据:用户头像、昵称、主题设置。
- 读多写少的 API 响应:如首页推荐列表,可在客户端缓存 30 秒 -1 分钟。
3. 注意事项
- 安全性:严禁在客户端存储敏感信息(明文密码、支付密钥)。
- 一致性:客户端缓存最难控制更新,通常采用“版本号强制刷新”或“短 TTL + 静默更新”策略。
四、全链路缓存流转流程
当用户发起一个请求时,数据是如何在“客户端 -> L1 -> L2 -> L3”之间流动的?以下是标准的读请求处理流程。
第一步:客户端自查
- 用户打开 App 或网页,首先检查本地
LocalStorage或Service Worker缓存。 - 策略:如果数据未过期,直接渲染,完全不发网络请求。这是性能优化的极致。
第二步:服务端本地缓存
- 请求到达服务器,先查
Caffeine。 - 优势:如果命中,直接返回,保护了 Redis 和网络带宽。
- 难点:如果是集群,A 机器有缓存,B 机器没有。当数据更新时,需要通过消息队列(如 RocketMQ/Kafka)广播“清除缓存”指令,让所有机器的 L1 失效。
第三步:分布式缓存
- L1 未命中,查
Redis。 - 回写策略:如果 Redis 命中,通常会将数据回写到 L1(同步或异步),以便下次请求直接在本地命中。
- 热点 Key 保护:对于超热点数据,L1 的存在极大地分摊了 Redis 的单点压力。
第四步:数据库兜底
- L2 也未命中,查询 MySQL。
- 缓存回填:查到数据后,必须按顺序写入 L2 和 L1(注意并发问题,避免缓存击穿)。
- 防穿透:如果数据库也没数据,要在 L2 和 L1 中存一个空对象(Null Object),并设置较短的过期时间(如 5 分钟),防止恶意攻击者反复查询不存在的数据。
五、核心难点与解决方案
1. 数据一致性如何保证?
- 客户端与服务端:无法做到强一致。采用版本控制(App 启动检查版本号)或短 TTL(如 1 分钟)来接受最终一致性。
- L1 与 L2(集群不一致):
- 方案 A(被动失效):数据更新时,发布消息到 MQ,所有服务节点订阅并清除本地 L1 缓存。
- 方案 B(短 TTL):L1 设置极短的过期时间(如 1-5 秒),容忍短暂的不一致,换取高性能。
2. 缓存雪崩与击穿
- 雪崩(大量缓存同时过期):给缓存过期时间增加随机值(如基础时间 + 随机 1-5 分钟),避免集体失效。
- 击穿(热点 Key 过期瞬间大量请求):使用互斥锁(Mutex Lock)或逻辑过期(后台线程异步更新,前台一直返回旧数据直到更新完成)。
3. 热点 Key 问题
- 某个 Key(如明星八卦新闻)访问量极大,单台 Redis 节点扛不住。
- 解决:
- L1 拦截:利用本地缓存挡住 90% 的流量。
- Key 分片:在 Redis 中将热点 Key 复制多份(如
key_1,key_2...),客户端随机读取,分散压力。
正文到此结束
- 本文标签: Java 高并发
- 本文链接: https://xiaolanzi.cyou/article/76
- 版权声明: 本文由卓原创发布,转载请遵循《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权
