
开篇先聊一个直击灵魂的问题高并发系统到底难在哪很多人第一反应是“加机器不就行了”但真把系统铺到千级并发、万级并发甚至更高的时候你会发现瓶颈根本不在单机的天花板而在整个系统的“木桶效应”——数据库连不上、缓存穿透、队列积压、接口超时、日志把磁盘打满每一处不起眼的短板都可能成为压垮系统的最后一根稻草。我这些年从几百 QPS 的小服务一路做到日请求量过亿的交易链路踩过的坑比写过的代码还多今天把整套“如何开发一个高并发系统”的实战方法论拿出来从架构设计、组件选型、数据拆分到压测、监控、治理一条线讲透适合正在做后端开发、系统架构设计或者正在经历“接口突然被打爆”的团队参考。很多人对高并发有一个误区以为只要用了 Redis、Kafka 就算高并发架构了。其实工具只是手段核心在于你对业务特性的理解和对流量的预估。我在接手一个千万级用户的活动系统时第一件事不是写代码而是去理清楚四个问题请求是读多还是写多峰值是平时多少倍数据一致性要求多高上下游依赖哪些服务这四个问题直接决定了整个系统采用什么样的架构形态。1. 高并发系统的整体设计思路先解决哪些问题1.1 从单体到分布式拆分是第一步高并发架构的一切起点是拆分。我见过太多团队拿着一坨几千行的 Controller 直接扛流量扛不住就想上缓存缓存穿透又去修代码最后整成一锅粥。正确的姿势是先把单体应用按业务边界拆成独立服务每个服务可以独立扩容、独立降级互相不影响。拆分的颗粒度有讲究。拆太粗比如只拆一个“用户服务”囊括所有跟用户有关的逻辑内部的慢 SQL 照样拖垮全局拆太细比如每个小功能都单独起一个服务光服务发现、配置管理和链路追踪的 overhead 就够你受的。我的惯例是按“业务能力域”拆比如拆成用户、商品、订单、支付、库存、营销六个域域之间通过 RPC 或者消息通信不直接共享数据库。1.2 缓存、异步、削峰应对高并发的三种标准动作高并发场景下接口性能的核心矛盾是“数据库处理能力有限但请求量远超处理能力”。解决问题的标准三板斧是缓存抗读、异步抗写、队列削峰。缓存抗读就是尽量让请求进不了数据库直接在 Redis 或者本地缓存里命中结果。适合缓存的数据有三个特征读多写少、数据短时间允许一定延迟、数据量不大。我做过一个配置中心的服务所有请求都依赖一组基础配置这组配置每天只变更两次但每秒要被读十万次这种场景把配置丢进本地缓存加上版本号校验简直是完美匹配。异步抗写是指把一些非核心链路、允许延迟的操作从同步流程中摘出去比如发短信、发券、写入日志、更新统计字段这种操作通过 MQ 异步消费能大幅度降低接口的 RT 和数据库的压力。队列削峰则是应对流量瞬间暴涨的经典方案秒杀、预约、热点事件这类场景用户请求集中涌入如果全部打到数据库必然雪崩但如果我们允许“先接受请求、稍后处理”把流量放到 MQ 里排个队消费端按照数据库的处理能力匀速消费问题就迎刃而解了。注意不是所有请求都能异步。用户下单后能否接受“支付成功通知延迟30秒”业务上要评估清楚通信层可以异步数据库事务必须是同步的。1.3 无状态设计与横向扩容的关系很多人都知道“加机器能扛流量”但机器加得上去的前提是应用做到无状态。什么叫无状态就是同一用户的多个请求落在任意一台机器上都能正确处理不依赖某台服务器的本地内存或磁盘文件。最典型的反例是 session 存本地内存用户第一次请求落在 A 机器下次请求被负载均衡转发到 B 机器发现 session 没了用户被强制下线。解决方式也简单session 放到 Redis 共享存储或者用 JWT 这类的 token 方案把状态塞进客户端让服务端彻底“失忆”。无状态设计带来的直接好处是扩容成本极低。活动开始前两小时压力测试顶不住了后台选中服务集群点一个“扩容”按钮等新机器拉起并注册到服务发现中心流量自动接入整个链路不需要重启也不需要改配置。这种从容是单体架构永远给不了的。2. 核心组件选型与部署为什么选它们2.1 Nginx LVS 负载均衡流量入口的第一道闸门入口层的目标是分流和防护。没有做过大流量的人往往会忽略这一层但实际在千万级 QPS 的入口场景下我们会把流量入口分为三层第一层是 LVS四层转发工作在传输层性能极高单机可以达到几十万并发连接一般是主备均衡部署做第一道流量入口。第二层是 Nginx七层负载均衡负责更细粒度的路由策略比如按 URL 路径转发到不同服务集群、做限流和防 DDoS 的简单规则过滤、以及 TLS 卸载。第三层是应用网关比如 Spring Cloud Gateway 或自研网关负责鉴权、灰度、动态路由、协议转换。部署上LVS 主备模式配合 Keepalived 做浮动 IPNginx 集群前置再有云厂商的负载均衡兜底。很多团队上云后直接用云 SLB 替代 LVS Nginx 组合这是合理的但内部环境下要清楚每一层的能力边界和故障转移路径。2.2 Redis 使用的三大坑穿透、击穿、雪崩Redis 是高并发系统的标配但用不好它反而会成为一个“放大器”——数据库没被流量打垮却被一个 Redis 故障引发全链路雪崩。这里必须区分三个概念缓存穿透查询一个一定不存在的数据每次都会打到数据库恶意刷接口时可能直接把 DB 打死。解决思路是布隆过滤器先把存在的 key 全量标记到 Bloom Filter查询前先过滤或者对空值也做缓存设置一个较短的过期时间。缓存击穿某个热点的 key 在缓存过期瞬间大量请求同时闯入数据库此时 DB 的 P99 可能瞬间飙升甚至挂掉。解决思路是加互斥锁即只有一个线程能去数据库加载数据其他线程等着缓存重建完成或者采用“逻辑过期”方案让 key 永不过期存一个过期时间戳发现过期时由后台线程异步重建。重建期间业务拿到旧值损失是可接受的。缓存雪崩大量 key 在同一时间集体过期请流量过去时数据库瞬间压力陡增。解决思路最简单过期时间加随机量比如原本 24 小时改成 24 小时加随机 1~5 分钟让失效时间均匀分散另外要重视缓存服务的 HA 部署Redis Cluster、Codis 或者云上 Redis 都应当选择主从加哨兵或集群模式。我自己的习惯是用中间件 SDK 封装一层“多级缓存”组件本地 Caffeine 做一级缓存、Redis 做二级缓存本地失效后查 RedisRedis 失效后查数据库同时用消息机制做本地缓存失效通知。这样的架构读性能极好但实现复杂适合并发量高到必须要榨干每一点性能的业务。2.3 消息队列选型Kafka、RocketMQ 还是 RabbitMQ消息队列在高并发系统里承担着“削峰填谷”和“应用解耦”两大职责。选型要结合团队的实际技术栈和运维能力对比项KafkaRocketMQRabbitMQ吞吐量极高百万级/秒高十万级/秒中万级/秒消息可靠性高需配置 ACK极高事务消息支持好高延迟毫秒级~秒级毫秒级微秒级顺序消息分区内有序支持全局/分区有序支持运维复杂度中依赖 ZK 或 Kraft中高简单典型场景日志、用户行为、大数据同步交易支付、订单状态变更内部系统通知、简单业务解耦我现在的经验是Kafka 适合做数据管道比如埋点日志、实时计算的数据采集RocketMQ 适合核心交易链路它的事务消息和消息重试机制在订单、支付场景下很省心RabbitMQ 适合轻量级场景团队规模小、消息量不大时用它成本最低。关于消息队列还有一个很多人忽略的关键点消费幂等性。分布式环境下MQ 会存在消息重复投递至少一次的投递语义如果消费者不做好幂等就会出现重复下单、重复发券的线上事故。幂等的实现方式一般有三种数据库表加唯一约束、Redis 里 set 一个消费标识、业务状态机上做状态流转校验。2.4 数据库层读写分离与连接池调优高并发系统的最终瓶颈有 90% 落在数据库。读写分离是成本最低的基础优化MySQL 主库负责写从库负责读通过主从复制把读流量分摊到多台只读实例上。但连接池参数必须调否则读写分离也没用。以 HikariCP 为例我常用的配置是maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000注意 maximum-pool-size 不是越大越好。PostgreSQL 的计算公式大约接近(核数 * 2 有效磁盘数)MySQL 可以略高但设成 200 个连接反而会加大数据库上下文的切换开销一般业务系统 20~50 就足够了。压测时记得监控线程池活跃数和活跃连接数找到最匹配压测流量模型的值。数据库优化的优先级应该是长期的先看慢查询日志加索引再考虑表结构优化和 SQL 改写最后才到分库分表。我见过太多团队第一个月就上了分库分表结果跨库 join 痛苦得要死而最简单的索引问题还没解决。分库分表是最后的手段它带来的复杂度增长是指数级的。3. 分库分表。核心方法论与实战过程3.1 分库分表的最佳时机与避坑原则数据量大到单表影响性能了这才考虑分库分表。业界一个经验阈值是 MySQL 单表行数超过 2000 万或者表容量超过 50GB但这不是绝对的实际判断依据是“单行数据宽度”和“是否热点行更新”。很多业务单表 5000 万依然跑得很快因为查询都走索引且有归档策略反而是某张只有 100 万行的订单表因为频繁 update 同一行导致锁竞争激烈反而需要拆分。分库分表前先做三件事确认历史数据能否归档到冷存储或历史库减少热表体积。优化查询模式看看能否通过冗余字段避免跨表查询。确认拆分键的选择这是切分设计最核心的决策点。3.2 选择正确的分片键订单表的实战拆解以单量过亿的订单表为例我们按user_id做水平分片分 16 个库、每个库 32 张表总 512 张表。分片规则保持简单稳定直接用 user_id 哈希后取模而不是用“按年月分表”——跨年查询的复杂度会让开发痛苦到怀疑人生而且订单数据天然属于某个用户按用户维度分片能让单个用户的所有订单集中在一起绝大多数业务查询都以用户为主体这正好覆盖了热点路径。分片键一旦确定就要接受两个限制不能再有跨分片的频繁 join需要从 SQL 层面规避。不能再用主键自增主流方案是雪花算法或者号段模式生成全局 ID。特别要小心的是“非分片键查询”。比如运营后台要按订单号查订单但分片键是 user_id怎么办我的方案是订单号本身就用雪花算法生成在取模后与 user_id 建立反向索引或者把订单号作为分片键但必须保证用 order_id 查的时候能定位到库。实际这里我更倾向于用“基因法”在订单号里编码分片基因这样从订单号可以直接计算出分片位置不需要二次路由。3.3 数据迁移的平滑方案双写与校验分库分表执行过程最怕的是“数据搬过去了业务挂了”。我有一套稳妥的五步走方案代码层加一个“双写开关”新数据同时写入旧库和新库以旧库为准。通过数据同步工具做历史数据的全量迁移。对每个用户做数据校验数量对不上就补偿重刷。验证没问题后把读写切到新库保留旧库只读。观察一段时间确认稳定后再将旧库下线。这套方案的核心其实是“可回滚”任何一步出了问题都能退回去。线上系统最怕的就是“只能进不能退”。4. 压测与监控。高并发系统上线前必做的事4.1 压测目标设定先定指标再谈工具压测不能上来随便拿 Jmeter 打一波看个数字就结束。你要先定义清楚三个指标QPS、响应时间、错误率。以我过去扛过的一个大促系统为蓝本目标通常是核心读接口QPS 20000P999 响应时间不超过 100ms。核心写接口QPS 3000P999 响应时间不超过 300ms。全局错误率不超过 0.05%不是 0因为网络抖动总是存在。压测工具有很多Apache JMeter 是老牌工具适合脚本维护Locust 用 Python 写压测脚本Go 的 wrk、hey 工具在纯接口性能测试时很轻量。但更贴合真实场景的是流量回放和压测平台线上拷贝真实的请求流量在预发环境放大倍数回放得出的压测数据才最接近作战数据。4.2 自己遇到的三个压测结果偏差陷阱压测最容易误导人的地方不在于工具多好用而在于压出的结果偏离真实。我总结三个高频偏差源第一压测流量是均衡的但真实流量是倾斜的。线上 90% 的请求都集中在 10% 的用户上缓存命中和热点分布完全不同压测时要模拟这种倾斜模型不能只做均匀分布。第二压测机器与线上机器性能不一致。如果你用一台 8 核的笔记本来压 100 台线上集群瓶颈可能在压测端而不是服务端压测结果会虚低。正确做法是先压“探针接口”校验压测水位或者直接使用云上性能测试服务确保压测机不被自身的资源限制拖住。第三没有压“灾难场景”。很多系统在正常流量下表现优秀但一个 Redis 节点抖动、一个下游超时整个系统就直接雪崩了。所以我习惯压测时故意引入延迟给下游加 200ms 的 sleep、模拟缓存节点杀掉一台看系统的隔离和降级逻辑是否如预期生效。这叫故障注入比单纯测极限 QPS 更有价值。4.3 监控三张核心大盘高并发系统的可观测性建设我只看三张盘简单直接第一张是业务大盘展示核心业务量比如订单量、支付成功量、DAU、转化率这是业务异常的第一感知来源。第二张是系统大盘展示各服务的 CPU、内存、线程池、连接池、GC 次数和延迟 P99配合 Prometheus 抓取指标和 Grafana 展示再配合链路追踪系统能快速定位到是哪个服务哪里点慢了。第三张是依赖大盘展示所有核心中间件Redis、MySQL、MQ的健康度特别要盯的是连接数和慢请求数以及缓存命中率。告警一定要分级而且要“够得着、不炸群”P0 告警短信电话系统宕机、成功率骤降P1 告警企业微信P99 超阈值、连接池告警、队列积压P2 定期汇总容量水位、慢查询新增。最忌讳的是把所有指标全部配上告警结果一天告警 1000 条团队逐渐麻木真正的问题反而被淹没。4.4 极限挑战全链路压测与流量染色最接近实战的检验方式是全链路压测。做法是在压测请求中埋入一个特殊标识通常是一个 header 或者参数链路各层的中间件“认识”这个标识压测流量在写到数据库时落到影子表、消费 MQ 时路由到影子 Topic不影响任何真实数据。全链路压测的价值在于把所有服务、数据库、中间件的真实表现联合起来验证模拟大促当天的完整流量沙盘。执行时还有一个很容易漏掉的环节依赖的下游第三方网关也会有容量上限比如短信供应商、支付网关。全链路压测时这些第三方服务不会配合你压测所以第三方依赖一律使用 Mock 服务只压自己的链路而第三方容量上限则通过合同保障和 SLI 在线上兜底。5. 常见问题与排查技巧实录5.1 CPU 飙升但 QPS 不高可能是内存泄漏遇到 CPU 高但流量不大第一个念头是查 JVM 线程栈。之前排查过一个诡异案例CPU 100% 但 QPS 只有几十线程 dump 一看几乎全部阻塞在日志框架的锁等待上。因为打日志的代码在并发下触发了 Log4j 的队列满同步落盘把系统拖垮了。解决方式是调整日志异步模式、精简日志级别。所以关键经验是高并发系统里 log 也可能是瓶颈线上环境日志级别必须控制在 WARN 以上大流量场景多方踩过坑。5.2 数据库主从延迟导致的数据不一致读写分离后写入主库的数据要从库要异步复制这个复制延迟在行量大时可能达几百毫秒。如果业务要求写入后立刻能查询到比如支付完成后立刻展示支付状态可以采取“强制读主”策略在写请求完成后的一个时间窗口内将用户的后续请求强制路由到主库。我常用的实现是 ThreadLocal 存标识网关解析请求头中的force-master: true结合路由中间件做到一键读主。5.3 缓存后同库的不一致问题这是高并发系统中被问得最多的问题。我推荐的最终方案是Cache Aside Pattern旁路缓存叠加延迟双删。流程是更新数据库 → 删除缓存 → 短暂等待约 500ms→ 再次删除缓存。第一次删除是让旧的缓存尽快失效第二次删除是为了解决“读线程把旧值写回缓存”的并发竞态。这种方式能控制不一致的时间窗在极短范围内。如果业务对一致性要求极高那就干脆放弃缓存直接读数据库再用性能更好的数据库硬件去扛。5.4 接口突然超时的处理思路先看负载均衡和网关所在的层有没有异常流量再看应用层的线程池有没有堆积线程 dump 看线程状态再跟进依赖的 Redis / MySQL / MQ 连接池。很多时候超时问题不在应用本身而是“某个下游拖慢了上游”这就要靠熔断器了。系统里我用过 Sentinel 和 Hystrix个人更推荐 Sentinel它把规则通过控制台动态下发限流降级实时生效。对每个依赖都设置一个合理的超时时间比如 Redis 50ms、DB 200ms、下游 RPC 500ms。宁可快速失败做降级也不要拖到全面超时打死自己高并发系统的韧性往往体现在“出现问题的时候能不能优雅地烂”。6. 一些额外想说的经验与细节实际上高并发系统的开发和普通业务系统的开发在技术手段上没有太多本质上的不同核心技术在多数场景都是“高可用架构 冗余建设 限流降级 监控告警”。但真正拉开架构师差距的是把这些手段组合起来应对实际业务的能力。针对很多刚起步的团队我心里有一条明确的路径建议先做大流量压测与容量规划再引入缓存与异步最后才考虑分库分表和微服务化。硬件的成本远低于架构复杂度的成本这是很多架构师过早地引入复杂技术后团队每天都在付出沉重运维代价的秘密。此外高并发系统不是一次性建成的。流量是涨上去的系统是演进出来的我们不要试图一步到位设计一个“完美的架构”而是设计一个“可以演进、允许容错、易于变更”的架构。能保守的时候就保守但是在几个关键点位——缓存、队列、限流、监控——要敢于提前部署。因为你永远不知道流量洪峰哪天到来而你被给予的备战时间往往只有几分钟最多一个晚上。