多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

高并发系统面试:从缓存穿透到连接池耗尽的实战拷问

高并发系统面试:从缓存穿透到连接池耗尽的实战拷问 面试官与水货程序员高并发系统的技术挑战我最近参与了团队的高并发系统专项招聘连着面了十几个候选人。简历上个个写着“精通高并发”“主导过千万级流量系统”结果一聊就露馅——有人把Redis当万能药有人说分库分表就是建100张表还有人对连接池的作用一无所知。最典型的是一个本科毕业三年的小伙子简历写得相当漂亮开场的技术栈也背得滚瓜烂熟结果我顺着一条业务链路问了几个“为什么”他额头上的汗就没停过。这个现象太普遍了。市面上讲高并发的资料一抓一大把但大多只讲“怎么搭”不讲“为什么”。面试官真正想验证的不是你会不会用某个中间件而是你面对流量冲击时能不能说清楚系统每一步都在干什么、瓶颈在哪里、为什么要这样取舍。这篇文章我想通过我实际面试里的一段对话把高并发系统核心的技术挑战摊开讲明白。既是给准备面试的人做参考也是给真正在做高并发的人一次对照自查。1. 开场三连问简历上的“高并发”到底有多少水分面试官看候选人简历第一眼盯的就是项目里的量级描述。这不是挑刺而是因为高并发系统的核心挑战本质上就是数字带来的连锁反应。1.1 从QPS到响应时间数字背后的真实含义我习惯先问一个极简单的问题“你说的日活10万那峰值QPS大概是多少你怎么估的”大部分候选人会愣住。好一点的会背出“QPS 活跃用户数 × 请求系数 / 峰值时间窗口”这种公式但算出来对不对、数值靠不靠谱心里完全没底。这里我分享一个常用的估算过程假设一个标准电商场景日活10万人平均一个活跃用户一天触发50次接口请求浏览商品、加购物车、下单、支付回调都算上分摊到每天4个小时的集中活跃时段总请求数 100,000 × 50 5,000,000 平均QPS 5,000,000 / (4 × 3600) ≈ 347 峰值QPS ≈ 平均QPS × 3 ≈ 1000左右这个数字不算夸张很多中小型电商系统真实峰值也就这个量级。但问题来了——一个系统号称支持“日活10万”实际后端代码如果在没有任何缓存、没有队列、同步调用三次数据库的情况下单机数据库QPS上限也就几百一旦业务促销流量翻倍系统直接雪崩。所以面试官听到“日活10万”时脑子里快速算出来的其实是“你的系统在什么环节替用户挡住了流量”。1.2 “中间件全家桶”不等于高并发能力关于高并发候选人身上最常见的通病是堆中间件。Redis、Kafka、RabbitMQ、Nginx能列的都列上。但当我追一句“你在里面承担了什么角色、遇到过什么问题”对话往往就冷场了。水货程序员有个标志性特征把中间件的存在本身当成方案。部署了Redis就认为缓存解决了用了消息队列就认为削峰解决了。他们不知道每个中间件都是双刃剑——Redis用不好会造成缓存穿透和雪崩消息队列用不好会导致消息堆积、顺序错乱甚至数据丢失。中间件不是护身符而是引入了一组新问题。我在面试里最常做的一个测试是让候选人在白板上画一张他做过系统的架构图然后指着一个点问“这里如果挂了会怎样”答得好的候选人三句话内能说清影响范围和数据链路答得差的连自己的图都圆不回来。这张图其实也推荐所有做后端的人自己画一画画得出来才算真做过。2. 第一个技术深挖你以为的缓存和真正的缓存策略是两码事不管什么业务场景高并发系统的第一道屏障都是缓存。但缓存不是“加一层Redis”这么简单。我面试时最喜欢在这个方向深挖因为这里藏得住真功夫。2.1 缓存三兄弟穿透、击穿、雪崩这三个词只要做过后端没人没听过。但很多人说不出它们之间的本质区别和针对性的解法。我用订单查询场景来说明。用户的订单表在数据库里按用户ID建立索引查询路径是“查缓存→未命中→查数据库→回写缓存”。三种异常情况是缓存穿透查询一个根本不存在的数据。比如一个订单号完全是随机乱造的缓存里没有数据库里也没有这时所有请求全部打到数据库。恶意攻击可以制造大量这种无效请求把数据库打垮。解法是布隆过滤器先把非法请求拦在缓存前或者把空结果也缓存很短时间。缓存击穿某个热点的key在缓存过期的瞬间大量并发请求同时涌入全部打到数据库。典型的比如首页推荐商品集中过期会导致瞬间流量冲击。解法是互斥锁只允许一个请求去查库回写缓存或逻辑过期。缓存雪崩大量key在同一时间段集中过期导致一波请求全部打到数据库。比击穿的范围更大。解法是给过期时间加随机扰动避免“整齐划一”地消失。这三种情况我在实际项目里都踩过。最惨的一次是活动大促当天把首页所有banner的缓存过期时间统一设定在零点零分零秒结果零点一到所有key同时失效数据库连接池瞬间被打满整个服务挂了15分钟。排查下来发现是当时图省事用了一个固定时间加上一层循环去重建缓存人一忙起来亲手埋了雷。2.2 为什么不直接提升数据库性能面试时我还会追加一个“反常识”的问题既然缓存是为了挡流量那我直接把数据库配置升级不也一样吗候选人如果说“加钱上更强的机器就行”基本就判死刑了。原因很简单再强的单机数据库也有天花板。一台高端机器可能扛住几万的QPS但用户量翻倍、活动力度加大时数据库的扩展成本是线性的每加一台机器只多一份性能。而缓存的命中率只要达到90%以上就能用相对小的代价挡住大部分请求——同样是抗流量缓存站的是“前端拦截”数据库升级是“后端硬扛”。用最直白的话说缓存的存在不是为了让数据库更快而是让数据库根本不用面对那么多请求。理解了这句话的人才算真正理解了缓存为什么是高并发系统的第一选择。2.3 实际业务里的缓存粒度设计还有一个容易暴露水货的点很多人只知道缓存Key对应的value是一个字符串但不知道缓存粒度怎么设计。比如商品详情页是把整个商品信息序列化放进一个Key还是拆成基本信息、库存信息、价格信息分别缓存前者在更新价格时整个缓存必须刷新后者可以做到只更新价格那个字段的缓存。但拆分的代价是缓存管理更复杂还要处理数据一致性。业内比较常见的做法是读多写少的基础数据商品标题、描述用长期缓存高频变化的数据库存、价格用短TTL缓存比如库存信息就10秒过期保证用户看到的价格和库存不至于滞后太久。这一层设计如果候选人没提我会主动引导一下能接上话的人才是真正在业务里做过缓存设计的人。3. 第二个深挖点数据库连接池耗尽是高频事故的根源高并发面试里数据库永远绕不开。但很多水货程序员对数据库的理解停留在我“用了MyBatis/ORM配置了连接池”这个层面。当我追问“连接池大小你设了多少、为什么”能答上来的人寥寥无几。这个现象在真实故障里特别常见。很多团队遇到过这个幽灵般的场景系统一直正常某天流量稍大日志里出现“GetConnectionTimeoutException”DBA看数据库负载其实不高CPU只用了30%但就是连不上。第一次遇到的人经常一头雾水最后发现不是数据库性能不够而是连接池被卡死了。3.1 连接池工作原理与大小设计数据库连接池的核心是复用连接避免每次请求都重新建立连接。理论上连接数越多系统处理并发能力越强但实际情况恰恰相反每个数据库连接都会占用服务端内存和CPU资源还会受数据库最大连接数限制。如果应用服务器的连接池设得过大比如每台机器配置100个连接10台机器就是1000个连接数据库得专门为连接维护资源能用来执行SQL的资源反而变少了。业内有个常用的经验公式连接池线程数 ((核心线程数 × 2) 有效磁盘并发数)举例一个8核CPU的机器本地磁盘并发写入性能按32算那连接池大小约等于(8×2)3248。但这个公式只作为起点真正的取值还得根据实际压测结果滚动调整。我们团队有过一个真实的优化案例某应用连接池从100调到40后总吞吐反而提升了近20%原因就是之前大量线程在排队等待数据库连接排队过程中又占用大量服务器的线程池资源。3.2 连接池耗尽时的应急三板斧真实的连接池耗尽事故往往发生在凌晨值班。我积累了一套应急链路在这里分享立即扩容应用实例数先把流量分摊开给连接池和数据库争取喘息空间。这是止血动作不是根因处理。降级非核心业务逻辑比如关闭数据统计上报、减少写日志的频率把稀缺的连接让给核心接口先跑。检查慢查询和长事务连接池耗尽的头号元凶常常是几个慢SQL长时间占住连接不释放。通过监控平台定位到跑了几十秒的查询语句立刻kill掉连接就能快速回补。这条路走完之后才是去修根因。现实中很多团队只走了第1、2步然后等系统自己恢复但慢SQL还在下次流量一起来照样出事。4. 消息队列的“削峰填谷”原理与面试必问的坑高并发系统里消息队列几乎是标准配置。面试官在这里喜欢追两个核心问题一是消息队列到底起了什么作用二是消息不丢失怎么保证。这两个问题能筛掉一大批背答案的人。4.1 削峰填谷的真实价值消息队列的削峰填谷用生活化的话说高峰期的请求就像地铁站突然涌进来一大波人如果每个人都要立刻通过闸机闸机一定过载。消息队列的作用是加一个等候区——闸机按自己的最大吞吐量慢慢放人过去保证站厅不被冲垮。在电商下单、秒杀、日志采集这类场景里消息队列把同步请求变成异步处理。客户端下单只返回“请求已受理”实际的库存扣减、订单落地、发货通知靠后台消费者慢慢消化。体验上用户可能多等几秒但对于下单系统这种需要强一致性的场景设置一个下游回调通知就解决了。我正在面试的那个“水货程序员”在解释消息队列作用时只说了一句“可以把请求放到队列里慢慢处理”。这其实是把他背过的答案念了出来他没有提到最终一致性没有提到消息积压的处理策略也没有提失败重试和死信队列。这些才是真正在使用消息队列时会面对的事情。4.2 消息不丢失的三段保障消息从生产者到消费者完整经过三个阶段生产者发送消息给Broker、Broker存储消息、消费者拉取并处理消息。每个阶段都可能丢数据。生产者阶段发送消息后必须确认Broker收到了。如果确认超时或失败要重发。很多人忽略“生产者确认”机制消息在网络上抖一下就丢了。Broker存储阶段消息不能只存在内存里必须落盘刷到磁盘。默认的异步刷盘模式在机器宕机时可能丢少量数据对金融对账等场景要开同步刷盘性能下降但数据绝对安全。消费者阶段消费者的核心误区是“先ack再处理业务”一旦处理后代码抛异常消息就丢掉了。正确做法是先处理业务、确认成功后再提交ack。这三段保障在面试里只要完整讲出来基本就能证明你真的是在消息队列里处理过数据而不是只会调API。5. 数据库水平拆分分库分表不是“建100张表”高并发继续往下走缓存、队列都挡不住了只能动数据库本身。分库分表是个听着简单、实操极容易翻车的领域。面试时我特别喜欢问“你们分库分表的分片键是怎么选的”不少人直接回答“用户ID取模”再追问一句“取模之后怎么扩容”就卡住了。5.1 分片键的选择逻辑分片键决定了数据分布是否均匀。选择的核心原则就一条能覆盖80%以上核心查询条件的字段才适合做分片键。用户体验用的是“用户ID维度”查询订单查询走的是“用户ID 订单ID”所以按用户ID取模是合理的。但有些候选人把订单号直接取模分库结果客服查单按用户ID来查时必须广播到所有库去查性能反而严重下降。这种错误在业务初期不会暴露等数据量上来了就是一场灾备级别的改造。选择分片键还有一层隐含逻辑如果核心查询条件是用户ID或订单ID两种就考虑“用户维度分库 订单维度分库”的双写方案或者用全局ID生成服务比如雪花算法在查询侧用映射表解决。这些权衡不是背题能背出来的必须真实趟过数据增长的坑才做得出来。5.2 扩容时的“搬家”难题分库分表最痛苦的时刻是扩容。假设一开始按用户ID取模分4个库用户量涨到一定程度需要扩到8个库。如果直接改分片规则老数据要找出来重新分布这个过程中服务还不能停。业界常用的方案有“双写双读”过渡以及更工程化的“不停机迁移”旧库继续服务同时后台任务把历史数据按新规则同步到新库切换时先切读流量验证再切写流量。整个流程涉及灰度发布、回滚预案、数据校验任何一个环节没考虑到位都可能造成数据错乱。我自己做过一次分库扩容计划了一周上线前三天每天加班到凌晨。最大的教训是永远不要用“先均匀分布再按需调整”的乐观思维面对数据迁移一定要在测试环境用真实数据量预先演练三遍最好顺便把迁移脚本中的数据校验部分写全否则不可逆的变更一旦出问题只能靠备份恢复代价极大。6. 面试官最后的杀招给你一个业务怎么一步一步扛住百万并发面试进行到后半程水货候选人基本已经出汗了。我最后会出综合题模拟一个从零搭建的高并发系统一个全新的社区产品用户量预估半年内能到千万日活百万核心场景是关注列表和动态流。你怎么设计这种题没有标准答案但它能测试一个人的架构思维。合格的回答应该从以下方向展开至少画出数据链路客户端 → 网关 → 业务服务 → 缓存 → 数据库/消息队列。能说清每一层的容量和瓶颈网关层承担限流、鉴权、灰度缓存扛90%读请求写请求进消息队列异步落地。预判未来扩展服务无状态化随时可以水平扩展实例数据库先单库读写分离数据量到一定程度再做分片。回答中需要体现的正确高并发理念没有一套架构能一步到位高并发能力是一步一步被流量逼出来的。初期单机能扛住一万QPS就可以上线通过监控识别瓶颈逐步加缓存、加队列、分库分表。很多人幻想一开始就完美的架构实际业务里这反而是项目推进的最大风险。6.1 压测数据是架构设计的分水岭面试到这一环节水货和真货的差别非常明显。真正的从业者会主动提到压测数据比如“我们做完这个优化后接口TPS从2000升到8000平均延迟从150ms降到45ms”。数字会说话因为它代表你不仅做过设计还真的验证过效果。我给候选人一个提示方向问“你怎么证明你的设计有效”绝大多数水货会沉默。真正有经验的人会给出这样的实践链路先搭一个最小闭环用压测工具模拟流量观察系统瓶颈在哪里CPU内存数据库连接数针对性做优化再压一遍。这个过程循环迭代每次提升都是真实数据驱动的。7. 面试之外的反思高并发能力是怎么长出来的从这场面试走出来我最大的感受是高并发系统的技术挑战从来不是某一个技术点有多难而是你能不能把一堆技术点串成一条完整的链路并且在任何一个环节出现问题时都有对应的预案。很多水货程序员的通病是背了一堆高并发的名词却没有经历过真实的故障场景。缓存穿透、连接池耗尽、消息积压、分片扩容任何一个都可能让系统在半夜叮叮当当响个不停。扛过这些事故的人说话时是有底气的因为他们知道那句“加个Redis就能解决”背后还有命中率、过期策略、重建流程和降级方案一整套事情要操心。我给正准备面高并发岗位的人一个建议不要只准备“怎么搭”多问问自己“如果这里是瓶颈怎么办”。在面试里能把这句话接得住的人比背完所有中间件文档的人值钱得多。
返回列表