核心内容摘要
这一夜,让世界静音:阿窝睡了,陪你跌入温柔的深梦
序章当“加个 Redis”不再是万能解药“系统慢了加个 Redis 缓存一下。
”“数据库 CPU 飙高把热点数据丢 Redis 里。
”在
年经验的工程师眼里Redis 仿佛是架构设计的“速效救心丸”。
然而当你的业务量从 QPS 1000 涨到 10 万甚至百万时你会发现这颗救心丸变成了毒药缓存穿透/击穿让数据库瞬间暴毙。
缓存与数据库的一致性问题让用户看到的数据“薛定谔化”。
高并发写入场景下Redis 并没有解决 MySQL 的行锁瓶颈反而引入了双写复杂性。
真正的架构师处理高并发不仅仅是“加缓存”而是对流量进行分层治理、对读写进行分离设计、对数据一致性做取舍。
这篇文章我不讲 Redis 的基本命令我们将深入读写分离架构的核心方法论通过一套组合拳解决“高并发读”和“高并发写”两大难题。
Ⅰ. 读架构设计流量过滤的漏斗艺术高并发读的核心思想是**“挡”**。
请求像洪水不能让它们全部冲到 MySQL 这座大坝上我们要在上游建立层层大坝Cache Layers。
但这不仅仅是“客户端 - Redis - DB”这么简单。
1 多级缓存的“洋葱模型”优秀的读架构应该像剥洋葱一样每一层都过滤掉一部分流量。
端侧缓存Client/Browser利用 HTTPCache-Control让静态资源直接在用户浏览器“安家”。
CDN 边缘节点动静分离将图片、CSS、JS 甚至静态化的 HTML 推送到离用户最近的节点。
接入层缓存Nginx/OpenResty在网关层通过 Lua 脚本直接查询本地 Shared Dict连应用服务器都不用进。
应用层本地缓存Local Cache这是最容易被忽视的一层。
使用 Caffeine 或 Guava在 JVM 进程内拦截热点。
分布式缓存Redis Cluster最后的防线抗住 90% 的剩余流量。
数据库DB兜底只承担 Miss 的那 1%。
2 架构图解三级缓存防御体系MissLua缓存 MissHitMissHitMiss用户请求CDN边缘节点Nginx 负载均衡应用服务集群Caffeine 本地缓存返回数据Redis 分布式缓存返回数据MySQL 数据库返回数据
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系统吞吐量直线下降。
1 写操作的“三板斧”异步化MQ将“同步写”转为“发消息”。
只要消息进到了 Kafka/RocketMQ就认为操作成功。
合并写Batching也就是“写聚合”。
将 100 次单独的INSERT合并为 1 次INSERT INTO ... VALUES (...), (...), (...)。
分库分表Sharding当单表数据量超过 2000 万或单机写入 QPS 超过 3000必须物理拆分。
2 架构图解高并发写入缓冲模型容灾降级每100条或每500ms故障用户写请求网关层消息队列 Kafka/RocketMQ消费服务组内存聚合 BufferMySQL 主库降级日志文件异步恢复任务Ⅲ. 核心代码实战手撸一个“自动合并写入”缓冲区光说不练假把式。
很多场景下我们不想引入沉重的 MQ只想在应用层做一个微型的“合并写入”来抗住突发写流量。
下面是一个基于 Java 阻塞队列 定时任务的高并发合并写入器实现。
它具备“定量触发”和“定时触发”双重机制。
importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.*;/** * 高并发写缓冲器 (Batch Writer) * 核心逻辑积攒够 N 条记录 或 超过 M 毫秒触发一次批量落库 */publicclassBatchWriterServiceT{// 内存缓冲区使用线程安全的阻塞队列privatefinalBlockingQueueTbufferQueuenewLinkedBlockingQueue(
;// 触发阈值达到 100 条就刷盘privatefinalintBATCH_SIZE100;// 触发时间每 500ms 必须刷盘一次防止数据长时间滞留privatefinallongTIMEOUT_MS500;privatevolatilebooleanisRunningtrue;publicBatchWriterService(){startConsumer();}//
生产者接口业务层只管往里塞极其轻量publicvoidadd(Ttask){if(!bufferQueue.offer(task)){// 队列满时的降级策略记录日志、抛出异常或转入MQSystem.err.println(Buffer full! Task dropped.);}}//
消费者线程负责聚合与落库privatevoidstartConsumer(){newThread(()-{ListTdrainListnewArrayList();while(isRunning){try{// 核心逻辑从队列中取数据// 如果队列为空drainTo 不会阻塞等待所以需要配合 take() 或 poll()// 这里使用一个简单的自旋 时间控制逻辑longstartSystem.currentTimeMillis();TfirstItembufferQueue.poll(TIMEOUT_MS,TimeUnit.MILLISECONDS);if(firstItem!null){drainList.add(firstItem);// 继续拉取剩余的最多拉取 BATCH_SIZE - 1 个bufferQueue.drainTo(drainList,BATCH_SIZE-
;}// 判断触发条件if(!drainList.isEmpty()){flushToDB(drainList);drainList.clear();}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}catch(Exceptione){// 兜底异常处理防止线程退出e.printStackTrace();}}},Batch-Writer-Thread).start();}//
模拟批量落库privatevoidflushToDB(ListTlist){System.out.println( 批量插入数据库条数: list.size());// jdbc.batchUpdate(...)}//
优雅停机 Hookpublicvoidshutdown(){this.isRunningfalse;// 停机前最后一次刷盘防止数据丢失ListTremainnewArrayList();bufferQueue.drainTo(remain);if(!remain.isEmpty()){flushToDB(remain);}}}代码解析双重触发机制仅仅判断数量是不够的如果流量突然低谷数据可能卡在内存里几分钟不落库这是 Bug。
必须加上poll(timeout)的时间兜底。
优雅停机生产环境服务重启频繁必须提供shutdown()钩子保证 JVM 销毁前把内存里的数据吐干净。
Ⅳ. 数据一致性CAP 理论的终极博弈高并发架构中最让人头秃的莫过于DB 和 Redis 的数据一致性。
网上盛传的“延时双删”Delete - Write DB - Sleep - Delete在极端高并发下依然会有概率脏数据且Sleep多久是一个玄学。
1 终极方案基于 Binlog 的异步更新Canal 模式与其在应用层纠结先删缓存还是先改库不如把缓存更新的逻辑从业务代码中剥离出来下沉到基础设施层。
方案逻辑业务代码只管写 MySQL完全不操作 Redis。
Canal阿里开源中间件伪装成 MySQL Slave监听 Master 的 Binlog。
一旦 MySQL 发生变更Canal 解析 Binlog 消息投递到 MQ。
消费服务订阅 MQ解析出变更的数据重放到 Redis 中。
2 架构图解Canal 旁路同步
Update/Insert
Binlog Replication
解析 Binlog
订阅变更
Upsert/Del业务应用MySQL MasterCanal Server消息队列 Kafka缓存同步服务Redis 缓存优点业务解耦业务代码里没有一行 Redis 操作代码清爽。
最终一致性只要 Binlog 不丢MQ 不丢缓存最终一定会一致。
防抖动如果同一条数据 1 秒内被改了 100 次同步服务可以在内存中合并这 100 次变更只写 Redis 一次Write Behind。
Ⅴ. 性能/稳定性分析架构师的体检表在设计完上述架构后必须进行自我拷问。
以下是针对该架构的性能瓶颈分析与优化对比关注维度潜在瓶颈/风险优化/兜底方案读性能Redis 成为单点瓶颈大 Key 导致网卡打满
上 Local Cache 分担热点
Redis Cluster 分片
开启多级副本读写分离 ||写性能| MQ 积压导致数据入库延迟 |
增加 Topic 分区数
消费者改为多线程并发消费
动态扩容监控积压阈值自动拉起更多消费者容器 ||一致性| Canal 同步延迟秒级用户刚改完刷新旧数据 |
强制读主在写完后的短期窗口内如500ms特定接口强制走 DB
接受现实大部分互联网业务接受
秒的数据延迟 ||可用性| 缓存雪崩Cache Avalanche |
Redis Key 过期时间设为 Random(TTL)
使用 Hystrix/Sentinel 进行熔断降级返回默认值 |Ⅵ. 实战案例复盘某信息流 Feed 系统的重构背景某社交 App 的 Feed 流系统用户数 500 万。
原有架构是App - Server - MySQL。
随着用户增长早高峰刷 Feed 流时数据库 CPU 经常飙升到 90%且写入评论经常超时。
重构步骤读优化推拉结合对于大 V粉丝 100万发帖时直接写入 DB粉丝拉取时再去查询拉模式避免写扩散。
对于普通用户发帖时异步写入所有粉丝的 Redis 收件箱推模式 / Timeline Cache。
落地效果读取 QPS 提升 20 倍DB 压力几乎降为零。
写优化聚合写入对于“点赞”这种高频低价值操作不再实时写库。
使用 Redis 的HyperLogLog或Hash结构在内存计数。
每分钟通过定时任务将 Redis 里的点赞数同步回 MySQL 持久化。
落地效果写入 TPS 从 2000 提升至 Redis 极限的 80000。
防穿透设计对于查询不存在的 Feed ID在 Redis 中缓存一个 Null Object过期时间 5 分钟防止恶意攻击穿透到 DB。
Ⅶ. 经验
总结系统性设计高并发读写架构不是堆砌组件而是做权衡Trade-off。
读流量要分层离用户越近越好能在 CDN 解决的别去 Redis能在 Local Cache 解决的别去远端。
写流量要缓冲不要把 MySQL 当作实时处理引擎把它当作最终持久化仓库。
MQ 和 Batch 是写性能的救星。
一致性要取舍除非是金融账务否则不要追求强一致性。
最终一致性是高并发架构的基石。
监控先行没有监控的架构设计就是盲人摸象。
Prometheus Grafana 必须覆盖 QPS、RT、Cache Hit Rate、MQ Lag 等核心指标。
架构设计没有银弹只有最适合当前业务阶段的方案。
希望这套**“过滤缓冲异构同步”**的组合拳能为你现在的系统重构提供思路。