☰
高并发系统架构设计:从流量入口到数据存储的完整方案
2026/10/6 3:02:10 网站建设 项目流程

这道题我当面试官的时候问过很多人,也帮别人模拟面试陪练过无数次。说实话,十个候选人里能让我点头认可的,不超过两个。不是因为大家不懂技术,而是大多数人把“高并发系统架构设计”答成了技术名词堆砌——Redis、MQ、分库分表、缓存、CDN,随口就来,但一问到“为什么”“扛多少量”“崩了怎么办”,立刻就卡壳。

面试官问这个题,归根结底就一件事:你在高并发场景下,有没有完整的、可落地的系统设计能力。这不是背几个组件就能糊弄过去的,需要的是从流量入口到数据存储的整体思考,是每一层选型背后的取舍逻辑,是出问题之后的兜底方案。这篇文章我就把这套东西完整拆开揉碎讲清楚,不管你是准备面试还是真在负责高并发系统,都能拿去直接用。

1. 面试官到底在问什么

1.1 面试官眼中的标准答案长什么样

很多人以为面试官想听一套标准架构:Nginx负载均衡、Redis缓存、RabbitMQ削峰、MySQL分库分表,觉得把这些名词一字排开就算答完了。但面试官心里其实在评估三件事。

第一,你有没有量化思维。你说你的系统高并发,到底多高?是每秒100请求还是每秒10万请求?不同量级下架构方案天差地别,100 QPS用一台服务器加个数据库就足够了,非要上一套微服务加消息队列,那是过度设计。面试官想听到你说出具体的数字,并且基于数字推导出方案。

第二,你有没有分层意识。高并发不是某一个组件能解决的,而是接入层、应用层、缓存层、数据层、异步层各司其职,每层解决一类问题。你上来就说“我用Redis扛”,那缓存穿透了怎么办?数据库写压力大了怎么办?消息队列堆积了怎么办?这些都不是单一组件能兜住的。

第三,你有没有工程落地能力。方案说完之后,面试官一定会追问细节:缓存和数据一致性怎么保证?消息丢失了怎么处理?分布式事务怎么做?接口的幂等性如何设计?你的方案如果只是PPT级别的概念,一问细节就露馅。

说白了,面试官要的不是一个正确答案,而是一整个思考过程。你如何从业务场景出发,分析瓶颈,权衡选型,最终给出一个可实施、可运维、可扩展的方案。这跟写代码是一样的,需求分析永远比编码本身更重要。

1.2 从并发量到架构方案的推导链

我把高并发架构设计拆成一条推导链,你只要沿着这条链走,逻辑就不会乱。

第一环是业务场景。你得先搞清楚是什么类型的系统。是电商秒杀,还是社交信息流,还是支付转账?不同类型的系统有不同的瓶颈。秒杀是读多写少、瞬时流量极高;信息流是读多写多、关注的是个性化推荐和延迟;支付系统则是数据强一致、绝对不能丢单。

第二环是流量预估。面试官问“你的系统能扛多少并发”,你不但要给出目标峰值,还要说明依据。比如你做了促销活动,预计100万用户参与,每人点击5次,集中在30分钟内,那么平均QPS大约就是100万乘以5除以1800秒,约等于2778。考虑到流量毛刺,你需要预留2到3倍余量,目标设计值大约就是8000到10000 QPS。

第三环是分层拆解。这些QPS打进来之后,每一层各自的承受能力是多少?Nginx单机可以扛5万以上,应用层单机Java服务大概在1000到3000 QPS(视业务复杂度而定),Redis单节点读QPS在10万级别,MySQL单库一般建议控制在5000 QPS以内。哪一层会先成为瓶颈,就在哪一层加对应的解决方案。

第四环是方案选型。缓存扛读,队列扛写,分库扛总量,限流扛毛刺。每一环的选型都要基于前面的量化和分层分析,而不是凭空堆砌。

这条推导链走完,你在面试官眼里就是“有系统思维”的候选人,而不是只会背名词的复读机。

1.3 常见错误:把架构设计答成技术名词堆砌

我统计过候选人答题的共性错误,排第一的,就是在没有量化、没有场景的情况下,上来就甩一堆组件名。

有人一开口就是“我们用了Spring Cloud微服务、Redis集群、Kafka消息队列、ElasticSearch做搜索、分库分表中间件”,听起来很唬人,但面试官接下来只要问一句“你这个Kafka主要用来解决什么问题,不用行不行”,很多人就答不上来了。

另一个很常见的错误是把“高并发”等同于“多线程”。确实,Java并发编程是高并发系统里的基本功,但线程池、锁、CAS这些解决的是单机内的并发资源竞争问题,属于微观层面的手段。架构设计解决的是宏观层面的流量分发、容错、扩展问题。你说你用了ThreadPoolExecutor处理任务,这很好,但它替代不了消息队列削峰填谷,也替代不了分库分表分摊存储压力。两边根本不冲突,但是层次完全不同。

还有一类人特别可惜,技术功底不差,但答得太散。他讲缓存能讲半小时,问他数据库压力大怎么办,他说“分库分表”。再问分了之后跨库查询怎么做,他不吭声了。这就是典型的缺少完整框架,抱着一个问题死磕,没法把整个系统串联起来。

所以你看,面试官问“高并发系统架构设计”,考的不是某一个技术点,而是你能不能从宏观到微观、从流量到存储、从正常到异常,把整个系统有条理地讲明白。下面我就按这个框架,把每一层的设计思路和核心细节展开讲。

2. 高并发架构的分层设计

2.1 接入层:流量入口的第一道闸门

先讲接入层,因为所有请求进来第一脚踩在这里。接入层要解决的问题不是业务逻辑,而是怎么把流量安全、均匀、高效地放进来。

负载均衡是接入层的基本功。DNS轮询、Nginx反向代理、硬件F5、云上的SLB,本质上都是把请求分发到多台服务器上,避免单点过载。Nginx作为七层负载均衡,天然支持HTTP协议层面的处理,比如URL路由、静态资源缓存、gzip压缩,还能直接做一些简单的限流配置。配合Keepalived实现VIP漂移,Nginx自身也能做到高可用。

比负载均衡更关键的是限流和熔断。限流的意思是,系统每秒只能处理1万请求,你就别放进来2万,多余的要么排队要么直接拒绝。常见的限流算法有计数器、滑动窗口、令牌桶和漏桶。令牌桶允许一定程度的突发流量,适合大部分互联网场景;漏桶则强制平滑流量,适合对突发流量敏感的下游系统。

熔断更接近一种保护机制。当下游服务出现故障、响应时间飙升时,接入层应该快速切断对该服务的调用,走降级逻辑,而不是继续傻等,把故障放大到整个调用链。Sentinel和Hystrix都是干这个事的,但Sentinel对Java生态支持更好,而且控制台做得完善,目前用的更多。

CDN也属于接入层的一部分,但容易被人忽略。像图片、CSS、JS这种静态资源,直接划给CDN处理就行了,完全不用打到应用服务器。很多高并发系统的实际经验是,静态资源占了整体流量的大头,用CDN一挡,应用层的压力直接少一半。

接入层还有一个容易被忽略的工作叫作安全防护。不管是恶意刷接口、搞缓存穿透,还是带恶意参数的请求,都应该在网关层做基础的校验和拦截。黑名单、白名单、IP限流、参数校验、鉴权,能前置的都前置,不要等流量打到业务代码里再处理。

2.2 应用层:服务拆分与无状态化

过了接入层,请求就到了应用层。这一层是整个架构的核心计算单元,也是高并发设计里最考验功力的地方。

应用层第一个关键设计是无状态化。什么叫无状态?就是你任意一台服务器,在处理同一个请求时,不需要依赖本地存储的会话数据。如果把用户登录信息存在服务器内存的Session里,那用户下次请求被负载均衡分到另一台机器就找不到Session了,这叫有状态,它要求负载均衡做会话保持,限制了系统的水平扩展能力。

解决方式是把会话状态外置。登录状态放到Redis或Token里,用户信息每次都去Redis取。这样所有应用实例都是平等的,流量来了随便分到哪台机器都能处理,想扩就扩,想缩就缩,这才是水平扩展的前提。面试官问“你们的服务怎么支持水平扩展”,如果你回答“把Session放到Redis”,这就答到点子上了。

服务拆分也是应用层绕不开的话题。一个系统一开始都是单体应用,所有代码凑在一起,开发效率高,但当流量上来之后,单体服务的启动时间、发布影响面、故障隔离能力都成了瓶颈。这时候就需要按业务边界拆微服务,比如订单服务、用户服务、库存服务、支付服务各拆一个模块。

服务拆分有几个原则:第一,按业务能力划分,而不是按技术层次划分;第二,服务之间通信优先选轻量级协议,内部服务可以用gRPC这类高性能RPC框架;第三,每个服务有自己独立的数据库,避免多个服务直连同一个库,否则拆了服务不拆库,等于白拆。

微服务带来的新问题也不容忽视。服务调用链变长之后,任何一个环节抖动都可能拖垮整个链路,所以必须引入全链路追踪(如SkyWalking、Zipkin)和分布式日志体系。再有就是配置管理,几十个服务的配置必须统一管理,Nacos、Apollo这类配置中心基本是标配了。

应用层内部还有一个微观层面的高并发话题,就是Java多线程编程。单个应用实例的QPS上限,取决于线程池配置、锁竞争强度、IO模型的综合表现。对于CPU密集型任务,线程数设成CPU核心数加一就够了;对于IO密集型任务,线程数可以设到CPU核心数的两倍甚至更多。这些参数的设置直接影响单机性能上限,也是面试时的高频追问点。

2.3 缓存与数据层:读多写少时怎么扛

高并发系统里,最宝贵的资源其实就是数据库。数据库的读写能力是有上限的,再怎么优化SQL、加索引,单库的支撑能力也就摆在那里。所以业界有一句老话:高并发系统的核心,就是尽量让请求不要打到数据库。

缓存就是做这件事的。整个缓存体系从快到慢可以分为几层:CPU缓存、进程内缓存、分布式缓存、数据库本身的BufferPool。在实际的架构设计里,最常用的是进程内缓存加Redis分布式缓存这两层。

本地缓存的优势是快,零网络开销,适合存放系统配置、基础字典这类变化极少的静态数据。Caffeine是Java领域本地缓存的首选,性能比ConcurrentHashMap自带方案好很多。但本地缓存有个硬伤,就是多实例之间数据不一致,而且更新时需要主动失效,所以在分布式环境下,本地缓存只能存不太敏感的数据。

Redis作为分布式缓存的标配,能扛的读QPS单实例就在10万以上,配合主从复制和哨兵模式还能提供高可用。但Redis不是万能的,它只是把数据库的压力转移到了自己身上,你照样得考虑缓存容量、淘汰策略、缓存一致性、缓存穿透等一系列问题。

我见过很多架构,Redis加了一大堆,数据库照样被压垮,因为缓存命中率太低了。缓存设计有一个黄金法则:热点数据才值得缓存。你要是把所有数据都塞进Redis,光序列化、内存占用就够你喝一壶,而且缓存命中率上不去,数据库该扛的压力一点没少。所以在设计缓存时,第一件事是把业务里真正的高频访问数据识别出来,针对性地设计缓存Key和过期策略,而不是一股脑全部缓存。

缓存和数据保持一致性问题,我会在下一章详说,这里先记住一个总原则:缓存是加速手段,不是数据源;数据库才是最终的事实来源,任何缓存数据都要能追溯到数据库,且允许在一段极短的时间内存在不一致。

2.4 异步化与削峰:把同步压力变成异步缓冲

高并发场景最典型的特点是流量具有突发性,峰值可能是平均值的几十倍。如果系统完全按峰值流量设计,资源浪费巨大;如果按平均值设计,峰值一来就全崩。异步化和削峰填谷就是解决这个矛盾的,核心工具就是消息队列。

消息队列的核心价值在于解耦和缓冲。当一个请求不需要立即拿到最终结果时,比如用户下单后发短信、扣积分、生成对账单,这些操作完全可以丢进消息队列,由下游任务慢慢消费。用户端只要得到“下单成功”的响应就够了,后面的流程异步去跑,返回更快,用户体验反而更好。

常见的消息队列选型有Kafka、RocketMQ、RabbitMQ,每家各有侧重。Kafka吞吐量极高,适合日志收集、数据管道这类场景;RocketMQ在金融和电商领域用得很多,支持事务消息,消息不丢失的机制做得比较完善;RabbitMQ功能丰富,适合业务复杂度高、对消息可靠性要求高但对吞吐要求没那么极端的场景。

消息队列有一个经典组合用法:前端请求写入消息队列之后立刻返回“排队中”,后端消费者按照自己最大的处理能力去消费,这样无论流量尖峰多大,后端服务都不会被打垮。这就是秒杀系统的核心设计思路——先把流量挡在MQ这一层,然后慢慢消化。

但引入消息队列不等于高枕无忧,你自己要面对三个新问题:消息丢失、重复消费、消费积压。

消息丢失要分三个层面看,生产端、Broker、消费端各管一段。生产端要确认消息是否成功写入Broker,失败则重试;Broker要开启持久化,避免宕机丢数据;消费端要等业务处理完成后再提交消费位点,而不是拉到消息就立刻提交。三层都做到位,才能基本保证不丢消息。

重复消费其实比丢失更常见。下游服务处理完消息,却因为网络抖动没来得及提交位点,消息就被重新投递了。这种场景下,幂等设计是关键,后面单独讲。

消费积压的应对策略倒是简单粗暴:增加消费者实例,提升消费速率;或者临时修改消费者的逻辑,把堆积的消息直接丢弃,靠后续的定时任务去补数据。但这些都是应急手段,最好的方式还是上线之前就要对消费能力做容量评估,别等到积压几十万条了才手忙脚乱。

3. 几个核心难点的实操方案

3.1 缓存穿透、击穿、雪崩的应对

缓存这块如果光说“用Redis缓存”,那等于没说。面试官一定会在缓存上深挖,因为缓存是生产环境最容易出问题、也最能体现候选人对细节把控能力的一环。

缓存穿透,指的是大量请求查询一个缓存中和数据库中都不存在的数据。因为缓存没有这个Key,请求全部打到数据库,数据库压力瞬间飙升。这种问题常见于恶意攻击,比如用一个不存在的用户ID批量刷接口。解决方案有几种:最基础的是把查询结果为空的数据也缓存起来,设置一个较短的过期时间,比如30到60秒;进阶方案是使用布隆过滤器,把所有合法ID先过滤一遍,不存在的数据直接挡掉,连数据库的边都不沾。

缓存击穿,说的是一个热点Key在缓存过期的瞬间,大量并发请求同时又打到数据库。这个和穿透不一样,穿透查的是不存在的数据,击穿查的是存在但刚好过期了的超级热点。解决方案:第一,对热点数据设置永不过期,由后台异步任务在快要过期时去刷新缓存;第二,在查询数据库时加互斥锁,让同一时刻只允许一个请求去重建缓存,其他请求等待缓存被重建后直接读取新值;第三,用逻辑过期替代物理过期,给缓存值加一个逻辑过期时间字段,读取时判断是否过期,过期则异步刷新并返回旧值。这三种方案各有适用场景,但最推荐的还是“逻辑过期加互斥锁”的组合,既能保证数据新鲜又不会击穿数据库。

缓存雪崩,是指大量的缓存Key在同一时间段集中过期,导致压力全部落到数据库上,数据库应声崩溃。最常见的诱因是缓存设置了相同的过期时间。破解方法很简单:在设置过期时间时,增加一个随机值,比如5到10分钟之间的随机数,避免Key在同一秒集体过期。另外,如果Redis集群本身宕机了,应用层也要有兜底策略,比如做缓存的多级部署,或者开启限流降级,防止所有请求直接打到数据库。

我把这三个问题放在一张表里,方便你对照记忆:

问题产生原因核心应对策略
缓存穿透查询的数据在缓存和数据库中都不存在空值缓存、布隆过滤器
缓存击穿热点Key过期瞬间大量并发打库永不过期、互斥锁、逻辑过期
缓存雪崩大量Key同时过期或Redis宕机过期时间加随机数、多级缓存、限流降级

3.2 数据库分库分表,什么时候分、怎么分

数据库的分库分表,是很多候选人分水岭式的话题。嘴上说容易,但能说清楚“为什么分、怎么分、分了之后怎么办”的人非常少。

先说什么时候该分。数据库的瓶颈有两种,一种是有大量的写并发,比如每秒几万次写入;另一种是数据量巨大,单表存储过亿甚至几十亿行,导致索引深度过大、写入变慢、备份和DDL都痛苦不堪。如果是第一种,优先考虑分库,把写压力分摊到多个库上;如果是第二种,优先考虑分表,把单表的数据量降下来。

分表和分库其实可以按维度组合。水平分库分表,就是按照某个关键的拆分键取模或哈希,把数据均匀分布到多个库和多张表里。比如订单表按用户ID取模分成64张表,分布到4个库里,每个库16张表,单表数据量立刻下降一个数量级。

拆分键的选择是分库分表方案里最重要的决策,没有之一。核心原则是:让大部分查询能路由到固定的分片。订单系统按用户ID分片,那么用户查自己的订单列表就只需要查一个分片;但如果运营后台要按订单号查所有用户的订单,就要在所有分片上并行查询,这就是跨分片查询问题。如果你同时需要按用户ID和商家ID查询,就得想清楚哪个是主拆分键,另一个维度通过冗余表或搜索引擎来解决。

分库分表之后,原来单库的简单操作都变得复杂了。自增主键不能用,因为多个分片会重复;全局唯一ID需要分布式ID生成器,常见方案有雪花算法、Leaf、Redis自增配合日期前缀等。跨分片的join不能直接执行,需要在应用层做二次聚合。跨分片的事务问题更是头疼,已经不能依赖数据库本地事务,需要引入分布式事务框架,比如Seata的AT模式或TCC方案。

还要考虑后续扩容的问题。如果一开始按取模分库,分了8个库,后面数据涨了要扩到16个库,迁移成本极高,几乎是重建索引、全量迁移。业界更推荐的做法是使用一致性哈希,或者在拆分键设计时预留未来分片数翻倍的余量。还有一种思路是使用中间件层的方案,比如ShardingSphere,把分库分表的逻辑封装在中间件里,应用层改动少,但它也有自身的性能开销和复杂度。

千万别一上来就分库分表。很多系统在数据量只有几百万、QPS只有几百的时候就开始拆库拆表,结果把自己折腾得疲惫不堪。务实的路线是:先做读写分离,一主多从扛读流量;不够了再做垂直拆分,按业务模块拆库;还是不够,才做水平分库分表。每一步都是被流量逼出来的,而不是为了架构而架构。

3.3 幂等设计与最终一致性

高并发系统一定会用到消息队列和异步化,而只要存在网络调用和重试机制,幂等就是一个绕不开的话题。

幂等的含义很朴素:同一个操作执行一次和执行多次,最终的结果是一致的。比如用户误触了下单按钮,发了两次请求,系统不应该创建两笔订单;支付回调被重复投递了三次,订单状态不应该被覆盖为错误状态。

接口幂等性设计最常用的方案是唯一键约束。在数据库表上加一个业务唯一键,比如订单号加商品ID的组合,第二次插入时数据库会因为唯一键冲突而拒绝。但对于更新操作,唯一键约束没法直接解决,更常见的做法是靠状态机控制。比如订单状态有“待支付、已支付、已发货、已完成”几个状态,只有当订单当前状态是“待支付”时,才允许支付回调把它改成“已支付”,如果已经是“已支付”了,再来一次回调就直接忽略。

分布式锁也经常用于幂等控制。对于同一个订单的并发操作,可以用Redis分布式锁保证同时只有一个请求能进入关键流程,其他请求等待锁释放后,发现状态已被处理,就直接返回。Redisson提供了现成的可重入锁实现,但在高并发下要特别注意锁粒度和锁超时时间,锁粒度太大容易排队,锁时间太短容易提前释放导致并发控制失效。

消息消费端的幂等同样重要。消费者从MQ拿到消息,处理成功后还没来得及提交位点,MQ就重新投递了这条消息。此时消费者应该能识别出这条消息已经被处理过,否则就会重复更新数据。解决方案有几种,比较常用的是在消息体中携带业务主键,消费时先查一下Redis或数据库,发现已处理就直接跳过。

最终一致性这个概念,很多人能说出“BASE理论”和“柔性事务”,但你要能落到实际设计中。比如支付系统,用户支付成功后,支付服务更新自己的支付订单表,然后发送一条支付成功消息给订单服务,订单服务消费消息后把订单状态改成已支付。这个过程里存在短暂的时间窗口,订单服务可能还没消费到消息,用户已经看到支付成功但订单还是待支付,这就是短暂的不一致。系统必须保证的是,这个状态最终会变成一致,并且在这个过程中不会有业务错误发生。

实现最终一致性的核心是消息投递的可靠性加上消费的幂等性。投递保证消息不丢,幂等保证重复投递不产生副作用,两者配合,最终一致性就有了落地基础。

3.4 大事务与锁的优化

数据库层面的另一个常见性能杀手就是大事务。一个事务包含大量SQL,执行时间长,持有的数据库连接和行锁锁定时长就长。在高并发下,大量线程会阻塞在锁上,造成连接池耗尽,系统整体瘫掉。

大事务常见的优化手段,第一是缩小事务边界。事务只包围必要的写操作,把读操作、远程调用、消息发送这类耗时操作移出事务。很多人习惯在Service方法上直接加@Transactional,结果一个更新操作中间调了一次外部HTTP接口,整个事务挂起两秒,这段时间内所有涉及同一行数据的操作全部排队。

第二是尽量避免事务中调用RPC或MQ发送。外部调用存在不确定性,网络抖动、超时重试都会延长事务时间。稳妥的方案是先提交本地事务,再发消息,如果消息发送失败则通过本地消息表或事务消息机制补偿。

第三是注意锁的粒度。悲观锁有性能瓶颈,很多读多写少的场景完全可以用乐观锁替代。比如库存扣减,用“UPDATE t SET stock=stock-1 WHERE id=? AND stock>=1”这种CAS式SQL,受影响行数为0就说明库存不足,不需要显式加锁,性能提升非常明显。

第四是热点行的处理。像秒杀场景,所有请求都会去update同一个商品的库存行,这个行就是热点行,再好的数据库也扛不住几百个并发同时更新同一行。业界常见的解法是把库存拆分成多个子库存,比如100个库存拆成10个库存桶,每个桶10个库存,请求随机路由到其中一个桶去扣减,这样单个行的写并发瞬间降了一个量级。

4. 一个真实场景的架构落地推演

4.1 场景设定:电商大促下的订单系统

光讲理论和框架,很多读者还是觉得隔了一层。所以这一章我带大家完整走一遍一个真实场景:电商平台大促期间的订单系统设计。

业务背景是这样的:平台要做一场促销活动,预期参与用户50万,活动开始后30分钟内是下单高峰,用户下单之后需要扣减库存、生成订单、通知仓库发货、推送给用户。支付部分由第三方支付系统承接,订单系统只需处理支付回调并更新状态。

先做量化分析。50万用户,假设平均每人点击5次,那就是250万次请求。集中在30分钟内发起,平均QPS大约是250万除以1800秒,约1400。但流量的毛刺效应很明显,第一分钟往往是请求高峰,按平均值的三倍估算,峰值QPS大约在4200左右。按照50%的余量设计,订单系统的目标承载量至少要到6000 QPS。

数据库的量级也很关键。30分钟内预计生成20万笔订单,单表20万数据量根本不算什么,数据库的压力其实不在数据量,而在瞬间写入并发。6000 QPS打到数据库上,MySQL大概率扛不住,所以这里必须设计缓存层和异步化,不能让全部请求都穿透到数据库。

4.2 容量估算与资源规划

基于上面的量化结果,我们来算算需要多少资源。

首先是应用层。假设单台应用服务器经过优化后能支撑1000 QPS(这是一个比较保守的估算值,实际要看业务复杂度),那么6000 QPS就需要至少6台应用服务器,再加上活动期间还要应对流量毛刺和单台故障后的快速替换,建议直接上8台,并配置好自动伸缩策略。这就是容量从哪来的答案,不是拍脑袋,而是从QPS和单机能力推导出来的。

其次是Redis层。下单过程中的库存校验、用户Token校验、防重复提交校验,这些高频读操作全部走Redis。6000 QPS的读操作,单节点Redis完全扛得住,但为了主从切换的高可用,至少部署一对主从。库存数据需要提前预热到Redis里,避免活动开始的一瞬间大量请求穿透到数据库。

然后是消息队列。库存扣减和订单创建应该异步化。用户下单请求先写MQ,后端消费者按数据库的承受能力把消息拉出来处理。假设消息消费者单机每秒能处理200条消息,那么20万笔订单除以1800秒,平均每秒只有111条,消费者单机就够用了,但考虑到活动高峰第一分钟的下单量可能是平均值的几倍,建议上3到4个消费者实例。

最后看数据库。20万笔订单的写入,按峰值每秒6000 QPS的穿透量算,单库根本扛不住。但通过MQ削峰后,真正打到数据库的写请求已经降到了每秒几百的级别,单库主从就能支撑。不过为了保险起见,可以把订单表按用户ID分成8张表,分布到两个库,每库4张表,这样单库单表的写入压力都在合理范围内。

这套容量规划做完,总体的资源需求是:8台应用服务器、一套Redis主从、3到4个MQ消费者实例、2个数据库实例。这个规模对中小型电商项目来说非常合理,既不会资源浪费,也能扛住活动压力。

4.3 缓存方案落地:库存预热与防超卖

库存是秒杀类系统最敏感的数据,也是考察候选人对数据一致性理解深度的最佳切入点。

库存的缓存方案是这样的:活动开始前,先把商品库存总数加载到Redis中,Key的设计可以是“stock:{商品ID}”,Value就是剩余库存数量。用户下单时,先通过Redis的原子操作做预扣减,比如用Lua脚本执行“DECR stock:{商品ID}”,如果扣减后的值大于等于0,说明库存还有,请求继续往下走;如果小于0,说明库存没了,直接返回“已售罄”。

这里的关键点是,库存扣减要用Lua脚本或Redis原生的原子操作,不能在应用层先GET再SET,因为并发请求下这两个操作之间会有时间窗口,极端情况下100个请求同时GET到库存还有1个,然后同时DECR,导致超卖。Lua脚本把检查和扣减在Redis内部完成,原子性就有保障了。

库存Redis扣减成功之后,再发一条“库存扣减成功”的消息给订单服务,由订单消费者真正去数据库里生成订单,并把Redis的扣减结果同步到数据库的库存表里。数据库层面的库存更新使用“UPDATE stock SET stock=stock-1 WHERE product_id=? AND stock > 0”这种条件更新,保证数据库最终一致且不会超卖。

如果有用户下单后又取消订单,那么需要异步把库存加回来,同时要处理Redis和数据库的一致性顺序。我的经验是先把数据库的库存加回来,再更新Redis缓存,这样即使Redis更新失败,一段时间后还能通过过期策略或后台任务补偿,数据库永远是最准确的数据源。

4.4 压测与瓶颈定位

方案上线前不做压测,等于裸奔。我自己经历过的惨痛教训是:某个活动方案预演得头头是道,实际压测到一半数据库连接池先被打满,才发现连接池参数从没调过。所以压测不是可选动作,而是高并发系统上线前必须做的一道工序。

压测工具有很多选择,JMeter是最常用的开源工具,支持图形化配置和分布式施压;如果团队对性能工程要求高,可以上Locust或者wrk。我个人的建议是,JMeter做业务场景压测,wrk做纯接口性能测试,各司其职。

压测过程分三步走。第一步是基线压测,在没有任何缓存和优化的情况下跑一轮,记录系统天然的处理能力,这一步是后续所有优化的对照基准。第二步是目标压测,开启缓存、异步、限流等全部优化后,按设计目标QPS压测,观察各层的响应时间、错误率、资源占用情况。第三步是极限压测,把压力持续提升,直到系统出现明显错误,记录这个临界点,它就是系统的真实上限。

压测中最常见的瓶颈定位方式,是在压测过程中同时看四个维度的指标:CPU利用率、内存占用率、GC日志、数据库慢查询。CPU如果跑满,多半是业务逻辑里有一次大量计算或者正则表达式等CPU密集型操作;内存涨得很快且GC频繁,多半是对象创建过多或本地缓存容量设置不合理;数据库慢查询增多,说明SQL走了全表扫描或索引失效;连接数耗尽,说明连接池参数设置有问题或者有连接泄漏。

压测还有一个容易踩的坑:压测结果和生产数据量级不一致。因为压测环境的数据库里可能只有几万条数据,而生产环境已经上亿条,同样的SQL在两种环境下的执行计划是不一样的。所以压测前一定要把数据量补齐到和生产接近的水平,否则压测结论没有任何参考价值。

5. 面试中最常见的六个致命错误

5.1 无脑堆组件,讲不清为什么

我在前面反复强调过,面试官最反感的就是背名词。你说你用了Redis、MQ、分库分表、微服务、读写分离,那每一个组件背后的业务场景是什么?选型时的对比评估是什么?不用它行不行?如果换一个方案会怎样?这些问题答不清楚,就说明你只是在“抄”别人的架构,没有自己的理解。

正确的方式是讲清链路。比如这样回答:这个系统的读请求占比90%,所以我们把读流量用Redis做了一级缓存,Nginx本地缓存做二级缓存;系统有突发流量,所以我们用MQ做削峰;数据库数据量预计两年后过亿,所以我们按用户ID做了水平分库分表。每一句话都对应一个业务诉求,这才叫架构设计。

5.2 缓存一致性一笔带过

“我用Redis缓存了订单数据,更新的时候先更新数据库再删缓存”,这个回答只能算及格。面试官一定会追问:删除缓存失败了怎么办?删了缓存之后正好有请求读到了旧数据怎么办?数据库更新成功了但Redis删缓存这个动作还没执行时,并发请求会不会读旧值?

完整回答需要做到三步。第一,更新操作先更新数据库;第二,删除缓存(注意是删除而不是更新缓存,因为更新缓存可能引入并发写覆盖问题);第三,如果删除失败,通过延迟双删或者监听数据库binlog的方式异步补偿删除。更稳妥的方案是使用Canal监听MySQL binlog,只要数据库数据变了就自动失效对应缓存,从机制上抹掉了手动删除的不可靠性。

5.3 只讲技术不讲成本

架构设计里没有银弹,任何方案都是有代价的。你用了分库分表,就要付出数据迁移、跨分片查询、分布式事务的复杂性;你用了消息队列,就要处理消息丢失、重复消费、积压告警;你上了微服务,就要面对链路追踪、配置管理、部署运维的复杂度。

面试时主动谈成本,会让面试官觉得你不只是个技术人员,还有工程判断力。比如你可以说:我们现在的量级其实用单体加缓存就够了,但考虑到未来半年的快速增长,我们提前把订单模块独立出来做成服务,这样后续扩展成本更低。这话一讲,说明你在架构和业务之间找到了平衡点。

5.4 忽略一致性,一谈到并发就崩溃

高并发和强一致本身就是矛盾的。很多候选人谈起高并发头头是道,一被问到数据一致性就闪烁其词,这会让面试官对你的方案产生巨大怀疑。因为在高并发架构里,一致性问题不是例外,而是常态。

你要主动解释清楚自己的系统属于哪一类一致性模型。电商订单系统这种场景,允许短暂的最终一致,但绝对不能超卖和丢单;支付系统需要强一致,涉及资金的扣减必须保证要么全成功要么全失败。先讲清楚业务的一致性要求,再选合适的技术手段,这才是有说服力的回答。

5.5 说不清容量判断和扩展路径

不少候选人能讲清楚当下系统的运行机制,但对未来的扩展路径完全没有概念。面试官喜欢问:“如果流量再涨十倍,你的系统哪一层会先撑不住?你怎么处理?”回答不上来,说明你只是在描述现状而不是设计系统。

正确的回答框架是:先判断瓶颈层。应用层可以先扩容加机器;Redis如果内存不足可以做集群分片;数据库如果读压力大就加从库,写压力大就分库分表;消息队列如果消费速度跟不上就加消费者实例。每一层都有对应的扩展手段,这才叫可扩展架构。

5.6 不关注监控与自动恢复

高并发系统不是说设计完了就能稳定运行,它需要被持续观测和维护。线上出一个问题,如果全靠人工去看日志排查,那说明你的系统还没有完善的监控体系。

成熟的方案是三点:第一个是指标监控,Prometheus加Grafana是标配,重点监控QPS、RT、错误率、JVM内存和GC、MySQL慢查询和活跃连接数;第二个是链路追踪,SkyWalking或Jaeger把一次请求的完整调用链串起来,定位慢节点和高延迟服务;第三个是告警通知,配合AlertManager,配置好阈值,告警通过短信、企业微信等渠道及时推给值班同学。再加一个流量防护组件Sentinel,配置好降级规则和熔断规则,能让系统在异常流量下自动保护自己。

6. 写在最后的个人经验

这个题目带过很多候选人,也陪跑过很多团队从单体到高并发的演进过程。我自己的体会是,架构设计的能力一定是在项目里磨出来的,不是在面试题库里刷出来的。

如果现在有人问我,准备高并发架构面试最有效的方法是什么,我的建议很简单:找一个你身边的真实业务系统,先画出它当前的架构图,标出每一层是什么组件、处理什么流量、有哪些隐患,然后尝试给它提出一套改进方案,最后推演改进之后各层能够承载的QPS变化。这套练习做下来,你对高并发架构的理解一定比背一百道题管用。

还有一个面试时很实用的小技巧:当你被问到架构方案时,别急着说结论,先说场景和数字。你说“我们这个业务峰值QPS大概五千,读多写少,库存类数据需要强一致”,比直接说“我用Redis加MQ”高级得多。面试官听的是你的分析链路,不是你的组件清单。

高并发架构没有一步到位的银弹,它是流量逼迫、技术选型、架构演进的长期过程。把每一层的原理吃透,把每一个方案的代价想清,在不断的实测和复盘里积累自己的判断力,这才是能陪你走很远的核心能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询