1. 一个接口被打挂之后:为什么单点优化救不了高并发
先说个我亲历的场景。有一年做活动大促,零点一过,商品详情接口的QPS直接冲上了三万,数据库连接池瞬间被打满,紧接着缓存里的热点key开始批量失效,请求穿透到MySQL,主库的CPU飙升到100%,整个商品服务全部超时。当时第一反应是加机器、加缓存、加连接池,结果加了四台应用服务器,问题只是往后推了五分钟,数据库一垮,全链路照样瘫。
那次事故之后我才真正想明白一件事:高并发从来不是某一个组件能扛下来的事情,而是缓存、队列、数据库这三层各司其职、按顺序接力的结果。缓存负责挡掉绝大部分重复读请求,队列负责把瞬时写入压力削平,数据库只处理真正需要落盘的请求。把这三者放在一起做组合优化,才是互联网系统扛住峰值流量的关键,而不是单点死磕某一层。
这篇文章我打算把我自己踩过的坑、压测验过的参数、以及从故障里总结出来的设计思路完整写一遍。适合正在做高并发系统设计、或者公司业务正被流量问题困扰的朋友参考,如果你是初中级后端开发或者刚接触架构设计,也可以把它当成一份组合优化的落地笔记来读,里面的很多细节我会尽量写到能直接照着做的程度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 组合优化的第一层:缓存不是"存数据",而是"挡流量"
2.1 Redis缓存到底在扛什么
很多人对缓存的认知是"把热点数据放到Redis里,查询走缓存,减少数据库压力"。这个理解没错,但太笼统了。实际在高并发场景里,缓存承担的是一个非常具体的任务:把99%的重复读请求拦在数据库之前。
我做过一次数据统计,在缓存健康的情况下,商品详情接口的访问量大约有97%会命中缓存,只有3%会落到数据库。这3%里还有相当一部分是缓存过期后的补偿请求。换句话说,数据库实际承受的流量只有入口流量的3%,这才是缓存真正的价值所在。
所以设计缓存时第一个要想清楚的,不是"存什么数据",而是"哪些数据值得用缓存"。热点商品、用户会话、配置信息、秒杀商品的库存状态,这类读多写少、频率高的数据才值得缓存。那种写入频繁、读取零星的数据,放进缓存反而会产生一致性问题,得不偿失。
2.2 三种缓存灾难的根因和防治方案
缓存层面常见的灾难有三种,我一个个说清楚,每一种都亲自修过。
第一种是缓存穿透。查询一个不存在的key,缓存没有,数据库也没有,请求每次都穿透到数据库。有人会拿空值也缓存来应对,这招有效但不彻底——如果攻击者用随机变化的key来打,你缓存空值根本来不及。更稳的方式是布隆过滤器,把所有可能存在的key先过滤一遍,请求来了先查布隆过滤器,如果判定不存在,直接返回,根本不会碰数据库。布隆过滤器有一个小概率误判的问题,但它只会把不存在的key误判成存在,最多代价是多查一次数据库,不会漏掉真实数据。
第二种是缓存击穿。某个热点key突然失效,一瞬间大量请求全部涌入数据库。这个场景下,关键在于多线程重建缓存时的互斥控制。我常用的方案是加分布式锁,让请求到达后只有拿到锁的线程去数据库加载数据并重建缓存,其余线程短暂sleep后重新读取缓存。比单纯设置"热点key永不过期"要安全,因为永久缓存会带来数据一直不更新的风险。
第三种是缓存雪崩。大量key在同一时间段集中失效,数据库被瞬间打挂。这属于"批量过期事故",应对策略通常有三板斧:过期时间加随机抖动,比如在一个时间段内随机分布;核心数据用逻辑过期兜底,即使物理key过期了,异步任务刷新时仍然返回旧数据;再就是多级缓存,本地缓存挡一层,Redis挡一层。这三招配合用,雪崩基本能摁住。
2.3 缓存一致性的落地做法
一致性是缓存设计里最容易扯皮的问题。先看清楚本质:缓存和数据库的数据不一致,本质上是因为"写数据库"和"更新缓存"这两个动作不是原子的。
我比较推荐的做法是先更新数据库,再删除缓存。为什么是删除而不是更新?因为更新缓存是一个计算和写入动作,在并发环境下先后顺序容易出问题;删除则是一个幂等操作,即使删除失败,下次查询会触发缓存重建,最多有一段时间的旧数据。而且删除缓存几乎不会像更新那样产生额外的状态依赖。
这里有个细节要注意:删除缓存失败怎么办?我的方案是在删除失败时重试几次,还不行就把这条消息扔到队列里,由异步任务补偿删除。很多团队在这一步就停了,觉得"MySQL更新成功、R
