news 2026/4/16 14:19:14

别只会加缓存了:带你系统性设计高并发读写架构(附架构图)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别只会加缓存了:带你系统性设计高并发读写架构(附架构图)

0. 序章:当“加个 Redis”不再是万能解药

“系统慢了?加个 Redis 缓存一下。”
“数据库 CPU 飙高?把热点数据丢 Redis 里。”

在 1-3 年经验的工程师眼里,Redis 仿佛是架构设计的“速效救心丸”。然而,当你的业务量从 QPS 1000 涨到 10 万,甚至百万时,你会发现这颗救心丸变成了毒药:

  • 缓存穿透/击穿让数据库瞬间暴毙。
  • 缓存与数据库的一致性问题,让用户看到的数据“薛定谔化”。
  • 高并发写入场景下,Redis 并没有解决 MySQL 的行锁瓶颈,反而引入了双写复杂性。

真正的架构师,处理高并发不仅仅是“加缓存”,而是对流量进行分层治理、对读写进行分离设计、对数据一致性做取舍

这篇文章,我不讲 Redis 的基本命令,我们将深入读写分离架构的核心方法论,通过一套组合拳,解决“高并发读”和“高并发写”两大难题。


Ⅰ. 读架构设计:流量过滤的漏斗艺术

高并发读的核心思想是**“挡”**。请求像洪水,不能让它们全部冲到 MySQL 这座大坝上,我们要在上游建立层层大坝(Cache Layers)。

但这不仅仅是“客户端 -> Redis -> DB”这么简单。

1.1 多级缓存的“洋葱模型”

优秀的读架构,应该像剥洋葱一样,每一层都过滤掉一部分流量。

  1. 端侧缓存(Client/Browser):利用 HTTPCache-Control,让静态资源直接在用户浏览器“安家”。
  2. CDN 边缘节点:动静分离,将图片、CSS、JS 甚至静态化的 HTML 推送到离用户最近的节点。
  3. 接入层缓存(Nginx/OpenResty):在网关层通过 Lua 脚本直接查询本地 Shared Dict,连应用服务器都不用进。
  4. 应用层本地缓存(Local Cache):这是最容易被忽视的一层。使用 Caffeine 或 Guava,在 JVM 进程内拦截热点。
  5. 分布式缓存(Redis Cluster):最后的防线,抗住 90% 的剩余流量。
  6. 数据库(DB):兜底,只承担 Miss 的那 1%。

1.2 架构图解:三级缓存防御体系

Miss

Lua缓存 Miss

Hit

Miss

Hit

Miss

用户请求

CDN边缘节点

Nginx 负载均衡

应用服务集群

Caffeine 本地缓存

返回数据

Redis 分布式缓存

返回数据

MySQL 数据库

返回数据

1.3 核心痛点解决:热点 Key 的“本地化”

在秒杀场景下,即使是 Redis 也扛不住单 Key 100万 QPS 的访问(由于 Redis 单线程模型,单节点热点 Key 会导致 CPU 100%)。

解决方案:热点探测 + 本地缓存

不要所有请求都去 Redis,在应用层引入Caffeine

  • 原理:应用启动一个异步线程或利用 Sentinel 的热点参数限流功能,统计最近 1 秒内的 Top N Key。
  • 动作:一旦发现某 Key 是热点,将其缓存到 JVM 堆内存中,过期时间设为极短(如 3 秒)。
  • 效果:哪怕 Redis 挂了,这 3 秒内的百万流量也只会在应用内存中打转,根本出不去。

Ⅱ. 写架构设计:削峰填谷的蓄水池

高并发写的核心思想是**“缓冲”“异步”**。数据库是磁盘 IO 密集型组件,对不起,它真的很快(写 WAL 日志很快),但它也很慢(随机写数据页很慢)。

直接让高并发写请求打到 DB,会导致大量的行锁竞争(Row Lock Contention),系统吞吐量直线下降。

2.1 写操作的“三板斧”

  1. 异步化(MQ):将“同步写”转为“发消息”。只要消息进到了 Kafka/RocketMQ,就认为操作成功。
  2. 合并写(Batching):也就是“写聚合”。将 100 次单独的INSERT合并为 1 次INSERT INTO ... VALUES (...), (...), (...)
  3. 分库分表(Sharding):当单表数据量超过 2000 万或单机写入 QPS 超过 3000,必须物理拆分。

2.2 架构图解:高并发写入缓冲模型

容灾降级

每100条或每500ms

故障

用户写请求

网关层

消息队列 Kafka/RocketMQ

消费服务组

内存聚合 Buffer

MySQL 主库

降级日志文件

异步恢复任务


Ⅲ. 核心代码实战:手撸一个“自动合并写入”缓冲区

光说不练假把式。很多场景下,我们不想引入沉重的 MQ,只想在应用层做一个微型的“合并写入”来抗住突发写流量。

下面是一个基于 Java 阻塞队列 + 定时任务的高并发合并写入器实现。它具备“定量触发”和“定时触发”双重机制。

importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.*;/** * 高并发写缓冲器 (Batch Writer) * 核心逻辑:积攒够 N 条记录 或 超过 M 毫秒,触发一次批量落库 */publicclassBatchWriterService<T>{// 内存缓冲区,使用线程安全的阻塞队列privatefinalBlockingQueue<T>bufferQueue=newLinkedBlockingQueue<>(10000);// 触发阈值:达到 100 条就刷盘privatefinalintBATCH_SIZE=100;// 触发时间:每 500ms 必须刷盘一次(防止数据长时间滞留)privatefinallongTIMEOUT_MS=500;privatevolatilebooleanisRunning=true;publicBatchWriterService(){startConsumer();}// 1. 生产者接口:业务层只管往里塞,极其轻量publicvoidadd(Ttask){if(!bufferQueue.offer(task)){// 队列满时的降级策略:记录日志、抛出异常或转入MQSystem.err.println("Buffer full! Task dropped.");}}// 2. 消费者线程:负责聚合与落库privatevoidstartConsumer(){newThread(()->{List<T>drainList=newArrayList<>();while(isRunning){try{// 核心逻辑:从队列中取数据// 如果队列为空,drainTo 不会阻塞等待,所以需要配合 take() 或 poll()// 这里使用一个简单的自旋 + 时间控制逻辑longstart=System.currentTimeMillis();TfirstItem=bufferQueue.poll(TIMEOUT_MS,TimeUnit.MILLISECONDS);if(firstItem!=null){drainList.add(firstItem);// 继续拉取剩余的,最多拉取 BATCH_SIZE - 1 个bufferQueue.drainTo(drainList,BATCH_SIZE-1);}// 判断触发条件if(!drainList.isEmpty()){flushToDB(drainList);drainList.clear();}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}catch(Exceptione){// 兜底异常处理,防止线程退出e.printStackTrace();}}},"Batch-Writer-Thread").start();}// 3. 模拟批量落库privatevoidflushToDB(List<T>list){System.out.println("🔥 批量插入数据库,条数: "+list.size());// jdbc.batchUpdate(...)}// 4. 优雅停机 Hookpublicvoidshutdown(){this.isRunning=false;// 停机前最后一次刷盘,防止数据丢失List<T>remain=newArrayList<>();bufferQueue.drainTo(remain);if(!remain.isEmpty()){flushToDB(remain);}}}

代码解析:

  • 双重触发机制:仅仅判断数量是不够的,如果流量突然低谷,数据可能卡在内存里几分钟不落库,这是 Bug。必须加上poll(timeout)的时间兜底。
  • 优雅停机:生产环境服务重启频繁,必须提供shutdown()钩子,保证 JVM 销毁前把内存里的数据吐干净。

Ⅳ. 数据一致性:CAP 理论的终极博弈

高并发架构中,最让人头秃的莫过于DB 和 Redis 的数据一致性

网上盛传的“延时双删”(Delete -> Write DB -> Sleep -> Delete)在极端高并发下依然会有概率脏数据,且Sleep多久是一个玄学。

4.1 终极方案:基于 Binlog 的异步更新(Canal 模式)

与其在应用层纠结先删缓存还是先改库,不如把缓存更新的逻辑从业务代码中剥离出来,下沉到基础设施层。

方案逻辑:

  1. 业务代码只管写 MySQL,完全不操作 Redis
  2. Canal(阿里开源中间件)伪装成 MySQL Slave,监听 Master 的 Binlog。
  3. 一旦 MySQL 发生变更,Canal 解析 Binlog 消息,投递到 MQ。
  4. 消费服务订阅 MQ,解析出变更的数据,重放到 Redis 中。

4.2 架构图解:Canal 旁路同步

1. Update/Insert
2. Binlog Replication
3. 解析 Binlog
4. 订阅变更
5. Upsert/Del

业务应用

MySQL Master

Canal Server

消息队列 Kafka

缓存同步服务

Redis 缓存

优点:

  • 业务解耦:业务代码里没有一行 Redis 操作代码,清爽。
  • 最终一致性:只要 Binlog 不丢,MQ 不丢,缓存最终一定会一致。
  • 防抖动:如果同一条数据 1 秒内被改了 100 次,同步服务可以在内存中合并这 100 次变更,只写 Redis 一次(Write Behind)。

Ⅴ. 性能/稳定性分析:架构师的体检表

在设计完上述架构后,必须进行自我拷问。以下是针对该架构的性能瓶颈分析与优化对比:

关注维度潜在瓶颈/风险优化/兜底方案
读性能Redis 成为单点瓶颈,大 Key 导致网卡打满1. 上 Local Cache 分担热点


2. Redis Cluster 分片


3. 开启多级副本读写分离 |
|写性能| MQ 积压,导致数据入库延迟 | 1. 增加 Topic 分区数


2. 消费者改为多线程并发消费


3.动态扩容:监控积压阈值,自动拉起更多消费者容器 |
|一致性| Canal 同步延迟(秒级),用户刚改完刷新旧数据 | 1. 强制读主:在写完后的短期窗口内(如500ms),特定接口强制走 DB


2. 接受现实:大部分互联网业务接受 1-2 秒的数据延迟 |
|可用性| 缓存雪崩(Cache Avalanche) | 1. Redis Key 过期时间设为 Random(TTL)


2. 使用 Hystrix/Sentinel 进行熔断降级,返回默认值 |


Ⅵ. 实战案例复盘:某信息流 Feed 系统的重构

背景:
某社交 App 的 Feed 流系统,用户数 500 万。原有架构是App -> Server -> MySQL。随着用户增长,早高峰刷 Feed 流时,数据库 CPU 经常飙升到 90%,且写入评论经常超时。

重构步骤:

  1. 读优化(推拉结合):
  • 对于大 V(粉丝 > 100万):发帖时,直接写入 DB,粉丝拉取时再去查询(拉模式),避免写扩散。
  • 对于普通用户:发帖时,异步写入所有粉丝的 Redis 收件箱(推模式 / Timeline Cache)。
  • 落地效果:读取 QPS 提升 20 倍,DB 压力几乎降为零。
  1. 写优化(聚合写入):
  • 对于“点赞”这种高频低价值操作,不再实时写库。
  • 使用 Redis 的HyperLogLogHash结构在内存计数。
  • 每分钟通过定时任务将 Redis 里的点赞数同步回 MySQL 持久化。
  • 落地效果:写入 TPS 从 2000 提升至 Redis 极限的 80000+。
  1. 防穿透设计:
  • 对于查询不存在的 Feed ID,在 Redis 中缓存一个 Null Object,过期时间 5 分钟,防止恶意攻击穿透到 DB。

Ⅶ. 经验总结

系统性设计高并发读写架构,不是堆砌组件,而是做权衡(Trade-off)

  1. 读流量要分层:离用户越近越好,能在 CDN 解决的别去 Redis,能在 Local Cache 解决的别去远端。
  2. 写流量要缓冲:不要把 MySQL 当作实时处理引擎,把它当作最终持久化仓库。MQ 和 Batch 是写性能的救星。
  3. 一致性要取舍:除非是金融账务,否则不要追求强一致性。最终一致性是高并发架构的基石。
  4. 监控先行:没有监控的架构设计就是盲人摸象。Prometheus + Grafana 必须覆盖 QPS、RT、Cache Hit Rate、MQ Lag 等核心指标。

架构设计没有银弹,只有最适合当前业务阶段的方案。希望这套**“过滤+缓冲+异构同步”**的组合拳,能为你现在的系统重构提供思路。

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

拒绝 CRUD 搬砖:我如何用脚本 + 模板把重复工作降到 10%

一、真实痛点引入&#xff1a;我们是工程师&#xff0c;还是“高级打字员”&#xff1f; 回想一下你最近接的一个需求&#xff1a;“给后台增加一个商品分类管理功能”。 逻辑极其简单&#xff1a;增删改查&#xff08;CRUD&#xff09;。但你需要做哪些动作&#xff1f; 设…

作者头像 李华
网站建设 2026/4/13 6:15:33

2026毕设ssm+vue旅行网的设计与实现论文+程序

本系统&#xff08;程序源码&#xff09;带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。 系统程序文件列表 开题报告内容 一、选题背景 关于旅游信息化管理问题的研究&#xff0c;现有研究主要以传统OTA平台&#xff08;在线旅游代理&#xff09;的整体架构…

作者头像 李华
网站建设 2026/4/16 12:28:56

气动式定尺飞锯机设计

2气动式定尺飞锯机整体设计 2.1功能分析 气动式定尺飞行锯机应用的领域广泛&#xff0c;例如锯切焊接管&#xff0c;钢筋&#xff0c;轻铝管和大多合金管。显着的特征是由于其高的切割力和可切割管道的大直径范围&#xff0c;以及由于空气床装置保证了气动技术的高压力性&#…

作者头像 李华
网站建设 2026/4/12 15:48:57

A型半自动平面贴标机结构设计

2平面纸盒贴标机 2.1平面纸盒贴标机用途功能介绍及研究意义 本次设计的平面贴标机主要应用于纸盒的平面贴标。例如烟盒&#xff0c;日用品包装盒&#xff0c;食品包装盒等。能够应用于各种产品的不同生产流水线对其进行商标标签粘贴是其主要的工作用途。相对于人工贴标来说&…

作者头像 李华
网站建设 2026/4/16 13:00:43

2026毕设ssm+vue论文投稿系统论文+程序

本系统&#xff08;程序源码&#xff09;带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。 系统程序文件列表 开题报告内容 一、选题背景 关于文档管理系统与在线协作平台的研究&#xff0c;现有研究主要以通用型办公自动化系统和企业级内容管理系统为主&…

作者头像 李华
网站建设 2026/4/16 12:25:20

基于 8086 多功能电子时钟系统设计

一、系统设计背景与核心目标 随着电子技术的发展&#xff0c;单一功能的电子时钟已难以满足多样化需求。在家庭、实验室、工业控制等场景中&#xff0c;人们不仅需要准确的时间显示&#xff0c;还期望时钟具备闹钟、倒计时、温度监测等附加功能。8086 微处理器凭借强大的运算能…

作者头像 李华