news 2026/8/19 14:13:15

【架构实战】缓存架构设计实战:从Redis到多级缓存,彻底搞定高并发读

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【架构实战】缓存架构设计实战:从Redis到多级缓存,彻底搞定高并发读

【架构实战】缓存架构设计实战:从Redis到多级缓存,彻底搞定高并发读

今天早上聊了消息队列,解决的是"写"和"异步"的问题。但高并发系统还有另一半江山:

互联网业务的常态是"读多写少"——商品详情页、微博首页feed、新闻列表、排行榜,读流量往往是写的几十倍甚至上百倍。如果每次读都打到数据库,MySQL在几千QPS时就趴下了。缓存,就是专门解决"读"的武器。

缓存这东西又爱又恨:用好了,系统QPS从几百冲到几万,延迟从几十毫秒降到亚毫秒;用不好,缓存穿透、击穿、雪崩三连击,能让你半夜被告警叫起来。这篇从缓存选型、读写策略、三大灾难、一致性问题,到多级缓存和商品详情页实战,一次讲透。

一、为什么需要缓存

先说清楚缓存到底解决了什么。

减轻数据库压力:数据库(尤其关系型)的并发能力有限,且每查一次都要走磁盘IO、加锁、解析SQL。把热点数据放内存,90%的读请求根本到不了DB。

降低响应延迟:内存访问是纳秒级,磁盘是毫秒级,差了几个数量级。缓存命中时响应直接从微秒级降到亚毫秒级,用户体验质变。

承接突发流量:热点事件(明星塌房、秒杀开始)会让读流量瞬间暴涨几十倍。缓存层像一道防洪堤,把洪峰拦在外面,DB只处理漏过来的涓涓细流。

成本优势:一台Redis顶几十台MySQL的读能力,用便宜的内存换昂贵的数据库连接和CPU,性价比极高。

经典经验法则:80%的请求集中在20%的数据上(二八定律)。只要把这20%的热点缓存住,系统整体性能就上去了。

二、缓存的核心概念

不管用什么缓存中间件,这几个概念必须刻进DNA:

  • 命中率(Hit Rate)命中数 / 总请求数。命中率低于90%的缓存基本等于没用,要查为什么(缓存粒度太粗?过期太快?)。
  • 缓存穿透(Penetration):查一个根本不存在的数据,缓存没有、DB也没有,请求每次都穿透到DB。恶意攻击最爱用这个刷库。
  • 缓存击穿(Breakdown):某个热点key突然过期,瞬间海量请求同时穿透到DB,把DB打挂。
  • 缓存雪崩(Avalanche):大量key在同一时刻集中过期,或Redis宕机,请求全部打到DB,DB瞬间崩。
  • 预热(Warm Up):系统启动或大促前,提前把热点数据加载进缓存,避免冷启动被打爆。
  • 降级(Degradation):缓存不可用时,直接返回默认值/静态页/稍后重试,保住核心链路。

三、Redis核心数据结构与实战选型

Redis不是简单的key-value,它的数据结构选型直接决定代码复杂度。选错结构,代码又臭又长。

3.1 String:计数器与分布式锁

# 商品库存计数 INCR article:view:10086 # 分布式锁 SET lock:order:123 uuid NX EX 30

最通用,但存对象要自己序列化(JSON),频繁更新单个字段会很浪费(整存整取)。

3.2 Hash:对象存储

HSET user:10086 name "张三" age 28 score 99 HGET user:10086 name

适合字段多、需要单独更新的对象(用户资料、商品属性)。更新单个字段不用整体重写,省带宽。

3.3 ZSet:排行榜与延迟队列

ZADD leaderboard 99 "张三" 88 "李四" ZREVRANGE leaderboard 0 9 WITHSCORES # 取Top10

分数(score)自动排序,排行榜、热帖、延迟任务(score=执行时间戳)都靠它。

3.4 布隆过滤器:判断"可能存在"

缓存穿透的终极武器(后面详讲)。它用位数组+多个哈希函数,能高效判断"某元素是否一定不存在",空间极小。

选型经验:简单KV用String;对象且要局部更新用Hash;排名/范围用ZSet;去重/存在性判断用Set或布隆过滤器。

四、缓存读写策略(核心)

这是缓存设计里最容易写错的地方。错误的读写顺序 = 数据不一致的根源。

4.1 Cache Aside Pattern(旁路缓存)——最常用

:先查缓存,命中直接返回;未命中查DB,写入缓存,返回。
先更新数据库,再删除缓存(不是更新缓存!)。

# 读defget_user(user_id):data=redis.get(f"user:{user_id}")ifdata:returnjson.loads(data)# 缓存命中user=db.query_user(user_id)# 查DBredis.setex(f"user:{user_id}",3600,json.dumps(user))# 回写returnuser# 写defupdate_user(user_id,new_data):db.update_user(user_id,new_data)# 1. 先更新DBredis.delete(f"user:{user_id}")# 2. 再删除缓存(不是set!)

为什么是"删除"而不是"更新"缓存?因为更新缓存可能把"还没提交的事务数据"写进去,也可能多个写并发导致脏数据。删掉让它下次读时自动回写,最简单也最安全。

4.2 Read/Write Through(穿透型)

缓存自己负责和DB同步,应用只和缓存打交道。CacheLoader模式(如Caffeine、Spring Cache)就是这种。对应用透明,但实现复杂。

4.3 Write Behind(异步回写)

写只写缓存,由缓存异步批量刷回DB。性能极高(减少DB写次数),但丢数据风险大(缓存挂了没刷盘的就没了),多用于对一致性不敏感的场景。

我的结论:绝大多数业务用Cache Aside + 先更新DB再删缓存就够了,简单可靠。

五、缓存三大灾难:穿透、击穿、雪崩

这是面试和实战必考。逐一拆解。

5.1 缓存穿透:查不存在的数据

现象:黑客用一堆不存在的ID狂刷(如user_id=-1user_id=99999999),缓存永远miss,请求全打到DB。

解法1:缓存空值

defget_user(user_id):data=redis.get(f"user:{user_id}")ifdata==b"NULL":# 缓存了空标记returnNoneuser=db.query_user(user_id)ifnotuser:redis.setex(f"user:{user_id}",300,"NULL")# 空值也缓存,短过期returnNoneredis.setex(f"user:{user_id}",3600,json.dumps(user))returnuser

缺点:大量空值会占内存,适合不存在的key有限的场景。

解法2:布隆过滤器(推荐)

# 所有合法user_id预先放入布隆过滤器bloom.add(user_id)defget_user(user_id):ifnotbloom.contains(user_id):# 一定不存在,直接拒绝returnNone# 继续走缓存/DB逻辑

布隆过滤器说"不存在"就一定不存在,从根上挡住了穿透。代价是有误判率(说存在可能不存在),且不支持删除。

5.2 缓存击穿:热点key过期瞬间

现象:某个爆款商品key突然过期,几万QPS同时发现缓存miss,一窝蜂查DB,DB被冲垮。

解法1:互斥锁(分布式锁)

defget_hot_product(pid):data=redis.get(f"product:{pid}")ifdata:returndata# 只有一个线程能拿到锁去查DB,其他等待重试ifredis.set(f"lock:{pid}","1",nx=True,ex=3):try:data=db.query_product(pid)redis.setex(f"product:{pid}",3600,data)finally:redis.delete(f"lock:{pid}")returndataelse:time.sleep(0.05)returnget_hot_product(pid)# 重试读缓存

解法2:逻辑过期(不设真过期,代码里判断)
key不设置TTL,值里带expire时间戳;发现"逻辑过期"时,开一个后台线程重建缓存,当前请求先返回旧值。用户体验最好(永远不等待),但实现稍复杂。

5.3 缓存雪崩:大量key同时失效

现象:几百个key设了相同过期时间(比如都3600秒),到点一起失效,DB瞬间被海量请求淹没。

解法

  1. 过期时间加随机值expire = base + random(0, 300),错开失效时间。
  2. 多级缓存:本地缓存兜底,Redis挂了还有Caffeine扛着。
  3. 限流降级:DB前置限流,超出阈值的请求直接返回降级页。
  4. Redis高可用:主从+哨兵或集群,避免单点宕机导致全量雪崩。
# 加随机过期,避免集体失效importrandom ttl=3600+random.randint(0,300)redis.setex(key,ttl,value)

六、缓存与数据库一致性

这是缓存设计里最让人纠结的问题:缓存和DB的数据怎么保证一致?

6.1 先删缓存再更新DB,还是反过来?

  • 先删缓存,再更新DB:在"删缓存"和"更新DB"之间,如果有读请求进来,会读DB旧值并回写缓存,导致缓存是脏数据,且长期存在(直到下次更新)。❌ 不推荐。
  • 先更新DB,再删缓存(Cache Aside):极端情况下(读请求在DB更新前读到旧值,且恰好缓存刚失效),可能写回旧值。但读比写快得多,这个窗口极小,且可以靠"延迟双删"兜底。✅ 推荐。

6.2 延迟双删

应对上面的极端竞态:

defupdate_with_double_delete(user_id,data):redis.delete(f"user:{user_id}")# 1. 删缓存db.update_user(user_id,data)# 2. 更新DBtime.sleep(0.5)# 3. 等旧读请求回写完成redis.delete(f"user:{user_id}")# 4. 再删一次,清掉脏回写

sleep时间要大于"读请求查DB+回写缓存"的耗时。简单有效,但sleep不优雅。

6.3 终极方案:binlog订阅(Canal)

业务只更新DB,用Canal监听MySQL binlog,异步删除/更新Redis。彻底解耦,一致性最强,是大厂标配。代价是引入中间件,架构变重。

结论:中小业务用 Cache Aside + 延迟双删足矣;大规模强一致诉求上Canal。记住一句话——缓存本来就是"容忍短暂不一致换性能"的,追求强一致就别用缓存,或接受"最终一致"

七、多级缓存架构:把性能榨到极致

单级Redis在极端流量下也会成瓶颈(网络IO、序列化开销)。真正的高并发系统是多级缓存

请求 → Nginx/CDN(静态资源) ↓(动态请求) 本地缓存 Caffeine(JVM内存,纳秒级,扛80%流量) ↓(miss) Redis集群(内存,微秒级,扛剩余流量) ↓(miss) MySQL(磁盘,兜底)
  • L1 本地缓存(Caffeine):进程内,零网络开销,纳秒级。容量小(几百MB),存最热的数据。适合"读多写少、允许短暂不一致"的数据。
  • L2 分布式缓存(Redis):跨进程共享,所有节点一致,容量大。扛本地缓存miss的流量。
  • L3 静态化/CDN:商品详情页整页HTML直接CDN分发,连应用都不进。
// Caffeine本地缓存示例Cache<String,Product>cache=Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10,TimeUnit.MINUTES).build();ProductgetProduct(Stringpid){returncache.get(pid,id->{// 本地miss,查Redis,再miss查DBreturnredisOrDbQuery(id);});}

关键设计:本地缓存要能主动失效(通过Redis的Pub/Sub广播失效消息),否则一台机器更新了DB,其他机器的本地缓存还是旧的。

八、生产级案例:商品详情页

一个日活千万级的商品详情页,典型架构长这样:

用户请求商品详情 ↓ CDN:命中整页静态HTML?→ 直接返回(占比~60%) ↓ miss Nginx:Lua读取本地缓存(OpenResty shared dict)?→ 返回(占比~15%) ↓ miss 应用层:Caffeine本地缓存?→ 返回(占比~15%) ↓ miss Redis:商品缓存?→ 返回并回写本地(占比~9%) ↓ miss(~1%) MySQL:查库 → 回写Redis + 本地缓存

全链路命中率99%+,最终打到DB的请求不到1%。即使Redis宕机,Caffeine和CDN还能扛住大部分流量,DB稳如老狗。这就是多级缓存的威力。

九、避坑清单:过来人的血泪经验

坑1:缓存大key
一个String存了整个商品列表(几MB),序列化/反序列化慢,网卡打满,Redis阻塞。解决:拆分(按页/按字段)、压缩、用Hash分字段。

坑2:热key集中打爆单节点
某个爆款key全落在Redis一个slot/节点,该节点CPU 100%。解决:热key打散(key加随机后缀hotkey:{0~9}),或本地缓存兜底。

坑3:缓存和DB数据长期不一致
忘了"先更新DB再删缓存",或更新DB后删缓存失败(异常没catch)。解决:删缓存操作放finally,或引入binlog补偿。

坑4:缓存过期时间统一,引发雪崩
见5.3,加随机值错峰。

坑5:用了缓存就不设兜底降级
Redis挂了,请求全穿透到DB,DB被冲死,整个系统雪崩。解决:Redis不可用时,本地缓存/默认值/限流降级,保住核心链路。

坑6:序列化选型随意
用JDK原生序列化,体积大、跨语言不兼容。解决:统一用JSON(或Protobuf/Kryo),体积小、可读、跨语言。

坑7:缓存命中率不监控
上线后从不看命中率,缓存其实一直在miss(等于没用)。解决:监控keyspace_hits / (keyspace_hits + keyspace_misses),低于90%就要查原因。

十、总结

  • 定位:缓存解决"读多写少"的高并发读问题,和MQ(解决写/异步)是黄金搭档。
  • 策略:Cache Aside + 先更新DB再删缓存,简单可靠;强一致诉求上Canal订阅binlog。
  • 三大灾难:穿透用布隆过滤器/空值缓存,击穿用互斥锁/逻辑过期,雪崩用随机过期+多级缓存+限流。
  • 多级缓存:CDN → 本地(Caffeine) → Redis → DB,把99%流量拦在DB之外。
  • 一致性:缓存天生容忍短暂不一致,追求强一致就别用缓存,接受"最终一致"才是工程现实。

缓存用好了是高并发系统的护城河,用不好是半夜故障的温床。今天这篇和早上的消息队列合起来,基本覆盖了"高并发系统"读写两侧的骨架。下一篇可以聊聊怎么用限流、熔断、降级把这些组件串成一座不宕机的系统——关注我,别错过。

我是做架构的,关注我,一起搞定分布式系统里的那些坑。

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

基于LLM智能体与树搜索的自动化形式化验证技术解析

1. 从“人肉验证”到“智能导航”&#xff1a;形式化验证的自动化新范式 在芯片设计、安全协议和关键软件系统的开发中&#xff0c;形式化验证&#xff08;Formal Verification&#xff09;是确保系统行为绝对正确的“黄金标准”。它通过严格的数学方法证明系统模型是否满足其规…

作者头像 李华
网站建设 2026/8/19 14:07:04

多智能体大模型协作失效?解析探索机制缺失与动态交互设计

1. 项目概述&#xff1a;当多智能体大模型陷入“信息茧房”最近在折腾一个多智能体协作的项目&#xff0c;目标是让几个不同的大语言模型&#xff08;LLMs&#xff09;扮演不同角色&#xff0c;比如一个负责规划&#xff0c;一个负责执行&#xff0c;一个负责审核&#xff0c;共…

作者头像 李华
网站建设 2026/8/19 14:06:19

imwallet官网完整架构与全套部署流程

下文整套架构适配 imToken 冷钱包官网业务,严格采用在线官网下载网关、热端观察服务、空气隔离离线签名端、硬件HSM固件分发四层分离架构;全程遵循核心安全准则:服务器永远不保存助记词、明文私钥,私钥只存活在断网冷设备;官网前端搭载多层反调试、真机设备检测技术学习。一、整…

作者头像 李华
网站建设 2026/8/19 14:05:20

【计算机毕业设计单片机案例】STM32 控制的多档位舵机药品仓自动开启装置设计 基于 STM32 单片机的便携式老年人智能服药提醒终端设计(024303)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/8/19 14:02:21

工业以太网无线通信:从技术选型到现场部署的完整指南

1. 先搞清楚“工业以太网无线通信”到底要解决什么实际问题在工厂、产线、自动化仓库这类地方&#xff0c;你经常会遇到一个头疼的问题&#xff1a;设备之间需要高速、稳定地交换数据&#xff0c;但拉网线太麻烦&#xff0c;甚至根本拉不了。比如&#xff0c;移动的AGV小车、旋…

作者头像 李华