从一次 HTTP 请求到高并发系统:Java Web 架构的六阶段演进 | 星雨笔录
一次 HTTP 请求如何从浏览器抵达 Spring MVC,再经 Controller、Service、Mapper 和 MySQL 返回响应?当请求从一个变成多个、从单机并发演进到集群和分布式系统时,线程池、数据库连接池、事务与锁、Redis、Nginx、消息队列、读写分离以及微服务分别解决什么问题?本文以 Java 全栈开发为主线,沿着“单次请求 → 单机并发 → 正确性与性能瓶颈 → 多实例集群 → 数据与异步扩展 → 微服务治理”六个阶段建立完整认知,并给出可验证的实操路线。
本文定位:这是一篇架构认知总览,而非照着安装一堆中间件的部署手册。目标是弄清楚:压力从哪里来、瓶颈出现在哪里、技术怎样解决问题、又引入了什么新问题。后续可把六个阶段各写成一篇专题。
适用基线:常见的 Spring Boot + Spring MVC + 内嵌 Tomcat + MyBatis + MySQL 单体应用。文中的配置与流量数字均用于阐明原理,不是可直接套用的生产容量承诺。
一、先看全貌:六个阶段到底在演进什么?
阶段 1 单次请求:一次数据从哪里来、经过哪里、去往哪里?
│ Spring MVC / Controller / Service / Mapper / MySQL
▼
阶段 2 单机并发:多个请求怎样同时推进?
│ Tomcat 工作线程 / 并发 / HikariCP 连接池
▼
阶段 3 正确性与性能:多个请求竞争资源怎么办?
│ 事务 / 锁 / MVCC / 幂等 / Redis / 限流
▼
阶段 4 应用扩展:一台机器确实不够了怎么办?
│ Nginx / 负载均衡 / Spring Boot 多实例集群
▼
阶段 5 数据与异步:数据库和下游服务成为瓶颈怎么办?
│ SQL 优化 / 读写分离 / 分片 / RabbitMQ、Kafka
▼
阶段 6 大型系统治理:很多团队、服务和机器如何协作?
微服务 / 网关 / 服务发现 / 熔断 / 可观测性
特别说明:这是一条学习路线,不是软件必须遵循的升级顺序。 有的单体应用先引入缓存;有的系统先部署两个实例;有的业务永远不需要微服务。技术选型必须由业务需求和真实测量决定。
1.1 先区分四个很容易混淆的概念
| 概念 | 一句话解释 | 直观例子 |
| -------------------- | ------------------------------ | ------------------------------------- |
| 并发(Concurrency) | 多个任务在同一时间段内交错推进 | 100 个 HTTP 请求都在处理或等待 |
| 并行(Parallelism) | 多个任务在同一时刻实际执行 | 多核 CPU 同时运行不同计算任务 |
| 吞吐量(Throughput) | 单位时间完成多少任务 | 每秒成功完成 800 次请求,即约 800 RPS |
| 响应时间(Latency) | 一个请求从发起到完成耗费多久 | 平均 120 ms,P95 为 300 ms |
还要明确:在线用户数 ≠ 同时请求数 ≠ 每秒请求数 ≠ 数据库连接数。一万人登录网站,不代表一万人在同一毫秒都发出了请求,更不代表必须准备一万个数据库连接。
1.2 理解吞吐量、响应时间和在途请求数
在系统稳定、统计口径一致时,Little 定律可以写成:
平均在途请求数 L ≈ 完成吞吐量 λ × 平均停留时间 W
例如:1000 请求/秒 × 0.2 秒 ≈ 200 个在途请求
这里的 200 是平均在途请求数,不一定是 200 个工作线程。不同的网络模型、异步机制和队列结构,决定了请求与线程如何对应。
如果服务端响应越来越慢,即便请求到达率不变,在途请求和排队压力也可能持续增长。这解释了为什么“数据库突然慢了”,整个接口层也可能跟着超时。
二、阶段一:从一次 HTTP 请求开始——打通完整执行链路
2.1 一个真实例子
用户在 Vue 页面点击“查看文章”,Axios 发出:
GET /api/articles/1001 HTTP/1.1
Host: example.com
Accept: application/json
在典型的 Spring Boot 单体项目里,概念链路如下:
Vue / Axios
↓ HTTP/HTTPS
Nginx(如果部署了反向代理)
↓
Tomcat:接收连接、解析 HTTP、调度请求
↓
Servlet Filter(过滤器,可有多个)
↓
DispatcherServlet(Spring MVC 前端控制器)
↓
HandlerMapping(查找处理器和对应拦截器链)
↓
HandlerAdapter(完成参数绑定并调用处理方法)
↓
Controller:接收参数,调用业务
↓
Service:执行业务规则、协调事务
↓
Mapper / MyBatis:生成并执行 SQL
↓
JDBC / 连接池 → MySQL
↑
Mapper → Service → Controller
↑
HttpMessageConverter / Jackson:对象转 JSON
↑
HTTP 响应 → 浏览器页面更新
这里的 Mapper XML 不是每次调用都必须经过的固定一层:MyBatis 既可以使用 XML 映射,也可以采用注解等方式定义 SQL。并且不是所有请求都需要查数据库,例如健康检查、缓存命中或纯计算接口。
2.2 用最小代码串起来
// Controller:负责 HTTP 入口
@RestController
@RequestMapping("/api/articles")
public class ArticleController {
private final ArticleService articleService;
public ArticleController(ArticleService articleService) {
this.articleService = articleService;
}
@GetMapping("/{id}")
public ArticleVO getArticle(@PathVariable Long id) {
return articleService.getArticle(id);
}
}
// Service:负责业务
@Service
public class ArticleService {
private final ArticleMapper articleMapper;
public ArticleService(ArticleMapper articleMapper) {
this.articleMapper = articleMapper;
}
public ArticleVO getArticle(Long id) {
Article article = articleMapper.selectById(id);
if (article == null) {
throw new IllegalArgumentException("文章不存在");
}
return new ArticleVO(article.getId(), article.getTitle());
}
}
SELECT id, title
FROM article
WHERE id = #{id}
ArticleMapper 接口、Article、ArticleVO 及异常处理代码在此省略;示例旨在说明层次职责。
2.3 这个阶段一定要回答的五个问题
HTTP 数据放在哪里? Path、Query、Header、Cookie、Body;JSON 与 multipart/form-data 是请求体的不同表示方式。
Spring Boot 和 Spring MVC 是什么关系? Spring Boot 负责应用启动与自动配置等便利能力;Spring MVC 负责 Servlet Web 请求处理框架。
谁真正把请求分配给 Controller? DispatcherServlet 协调处理流程,通过 HandlerMapping 与 HandlerAdapter 等组件完成匹配、调用。
Service 为什么单独存在? 把业务规则从 HTTP 协议细节和 SQL 实现中分离,便于测试、复用和维护。
响应如何产生? Controller 返回 Java 对象,合适的消息转换器可将其序列化为 JSON,再作为 HTTP 响应体输出。
阶段结论:一次请求是一条执行路径。 下一步不是重新发明 Controller,而是研究:多个执行路径为什么能够同时存在?
参考:Spring MVC DispatcherServlet、Spring MVC 请求处理流程。
三、阶段二:从单次请求到多个并发请求——线程池和连接池
3.1 三个用户同时请求,会发生什么?
假设用户 A、B、C 几乎同时调用同一个接口。在典型同步阻塞的 Spring MVC + Tomcat 工作模式下,Tomcat 可以安排不同的工作线程来处理这些请求:
请求 A → Tomcat 工作线程 T1 → Controller → Service → SQL
请求 B → Tomcat 工作线程 T2 → Controller → Service → SQL
请求 C → Tomcat 工作线程 T3 → Controller → Service → SQL
↓
竞争共享资源
线程是 JVM 中承载程序执行的单位之一,并不是每个用户固定绑定一个线程。 浏览器保持 TCP 连接也不等于一个连接始终独占一个工作线程。以常见同步处理为例,线程在执行业务、等待同步数据库调用时通常仍被占用;不同的异步或虚拟线程模型另有差异。
3.2 Tomcat 工作线程池(Thread Pool)解决什么?
建立和销毁线程需要成本。Tomcat 将工作线程作为可复用资源管理,让请求在可用线程上执行。超过处理能力时,可能发生等待、排队、超时或拒绝连接。
这涉及三个概念:
maxThreads:Connector 可用工作线程的上限之一;
maxConnections:同时接受和处理连接的数量上限;
acceptCount:操作系统连接等待队列相关的配置。
三者不等价,更不是“把 maxThreads 设为 1000,接口就能达到 1000 RPS”。还要考虑连接复用、队列、CPU 和下游等待。参见 Tomcat HTTP Connector。
3.3 HikariCP 数据库连接池(Connection Pool)解决什么?
数据库连接不是普通的 Java 对象,它关联真实的数据库会话及资源,建立和复用都需要管理。
请求线程 T1 ──┐
请求线程 T2 ──┤
请求线程 T3 ──┼── 获取/归还 JDBC Connection ── MySQL
请求线程 T4 ──┤ HikariCP
请求线程 T5 ──┘
假设有 100 个正在处理的请求,但连接池最大只有 20 个连接。当连接全部被借走时,后续需要连接的线程可能等待;超过 connectionTimeout 后,获取连接失败。线程池与连接池各自限制不同资源,不能互相替代。参见 HikariCP 配置说明。
仅演示参数含义,不是通用生产调优模板
server:
tomcat:
threads:
max: 200
spring:
datasource:
hikari:
maximum-pool-size: 20
connection-timeout: 3000
为什么不是把连接池调得越大越好? MySQL 自身也受 CPU、锁、I/O、内存和执行计划限制。连接数过多可能加剧争用;连接池大小必须根据 SQL 耗时、数据库容量、应用实例数和压测数据确定。
3.4 一定要理解线程安全
Spring 中的 Service 通常是单例 Bean。多个线程可以同时执行同一个 Service 实例的方法。
@Service
public class WrongCounterService {
private int count = 0; // 共享可变状态
public void increase() {
count++; // 不是线程安全的原子操作
}
}
count++ 包含读、加、写,可能发生更新丢失。真实业务计数往往应根据一致性要求采用数据库原子更新、Redis 原子命令或其他并发协调方式,而不是把重要状态简单放在 Service 成员变量中。
阶段结论:并发带来执行能力,也带来等待与共享状态竞争。 下一阶段要同时研究“正确性”和“性能”。
四、阶段三:并发造成竞争与瓶颈——事务、锁、MVCC、缓存和限流
4.1 正确性问题:100 人争抢 1 件商品
设库存 stock = 1。一种错误的业务逻辑是:
请求 A 查询库存 → 得到 1
请求 B 查询库存 → 得到 1
A 判断有货 → 创建订单
B 判断有货 → 创建订单
A 更新库存为 0
B 更新库存为 0
结果可能是 产生两笔成功订单,但库存只扣减一次。这叫竞态条件(Race Condition)。
正确思路之一:把“判断库存是否充足”和“扣减库存”合并为数据库中的条件更新。
UPDATE product
SET stock = stock - 1
WHERE id = 1001 AND stock > 0;
在 MyBatis 中检查受影响行数:等于 1 才表示本次成功扣减。订单创建与库存扣减应纳入适当的数据库事务边界,保证发生失败时按预期回滚。
@Transactional
public void createOrder(Long productId) {
int affected = productMapper.decreaseStock(productId);
if (affected != 1) {
throw new IllegalStateException("库存不足");
}
orderMapper.insertOrder(productId);
}
这里假设两个 Mapper 在同一事务管理器管理的数据库事务中执行,并省略实际的用户、金额、订单主键和唯一约束等字段。默认 Spring 事务对运行时异常回滚;它也不是万能的跨服务分布式事务。参见 Spring 事务文档。
4.2 相关技术怎么分类?
| 技术 | 主要解决什么问题 | 典型应用 |
| ---------- | ------------------------------------------ | ------------------------------ |
| 数据库事务 | 多个数据库操作作为一个逻辑工作单元 | 创建订单 + 扣减库存 |
| 悲观锁 | 获取锁后排他地修改共享数据 | SELECT ... FOR UPDATE |
| 乐观锁 | 更新前检测版本是否变化 | UPDATE ... WHERE version = ? |
| MVCC | 通过多版本支持一致性读取、改善读写并发 | InnoDB 一致性普通SELECT |
| 唯一约束 | 从数据库层阻止重复记录 | 相同支付流水号只允许一条 |
| 幂等性 | 同一业务请求重复提交时仍得到期望的业务效果 | 网络重试不能重复扣款 |
| 分布式锁 | 跨应用实例协调对共享资源的操作 | 必须跨节点互斥的特殊业务 |
锁、MVCC 与事务不是同义词。 InnoDB 的普通一致性读和锁定读行为不同;锁的范围还与索引、查询条件和隔离级别有关。参见 InnoDB 锁、一致性读。
4.3 性能问题:同一篇热门文章被反复读取
用户 A、B、C 都请求文章 1001,如果每次都访问 MySQL,就会产生重复查询。对于热点、读多写少、允许一定缓存时效性的数据,可采用 Redis 旁路缓存(Cache-Aside):
GET /api/articles/1001
↓
查询 Redis
/ \
命中 未命中
↓ ↓
返回缓存 查询 MySQL
↓
写入 Redis(带 TTL)
↓
返回
读取:先查 Redis → 未命中查 MySQL → 回填缓存
写入:先更新 MySQL → 再按策略删除或更新缓存
注意:缓存会引入数据过期、缓存击穿、缓存雪崩、缓存穿透、数据库与缓存一致性等新问题。Redis 不能取代数据库中对重要业务状态的正确性约束。参见 Redis Cache-Aside 官方说明。
4.4 保护问题:如果请求速度超过处理能力?
假设应用稳定处理能力低于突发流量,若无限排队,请求可能越积越多。此时可采用:
限流(Rate Limiting):限制用户、IP 或接口的请求速率;
超时(Timeout):限制请求等待下游的时间;
隔离(Bulkhead):避免一个慢业务耗尽所有工作资源;
熔断(Circuit Breaker):下游连续故障时快速失败;
降级(Degradation):暂时关闭非核心功能,保住关键链路。
超出限流额度时常见状态码是 429 Too Many Requests;服务器暂时不可用时可能是 503 Service Unavailable。具体状态码取决于失败原因和 API 设计。
阶段结论:高并发首先要求数据正确,其次才是速度。缓存解决一类重复读问题,限流解决过载保护,但两者都不等于无限扩容。
五、阶段四:单台机器不够——Nginx 负载均衡与应用集群
5.1 什么时候考虑加机器?
假设经过 SQL 优化、缓存、线程池与 GC 排查,单台应用服务器的 CPU、内存或处理能力仍然接近上限,才有较充分的理由横向扩容(Horizontal Scaling)。
纵向扩容:把一台机器从 2 核升级到 8 核;
横向扩容:部署多个 Spring Boot 实例,由负载均衡器分发请求。
浏览器 / APP
│
▼
Nginx(入口)
/ \
▼ ▼
Spring Boot A Spring Boot B
\ /
▼ ▼
Redis / MySQL / OSS
两个实例可以运行同一套完整单体应用代码。因此:
多实例集群 ≠ 微服务;单体应用也完全可以是分布式部署。
5.2 Nginx 怎样将请求分给不同实例?
upstream backendcluster {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
}
server {
listen 80;
location /api/ {
proxypass http://backendcluster;
}
}
这是一份演示 upstream 和 proxypass 的简化片段;实际生产还需要 HTTPS、转发头、超时、健康检查、日志等配置。Nginx 可采用轮询、最少连接等策略。参见 NGINX HTTP Load Balancing。
5.3 为什么“增加一台服务器”不是结束?
多个实例之间会出现共享状态问题:
| 新问题 | 原因 | 常见解决思路 |
| ------------------------------------ | ---------------------- | ------------------------------ |
| 用户 A 在实例 1 登录,下一次到实例 2 | 本地 Session 不共享 | Redis Session 或无状态认证方案 |
| 实例 1 保存头像,实例 2 找不到 | 文件保存在各自磁盘上 | 对象存储 OSS / 共享文件存储 |
| 两台机器缓存内容不一致 | 各自维护本地缓存 | 统一缓存策略与失效机制 |
| 两个实例共同拖慢 MySQL | 下游数据库仍是一个瓶颈 | 控制总连接数、优化查询与缓存 |
| 一个实例崩溃 | 故障可能影响部分请求 | 故障检测、流量摘除、必要时重试 |
例如每台实例 HikariCP 上限 20,扩到 5 台,理论上应用侧可同时建立的数据库连接上限就可能达到 100(不含其他客户端)。因此扩容应用服务器必须重新审视数据库容量。
阶段结论:集群扩展的是应用执行能力,不一定同时扩展持久化能力。 当瓶颈转移到数据库、邮件、AI 生成等下游服务时,要进入下一阶段。
六、阶段五:数据库与耗时任务成为瓶颈——读写分离、分片和消息队列
这个阶段有两条彼此独立、可以按需组合的路线:扩展数据访问能力,以及改变任务处理时间结构。
6.1 先做 SQL 优化,不要一遇到慢查询就分库分表
常见排查顺序:
慢接口
↓
查调用日志、慢 SQL、EXPLAIN 执行计划
↓
检查索引、返回列、查询条件、分页方式
↓
检查事务时间、锁等待、连接池和数据库 CPU/I/O
↓
确有必要才引入 Redis、读副本或数据分片
例如 SELECT 可能读取不必要的列;缺少合适索引可能让查询扫描大量记录;偏移量很大的分页可能很慢。但“加索引”也并不是无代价操作,索引会影响存储与写入。
6.2 读写分离(Read/Write Splitting)
如果系统绝大多数流量都是查询,可以配置 MySQL 源库和一个或多个副本:
Java 应用
/ \
写入 部分读取
▼ ▼
MySQL 源库 ──复制──→ MySQL 副本
INSERT SELECT
UPDATE
DELETE
MySQL 复制默认是异步的,副本可能短暂落后于源库。因此用户“刚修改头像,立刻刷新看不到”,就是需要警惕的读己之写一致性问题。重要查询可设计为走源库或采用其他一致性策略。参见 MySQL 8.4 Replication。
6.3 分库分表(Sharding)
当单库的数据量或写入吞吐已形成实质瓶颈,可能按某个业务键分片:
userid 取模 → 分片路由
userid = 1001 → 数据库 A
userid = 1002 → 数据库 B
userid = 1003 → 数据库 C
这只是路由思路的示例,并非推荐直接使用简单取模作为所有系统的分片策略。
分片带来的代价包括:跨片查询、全局唯一 ID、扩容迁移、热点分片、跨片事务和复杂统计。对于个人知识平台,通常应该先把单库的索引、备份和 SQL 优化做好。
6.4 消息队列(MQ)改变什么?
假设用户注册成功后,系统要发邮件、生成缩略图或者做全文索引。若这些任务全都在请求线程里同步执行,就会延长用户等待时间。
同步模式:
注册请求 → 写 MySQL → 发邮件(等待)→ 建索引(等待)→ 返回
异步模式:
注册请求 → 写 MySQL → 发布任务消息 → 返回“注册完成”
│
▼
RabbitMQ 队列
│
消费者异步发送邮件/建索引
这里要特别注意:注册成功表示核心注册事务已完成;邮件发送成功是另一件事,不应混为一个业务状态。
| MQ 能力 | 含义 | 必须一起考虑的问题 |
| ------------ | ------------------------------ | ---------------------------------------- |
| 异步 | 非关键任务不阻塞请求 | 异步任务什么时候真正完成? |
| 解耦 | 服务之间通过消息沟通 | 消息结构如何版本化? |
| 削峰 | 高峰任务先排队,消费者逐步处理 | 队列积压是否失控? |
| 重试 | 失败任务可再次尝试 | 消费者是否幂等? |
| 持久化与确认 | 降低消息丢失风险 | 投递确认、消费者确认、失败处理如何设计? |
RabbitMQ 官方 Work Queues 教程详细介绍任务分发、确认与预取(prefetch),参见 RabbitMQ Java Work Queues。
最关键的限制:MQ 不能凭空制造处理能力。 如果消息每秒进入 1000 条、消费者每秒只能处理 100 条,队列还是会持续积压。
另外,“数据库提交成功,但消息发送失败”也是一个经典一致性问题。可进一步学习 Transactional Outbox(事务发件箱) 等方案;不应仅靠盲目重试就假定数据一定一致。
阶段结论:读写分离扩展一部分读取能力,分片改变数据分布,MQ 改变任务执行时序。三者解决的是完全不同的瓶颈。
七、阶段六:微服务与分布式治理——系统大了如何继续开发和运行?
7.1 微服务为什么出现?
某个电商系统最初可能只有一个 Spring Boot 工程:
E-Commerce(一个应用、一次整体部署)
├── 用户模块
├── 商品模块
├── 订单模块
├── 支付模块
└── 物流模块
随着业务、开发团队、发布频率和扩容需求增加,可以考虑按业务边界拆分:
API Gateway
│
┌────────────┼────────────┐
▼ ▼ ▼
用户服务 订单服务 商品服务
│ │ │
用户数据 订单数据 商品数据
理想情况下,各服务能独立开发、测试、部署和按需扩容。但跨服务边界从原来的 Java 方法调用变为网络调用,复杂度显著提高。Martin Fowler 对微服务的介绍强调独立部署,同时也指出远程调用与服务边界会带来成本,参见 Microservices。
7.2 从单体迁移到微服务,多出来哪些基础设施?
| 技术类别 | 核心问题 | 代表技术 |
| -------------- | ------------------------------------ | --------------------------- |
| API 网关 | 谁统一接入、路由、鉴权和限流? | Spring Cloud Gateway |
| 服务注册与发现 | 服务 IP、端口变化时如何定位? | Nacos / Consul |
| 远程调用 | 服务之间怎样传递请求? | HTTP / OpenFeign / gRPC |
| 配置管理 | 多服务配置如何统一管理? | Nacos / Spring Cloud Config |
| 容错治理 | 下游慢、超时、故障如何避免连锁崩溃? | 超时、重试、熔断、隔离 |
| 数据一致性 | 不同服务各有数据库如何协调事务? | Saga / Outbox / 幂等 |
| 部署编排 | 大量实例如何部署、升级、恢复? | Docker / Kubernetes |
| 可观测性 | 一次请求横跨 5 个服务,到底哪里慢? | OpenTelemetry 等 |
7.3 分布式不等于微服务;微服务不等于高并发
分布式系统:组件运行在多个网络节点上,相互协作。
集群:多个实例共同承担某类工作,常用于吞吐量与可用性。
微服务:按较独立的业务能力拆分服务,强调服务边界、独立交付和治理。
一套完整的单体应用复制到两台服务器,已经是多节点部署,但仍是单体架构;一个拆成 20 个服务的系统,也可能因数据库瓶颈或糟糕的调用链而表现更差。
7.4 分布式环境中的几个“隐藏成本”
网络不可靠:远程调用会超时、失败或收到不确定结果。
局部故障:一个服务异常,不代表整个系统都宕机。
重复执行:超时后重试,可能导致重复订单与重复扣费。
跨服务一致性:不能简单假设一个本地 @Transactional 能覆盖多个独立数据库。
观测困难:仅看某个 Controller 的日志很难知道全链路耗时。
运维复杂:服务数量越多,发布、配置、监控和权限管理越复杂。
因此,是否采用微服务,应主要由领域边界、团队组织、发布周期、扩容特征和容错要求共同决定,而不是“用户突破某个固定数字就必须拆”。
阶段结论:微服务不是性能升级按钮,而是一组业务拆分与工程治理的架构取舍。
八、贯穿六个阶段的一条主线:可观测性与容量规划
不监测,就无法证明“性能已经提高”。需要把系统现象转换为指标。
| 指标 | 英文 | 说明 |
| ------------ | --------------- | ------------------------------------------ |
| 吞吐量 | RPS / TPS | 每秒完成的请求或事务数量,须定义统计口径 |
| 延迟 | P50 / P95 / P99 | 第 50、95、99 百分位请求耗时 |
| 错误率 | Error Rate | 失败请求占比,区分业务失败与系统失败 |
| CPU / 内存 | CPU / Memory | 应用和服务器资源使用情况 |
| JVM 垃圾回收 | GC | 停顿时间、频率、堆使用情况 |
| 工作线程 | Thread Pool | 活跃线程、排队、拒绝情况 |
| 数据库连接池 | HikariCP | 活跃连接、空闲连接、等待线程、获取连接耗时 |
| 数据库 | SQL / Locks | 慢查询、扫描量、锁等待和事务时长 |
| Redis | Hit Ratio | 缓存命中率、延迟、内存使用 |
| MQ | Queue Lag | 队列长度、最老消息等待时间、消费速率 |
| 分布式调用 | Trace | 一次请求跨服务的时间链条 |
典型工具对应:
业务压测:k6 / JMeter
应用指标:Spring Boot Actuator + Micrometer
指标存储与展示:Prometheus + Grafana
分布式追踪:OpenTelemetry
数据库诊断:慢 SQL 日志、EXPLAIN、Performance Schema
OpenTelemetry 将 Traces(追踪)、Metrics(指标)、Logs(日志) 作为观察系统的重要信号,参见 OpenTelemetry Signals。Spring Boot 可通过 Actuator 与 Micrometer 输出应用指标,参见 Spring Boot Metrics。
8.1 一个简化的容量诊断示例
假设某查询接口在低负载下响应 30ms,压测后 P95 变为 2 秒,错误率也开始升高。不要马上决定“上微服务”,而应依次问:
CPU 是否接近饱和?是否存在 GC 频繁停顿?
Tomcat 工作线程是否被慢调用长期占用?
HikariCP 是否存在大量等待连接的线程?
SQL 执行计划是否合理?是否发生锁等待?
是否大量重复读取热点数据?缓存是否合适?
数据库正常而应用 CPU 饱和,是否适合增加应用实例?
增加实例后数据库是否反而成为新瓶颈?
这一系列判断比背诵技术名词更加接近真正的软件架构工作。
九、六个阶段的技术地图与学习验收标准
| 阶段 | 必学概念 | 典型技术 | 能证明“我已理解”的表现 |
| ------------ | -------------------------------------- | ------------------------------- | ------------------------------------------------------- |
| 1 单次请求 | HTTP、Servlet、MVC、分层、ORM/Mapper | Spring MVC、MyBatis | 能口述一次请求从浏览器到数据库再返回的完整路径 |
| 2 单机并发 | 线程、共享变量、线程池、连接池、阻塞 | Tomcat、HikariCP | 能解释为什么 100 个请求不需要 100 个数据库连接 |
| 3 争用与瓶颈 | 竞态、事务、锁、MVCC、幂等、缓存、限流 | MySQL、Redis | 能解释库存超卖与缓存击穿的根因和应对方式 |
| 4 集群扩展 | 反向代理、负载均衡、无状态、共享存储 | Nginx、多 Spring Boot 实例、OSS | 能解释为什么单体集群不等于微服务,以及 Session/文件问题 |
| 5 数据与异步 | SQL 优化、复制延迟、数据分片、队列积压 | MySQL Replication、RabbitMQ | 能选择同步、异步、缓存、读副本而不过度设计 |
| 6 分布式治理 | 远程调用、超时、熔断、一致性、观测 | Gateway、Nacos、OpenTelemetry | 能解释微服务的收益、代价和适用前提 |
建议的六篇专题专栏规划
《Java Web 一次 HTTP 请求的完整生命周期》:Servlet、DispatcherServlet、Filter、Interceptor、参数绑定与响应。
《Tomcat 如何处理多个并发请求》:线程、线程池、连接池、阻塞、线程安全、压力测试。
《从库存超卖到高并发保护》:事务、锁、MVCC、幂等、Redis 与限流。
《从一台 Spring Boot 到多实例集群》:Nginx、负载均衡、无状态服务、Session 与文件存储。
《数据库扩展与消息队列》:索引、读写分离、分库分表、RabbitMQ、削峰与消息一致性。
《微服务与分布式系统入门》:服务拆分、注册发现、网关、容错、可观测性和架构取舍。
本篇作为专栏第 0 篇:总览与知识地图最合适。每一篇专题可以进一步按照“原理 → 真实场景 → 代码实验 → 压测验证 → 复盘”展开。
十、第一次实操:用最小实验亲眼看到并发
认知先建立,后续实操不必一次完成。第一轮只需要:
写一个 GET /api/test/sleep,让它在演示环境中同步等待约 1 秒;
用 k6 或 JMeter 分别发出 1、10、50、100 个并发请求;
记录 RPS、平均响应时间、P95、错误率;
在日志里打印当前线程名称,观察线程复用;
再给接口增加真实 MySQL 查询,观察连接池等待;
最后比较“调整线程配置”与“优化慢 SQL”分别会带来什么差异。
@RestController
@RequestMapping("/api/test")
public class ConcurrencyDemoController {
@GetMapping("/sleep")
public String sleep() throws InterruptedException {
String thread = Thread.currentThread().getName();
Thread.sleep(1000); // 仅供测试,生产业务不要无意义阻塞
return "handled by " + thread;
}
}
实验安全边界:只在本机或自己授权的测试环境进行压测,不要针对线上生产站点、他人服务或者共享数据库直接施加高并发请求。压测前准备监控、请求上限和退出条件。k6 的并发虚拟用户模型与固定到达率模型也要区分,参见 k6 Scenarios。
实验完成后的复盘题:
为什么 100 个请求不一定同时占用 100 个 CPU 核心?
为什么调大 maxThreads 后响应时间可能反而变长?
为什么调大 HikariCP 不一定使 SQL 更快?
为什么加了 Redis 还需要事务与唯一索引?
为什么部署两台 Spring Boot 仍然可能被同一个 MySQL 拖垮?
为什么微服务增加了服务之间的超时、重试和一致性问题?
十一、最后的认知总结
如果把整篇文章压缩成六句话:
单次请求阶段:先知道请求怎样从网络到 Controller、Service、Mapper,再返回响应。
单机并发阶段:Tomcat 线程池让多个请求推进,连接池管理昂贵的数据库连接。
竞争与瓶颈阶段:事务、锁和幂等保障数据正确;缓存和限流改善性能与稳定性。
应用集群阶段:Nginx 将请求分散到多个实例,但会产生共享状态和下游容量问题。
数据与异步阶段:数据库复制和分片扩展数据能力;MQ 将可异步的任务从主链路拆出。
分布式治理阶段:微服务服务于业务拆分和独立交付,也必须付出远程调用与治理成本。
所以,高并发系统的本质并不是“技术栈越多,系统就越高级”,而是:
先观察请求,找到瓶颈;先保障正确,再优化性能;能用简单机制解决,就不要引入无依据的复杂架构。
这条原则适用于个人博客、企业管理系统、电商平台,也适用于未来需要实时消息、异步任务与 AI 模型调用的应用。
十二、权威参考资料与后续延伸阅读
本文主要依据官方文档与经典架构资料组织;链接会随软件版本变化,学习时以当前项目对应版本为准。
建议优先阅读以下原始资料,按本文阶段顺序查阅即可:
请求链路与单机并发:Spring MVC DispatcherServlet、Tomcat HTTP Connector、HikariCP。
事务、数据正确性与缓存:Spring 事务、MySQL InnoDB Locking、Redis Cache-Aside。
集群与数据扩展:NGINX HTTP Load Balancing、MySQL Replication。
异步与分布式治理:RabbitMQ Java Work Queues、Microservices、OpenTelemetry Signals。
监控与压测:Spring Boot Metrics、k6 Scenarios。
[1] https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet.html Spring Framework - DispatcherServlet
[2] https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/sequence.html Spring Framework - Processing
[3] https://tomcat.apache.org/tomcat-10.1-doc/config/http.html Apache Tomcat 10.1 - HTTP Connector
[4] https://github.com/brettwooldridge/HikariCP HikariCP - Configuration
[5] https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html Spring Framework - Transactional
[6] https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html MySQL 8.4 - InnoDB Locking
[7] https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html MySQL 8.4 - Consistent Read
[8] https://redis.io/docs/latest/develop/use-cases/cache-aside/ Redis - Cache-Aside
[9] https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/ NGINX - HTTP Load Balancing
[10] https://dev.mysql.com/doc/refman/8.4/en/replication.html MySQL 8.4 - Replication
[11] https://www.rabbitmq.com/tutorials/tutorial-two-java RabbitMQ - Work Queues Java
[12] https://martinfowler.com/articles/microservices.html Martin Fowler - Microservices
[13] https://opentelemetry.io/docs/concepts/signals/ OpenTelemetry - Signals
[14] https://docs.spring.io/spring-boot/reference/actuator/metrics.html Spring Boot - Metrics
[15] https://grafana.com/docs/k6/latest/using-k6/scenarios/ Grafana k6 - Scenarios