原创

系统架构设计(一):构建“客户端+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 端

HTTP Cache

(Cache-Control, ETag)

静态资源、API 响应头控制

LocalStorage

/ SessionStorage

用户偏好、Token、小型配置

Service Worker

离线访问、PWA 应用、精细化的网络拦截

移动端

Memory/Disk Cache

(图片/文件)

图片缩略图、视频流、大文件下载

SQLite

/ Realm

复杂的离线业务数据、消息列表

2. 适合缓存的数据

  • 静态资源:CSS/JS/图片/字体(配合 CDN 效果最佳)。
  • 弱网/离线数据:新闻详情、地图瓦片、商品详情页(允许短暂旧数据)。
  • 个性化低频变更数据:用户头像、昵称、主题设置。
  • 读多写少的 API 响应:如首页推荐列表,可在客户端缓存 30 秒 -1 分钟。

3. 注意事项

  • 安全性严禁在客户端存储敏感信息(明文密码、支付密钥)。
  • 一致性:客户端缓存最难控制更新,通常采用“版本号强制刷新”或“短 TTL + 静默更新”策略。

四、全链路缓存流转流程

当用户发起一个请求时,数据是如何在“客户端 -> L1 -> L2 -> L3”之间流动的?以下是标准的读请求处理流程。

第一步:客户端自查

  • 用户打开 App 或网页,首先检查本地 LocalStorageService 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 节点扛不住。
  • 解决
    1. L1 拦截:利用本地缓存挡住 90% 的流量。
    2. Key 分片:在 Redis 中将热点 Key 复制多份(如 key_1, key_2...),客户端随机读取,分散压力。
正文到此结束