摘要
当 Java 后端服务面对大量并发请求时,系统最先出现的问题通常不是代码语法错误,而是资源被迅速耗尽:线程池队列堆积、数据库连接池耗尽、接口响应变慢、下游服务超时,最终导致请求级联失败。
高并发设计的核心,不是简单地增加服务器数量,也不是给所有方法加上异步注解,而是要围绕系统容量建立一套完整的保护机制。线程池负责管理有限的执行资源,限流负责控制进入系统的请求量,熔断负责隔离故障下游,降级负责在资源不足或服务异常时保留核心能力。
本文从一个典型的商品查询和下单系统出发,介绍 Java 后端高并发场景中的线程池设计、队列选择、拒绝策略、信号量限流、令牌桶、漏桶、熔断状态机、服务降级和容量评估,并通过 Spring Boot 示例展示如何把这些机制落到代码和配置中。
读完本文后,你应该能够:
- 识别高并发系统中的线程、连接和队列瓶颈;
- 理解线程池参数和拒绝策略的实际含义;
- 为不同业务配置隔离的线程池;
- 选择固定窗口、滑动窗口、令牌桶或漏桶限流;
- 设计超时、重试、熔断和降级策略;
- 避免线程池、重试和熔断之间的级联放大;
- 建立容量评估、监控告警和压测方法。
一、背景与问题
1. 高并发不只是请求数量多
"高并发"经常被简单理解为每秒请求数很高,但系统是否能够承受流量,取决于多个因素:
- 单个请求的 CPU 消耗;
- 单个请求访问数据库的次数;
- 外部服务响应时间;
- 数据库连接数;
- 工作线程数;
- 缓存命中率;
- 请求体和响应体大小;
- 是否包含锁竞争;
- 是否存在慢查询;
- 是否存在突发流量。
一个每秒 1000 次、只访问本地缓存的接口,可能比每秒 100 次、每次都访问多个外部服务的接口更容易承受。
可以用一个简化公式理解系统吞吐能力:
text
系统吞吐量
≈ 可用并发资源数 / 单请求平均占用时间
如果一个请求平均占用线程 200 毫秒,线程池中有 100 个工作线程,那么理论并发处理能力约为:
text
100 / 0.2 = 500 请求/秒
这只是粗略估计,实际还会受到 CPU、数据库、网络和锁竞争影响。
2. 典型的级联故障
假设订单服务依赖库存服务和优惠券服务:
#mermaid-svg-Yu7XDs2zZDO4mJFG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Yu7XDs2zZDO4mJFG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Yu7XDs2zZDO4mJFG .error-icon{fill:#552222;}#mermaid-svg-Yu7XDs2zZDO4mJFG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Yu7XDs2zZDO4mJFG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Yu7XDs2zZDO4mJFG .marker.cross{stroke:#333333;}#mermaid-svg-Yu7XDs2zZDO4mJFG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Yu7XDs2zZDO4mJFG p{margin:0;}#mermaid-svg-Yu7XDs2zZDO4mJFG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Yu7XDs2zZDO4mJFG .cluster-label text{fill:#333;}#mermaid-svg-Yu7XDs2zZDO4mJFG .cluster-label span{color:#333;}#mermaid-svg-Yu7XDs2zZDO4mJFG .cluster-label span p{background-color:transparent;}#mermaid-svg-Yu7XDs2zZDO4mJFG .label text,#mermaid-svg-Yu7XDs2zZDO4mJFG span{fill:#333;color:#333;}#mermaid-svg-Yu7XDs2zZDO4mJFG .node rect,#mermaid-svg-Yu7XDs2zZDO4mJFG .node circle,#mermaid-svg-Yu7XDs2zZDO4mJFG .node ellipse,#mermaid-svg-Yu7XDs2zZDO4mJFG .node polygon,#mermaid-svg-Yu7XDs2zZDO4mJFG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Yu7XDs2zZDO4mJFG .rough-node .label text,#mermaid-svg-Yu7XDs2zZDO4mJFG .node .label text,#mermaid-svg-Yu7XDs2zZDO4mJFG .image-shape .label,#mermaid-svg-Yu7XDs2zZDO4mJFG .icon-shape .label{text-anchor:middle;}#mermaid-svg-Yu7XDs2zZDO4mJFG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Yu7XDs2zZDO4mJFG .rough-node .label,#mermaid-svg-Yu7XDs2zZDO4mJFG .node .label,#mermaid-svg-Yu7XDs2zZDO4mJFG .image-shape .label,#mermaid-svg-Yu7XDs2zZDO4mJFG .icon-shape .label{text-align:center;}#mermaid-svg-Yu7XDs2zZDO4mJFG .node.clickable{cursor:pointer;}#mermaid-svg-Yu7XDs2zZDO4mJFG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Yu7XDs2zZDO4mJFG .arrowheadPath{fill:#333333;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Yu7XDs2zZDO4mJFG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Yu7XDs2zZDO4mJFG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Yu7XDs2zZDO4mJFG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Yu7XDs2zZDO4mJFG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Yu7XDs2zZDO4mJFG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Yu7XDs2zZDO4mJFG .cluster text{fill:#333;}#mermaid-svg-Yu7XDs2zZDO4mJFG .cluster span{color:#333;}#mermaid-svg-Yu7XDs2zZDO4mJFG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Yu7XDs2zZDO4mJFG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Yu7XDs2zZDO4mJFG rect.text{fill:none;stroke-width:0;}#mermaid-svg-Yu7XDs2zZDO4mJFG .icon-shape,#mermaid-svg-Yu7XDs2zZDO4mJFG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Yu7XDs2zZDO4mJFG .icon-shape p,#mermaid-svg-Yu7XDs2zZDO4mJFG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Yu7XDs2zZDO4mJFG .icon-shape .label rect,#mermaid-svg-Yu7XDs2zZDO4mJFG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Yu7XDs2zZDO4mJFG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Yu7XDs2zZDO4mJFG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Yu7XDs2zZDO4mJFG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端
订单服务
库存服务
优惠券服务
订单数据库
库存数据库
优惠券数据库
当库存服务响应变慢时,订单服务中的工作线程会等待库存响应。如果订单服务没有超时和隔离机制,就会出现:
text
库存服务变慢
-> 订单服务线程大量等待
-> 订单服务线程池耗尽
-> 新请求无法处理
-> 网关请求超时
-> 客户端重试
-> 请求数量继续增加
-> 整个系统雪崩
这类问题被称为级联故障。故障可能从一个下游服务开始,最终扩散到多个上游服务。
3. 资源都是有限的
后端服务中的资源都有上限:
| 资源 | 常见上限 |
|---|---|
| CPU | 核数和调度能力 |
| 内存 | 堆内存和直接内存 |
| 工作线程 | 线程池最大线程数 |
| 数据库连接 | 连接池大小和数据库连接上限 |
| Redis 连接 | 客户端连接池大小 |
| 文件句柄 | 操作系统限制 |
| 网络带宽 | 网卡和链路容量 |
| 消息消费并发 | 消费者线程数 |
| 下游配额 | 第三方接口限额 |
高并发设计的第一原则是:
不要让无限流量直接冲击有限资源。
4. 线程堆积是怎样发生的
假设一个接口的工作线程池配置如下:
text
核心线程数:20
最大线程数:50
队列容量:1000
当下游服务变慢时,请求处理时间从 100 毫秒上升到 2 秒:
text
前 50 个请求占用工作线程
后续请求进入队列
队列逐渐增长
队列满后触发拒绝策略
客户端开始超时或重试
如果队列设置得非常大,系统不会立刻报错,但会让请求等待更长时间,最终产生大量超时。一个看似"能够缓冲流量"的大队列,可能只是把故障延后。
5. 高并发系统需要保护什么
通常需要保护以下几类对象:
- 核心业务线程;
- 数据库连接;
- 外部服务调用;
- 消息消费能力;
- CPU 和内存;
- 关键业务接口;
- 租户或用户配额;
- 单个热点资源。
保护机制通常可以组合使用:
text
入口限流
-> 线程池隔离
-> 超时控制
-> 下游限流
-> 熔断
-> 降级
-> 异步化
-> 监控和告警
二、核心概念
1. 并发、吞吐量和响应时间
并发数
并发数表示同一时刻正在处理或等待处理的请求数量。
吞吐量
吞吐量表示单位时间内系统完成的请求数量,常用 QPS 或 TPS 表示。
响应时间
响应时间表示从请求进入到返回结果所经历的时间,通常要关注平均值、P95、P99,而不是只看平均值。
根据 Little's Law:
text
并发数 = 吞吐量 × 平均响应时间
例如:
text
吞吐量:500 请求/秒
平均响应时间:0.2 秒
并发数:500 × 0.2 = 100
如果响应时间上升到 2 秒,在吞吐量不变的情况下,并发数就会上升到 1000。大量并发请求会进一步占用线程、连接和内存,形成恶性循环。
2. 线程池
线程池负责管理一组可复用的工作线程。请求到达后,任务可以提交给线程池执行,执行结束后线程被复用。
线程池的基本组成:
- 核心线程数;
- 最大线程数;
- 空闲线程存活时间;
- 工作队列;
- 线程工厂;
- 拒绝策略。
执行流程可以表示为:
#mermaid-svg-eaNunAB0TJyNoNsj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eaNunAB0TJyNoNsj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eaNunAB0TJyNoNsj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eaNunAB0TJyNoNsj .error-icon{fill:#552222;}#mermaid-svg-eaNunAB0TJyNoNsj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eaNunAB0TJyNoNsj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eaNunAB0TJyNoNsj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eaNunAB0TJyNoNsj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eaNunAB0TJyNoNsj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eaNunAB0TJyNoNsj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eaNunAB0TJyNoNsj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eaNunAB0TJyNoNsj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eaNunAB0TJyNoNsj .marker.cross{stroke:#333333;}#mermaid-svg-eaNunAB0TJyNoNsj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eaNunAB0TJyNoNsj p{margin:0;}#mermaid-svg-eaNunAB0TJyNoNsj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eaNunAB0TJyNoNsj .cluster-label text{fill:#333;}#mermaid-svg-eaNunAB0TJyNoNsj .cluster-label span{color:#333;}#mermaid-svg-eaNunAB0TJyNoNsj .cluster-label span p{background-color:transparent;}#mermaid-svg-eaNunAB0TJyNoNsj .label text,#mermaid-svg-eaNunAB0TJyNoNsj span{fill:#333;color:#333;}#mermaid-svg-eaNunAB0TJyNoNsj .node rect,#mermaid-svg-eaNunAB0TJyNoNsj .node circle,#mermaid-svg-eaNunAB0TJyNoNsj .node ellipse,#mermaid-svg-eaNunAB0TJyNoNsj .node polygon,#mermaid-svg-eaNunAB0TJyNoNsj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eaNunAB0TJyNoNsj .rough-node .label text,#mermaid-svg-eaNunAB0TJyNoNsj .node .label text,#mermaid-svg-eaNunAB0TJyNoNsj .image-shape .label,#mermaid-svg-eaNunAB0TJyNoNsj .icon-shape .label{text-anchor:middle;}#mermaid-svg-eaNunAB0TJyNoNsj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eaNunAB0TJyNoNsj .rough-node .label,#mermaid-svg-eaNunAB0TJyNoNsj .node .label,#mermaid-svg-eaNunAB0TJyNoNsj .image-shape .label,#mermaid-svg-eaNunAB0TJyNoNsj .icon-shape .label{text-align:center;}#mermaid-svg-eaNunAB0TJyNoNsj .node.clickable{cursor:pointer;}#mermaid-svg-eaNunAB0TJyNoNsj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eaNunAB0TJyNoNsj .arrowheadPath{fill:#333333;}#mermaid-svg-eaNunAB0TJyNoNsj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eaNunAB0TJyNoNsj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eaNunAB0TJyNoNsj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eaNunAB0TJyNoNsj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eaNunAB0TJyNoNsj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eaNunAB0TJyNoNsj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eaNunAB0TJyNoNsj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eaNunAB0TJyNoNsj .cluster text{fill:#333;}#mermaid-svg-eaNunAB0TJyNoNsj .cluster span{color:#333;}#mermaid-svg-eaNunAB0TJyNoNsj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eaNunAB0TJyNoNsj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eaNunAB0TJyNoNsj rect.text{fill:none;stroke-width:0;}#mermaid-svg-eaNunAB0TJyNoNsj .icon-shape,#mermaid-svg-eaNunAB0TJyNoNsj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eaNunAB0TJyNoNsj .icon-shape p,#mermaid-svg-eaNunAB0TJyNoNsj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eaNunAB0TJyNoNsj .icon-shape .label rect,#mermaid-svg-eaNunAB0TJyNoNsj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eaNunAB0TJyNoNsj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eaNunAB0TJyNoNsj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eaNunAB0TJyNoNsj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
否
是
提交任务
核心线程是否空闲
使用核心线程执行
队列是否有空间
任务进入队列
线程数是否达到最大值
创建非核心线程执行
执行拒绝策略
线程池并不是越大越好。线程数过多会造成:
- 上下文切换增加;
- CPU 争用;
- 内存占用增加;
- 数据库连接竞争;
- 下游压力放大;
- 故障恢复变慢。
3. CPU 密集型和 IO 密集型
线程池大小需要考虑任务类型。
CPU 密集型任务
例如:
- 图片压缩;
- 大量计算;
- 加密和解密;
- 数据格式转换;
- 复杂规则匹配。
线程数通常接近 CPU 核数,常见起点是:
text
线程数 ≈ CPU 核数 + 1
IO 密集型任务
例如:
- 访问数据库;
- 调用 HTTP 服务;
- 读写文件;
- 等待消息队列;
- 访问缓存。
线程在等待 IO 时不占用大量 CPU,因此线程数可以高于 CPU 核数,但不能忽略下游容量:
text
线程数上限
<= 下游连接池容量
<= 下游服务可承受并发
如果线程池有 200 个线程,但数据库连接池只有 20 个连接,剩余线程大部分时间都在等待连接。
4. 限流
限流是限制单位时间内允许通过的请求数量,目的是保护系统资源。
限流对象可以是:
- 全局接口;
- 单个用户;
- 单个租户;
- 单个 IP;
- 单个商品;
- 单个下游服务;
- 单个业务操作。
限流不一定意味着返回错误,也可以把请求放入队列或降低处理速度。但对于同步 HTTP 接口,最常见的做法是快速拒绝超出容量的请求。
5. 熔断
熔断器用于监测下游服务的错误和慢调用。当故障达到阈值时,暂时停止访问下游,快速返回降级结果。
熔断器通常有三个状态:
text
Closed:正常调用下游
-> 失败率或慢调用率超过阈值
Open:直接短路,不再调用下游
-> 等待一段时间
Half-Open:放少量探测请求
-> 成功则恢复 Closed
-> 失败则回到 Open
熔断的目标不是消灭错误,而是避免故障持续占用上游资源。
6. 降级
降级是在系统资源不足、依赖不可用或流量超过容量时,主动减少非核心能力,保留核心功能。
例如商品详情页可以降级:
text
正常:
商品信息 + 实时库存 + 推荐商品 + 用户评价 + 促销信息
降级:
商品信息 + 缓存库存
订单系统可以降级:
text
正常:
实时优惠券计算 + 个性化推荐 + 风控画像
降级:
使用默认优惠规则,跳过推荐,保留订单创建
降级必须是业务可接受的结果,而不是简单返回空字符串。一个好的降级方案需要明确:
- 哪些能力可以关闭;
- 关闭后用户看到什么;
- 是否会影响数据正确性;
- 何时自动恢复;
- 是否需要通知用户。
三、工作原理
1. 高并发保护的整体链路
一个较完整的保护链路如下:
#mermaid-svg-gtc9uRTOEbY00iLx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gtc9uRTOEbY00iLx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gtc9uRTOEbY00iLx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gtc9uRTOEbY00iLx .error-icon{fill:#552222;}#mermaid-svg-gtc9uRTOEbY00iLx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gtc9uRTOEbY00iLx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gtc9uRTOEbY00iLx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gtc9uRTOEbY00iLx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gtc9uRTOEbY00iLx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gtc9uRTOEbY00iLx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gtc9uRTOEbY00iLx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gtc9uRTOEbY00iLx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gtc9uRTOEbY00iLx .marker.cross{stroke:#333333;}#mermaid-svg-gtc9uRTOEbY00iLx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gtc9uRTOEbY00iLx p{margin:0;}#mermaid-svg-gtc9uRTOEbY00iLx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gtc9uRTOEbY00iLx .cluster-label text{fill:#333;}#mermaid-svg-gtc9uRTOEbY00iLx .cluster-label span{color:#333;}#mermaid-svg-gtc9uRTOEbY00iLx .cluster-label span p{background-color:transparent;}#mermaid-svg-gtc9uRTOEbY00iLx .label text,#mermaid-svg-gtc9uRTOEbY00iLx span{fill:#333;color:#333;}#mermaid-svg-gtc9uRTOEbY00iLx .node rect,#mermaid-svg-gtc9uRTOEbY00iLx .node circle,#mermaid-svg-gtc9uRTOEbY00iLx .node ellipse,#mermaid-svg-gtc9uRTOEbY00iLx .node polygon,#mermaid-svg-gtc9uRTOEbY00iLx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gtc9uRTOEbY00iLx .rough-node .label text,#mermaid-svg-gtc9uRTOEbY00iLx .node .label text,#mermaid-svg-gtc9uRTOEbY00iLx .image-shape .label,#mermaid-svg-gtc9uRTOEbY00iLx .icon-shape .label{text-anchor:middle;}#mermaid-svg-gtc9uRTOEbY00iLx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gtc9uRTOEbY00iLx .rough-node .label,#mermaid-svg-gtc9uRTOEbY00iLx .node .label,#mermaid-svg-gtc9uRTOEbY00iLx .image-shape .label,#mermaid-svg-gtc9uRTOEbY00iLx .icon-shape .label{text-align:center;}#mermaid-svg-gtc9uRTOEbY00iLx .node.clickable{cursor:pointer;}#mermaid-svg-gtc9uRTOEbY00iLx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gtc9uRTOEbY00iLx .arrowheadPath{fill:#333333;}#mermaid-svg-gtc9uRTOEbY00iLx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gtc9uRTOEbY00iLx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gtc9uRTOEbY00iLx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gtc9uRTOEbY00iLx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gtc9uRTOEbY00iLx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gtc9uRTOEbY00iLx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gtc9uRTOEbY00iLx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gtc9uRTOEbY00iLx .cluster text{fill:#333;}#mermaid-svg-gtc9uRTOEbY00iLx .cluster span{color:#333;}#mermaid-svg-gtc9uRTOEbY00iLx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gtc9uRTOEbY00iLx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gtc9uRTOEbY00iLx rect.text{fill:none;stroke-width:0;}#mermaid-svg-gtc9uRTOEbY00iLx .icon-shape,#mermaid-svg-gtc9uRTOEbY00iLx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gtc9uRTOEbY00iLx .icon-shape p,#mermaid-svg-gtc9uRTOEbY00iLx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gtc9uRTOEbY00iLx .icon-shape .label rect,#mermaid-svg-gtc9uRTOEbY00iLx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gtc9uRTOEbY00iLx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gtc9uRTOEbY00iLx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gtc9uRTOEbY00iLx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败
客户端请求
网关限流
服务入口限流
请求校验
业务线程池
下游调用隔离
超时控制
熔断判断
调用下游
调用结果
正常响应
重试或降级
记录指标与日志
每一层解决的问题不同:
- 网关限流:避免流量直接进入服务集群;
- 服务限流:保护单个实例;
- 线程池:控制执行资源;
- 下游隔离:避免一个依赖拖垮全部业务;
- 超时:避免请求无限等待;
- 熔断:减少对故障依赖的访问;
- 降级:保留可用的核心能力;
- 监控:发现容量和故障变化。
2. Java 线程池的参数
Java 中常用 ThreadPoolExecutor 表示线程池:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,
30,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
threadFactory,
new ThreadPoolExecutor.CallerRunsPolicy()
);
参数含义:
| 参数 | 说明 |
|---|---|
| corePoolSize | 核心线程数 |
| maximumPoolSize | 最大线程数 |
| keepAliveTime | 非核心线程空闲存活时间 |
| workQueue | 等待执行任务的队列 |
| threadFactory | 创建线程的工厂 |
| rejectedExecutionHandler | 队列和线程都满后的处理策略 |
需要特别注意任务进入线程池的顺序:
text
先使用核心线程
核心线程忙时进入队列
队列满后创建非核心线程
线程达到最大值后执行拒绝策略
3. 工作队列的选择
有界队列
例如 ArrayBlockingQueue:
java
new ArrayBlockingQueue<>(200)
优点是容量明确,可以防止任务无限堆积。缺点是突发流量下可能更快触发拒绝。
无界队列
例如 LinkedBlockingQueue 不指定容量。由于队列几乎不会满,maximumPoolSize 基本不会生效,任务可能无限增长,最终导致内存压力。
生产环境通常不建议随意使用无界队列承接外部请求。
同步移交队列
例如 SynchronousQueue 不保存任务,提交任务时必须有线程立即接收。它适合希望快速扩容、但不希望任务排队的场景。
4. 拒绝策略
Java 内置四种常见拒绝策略。
AbortPolicy
直接抛出 RejectedExecutionException:
java
new ThreadPoolExecutor.AbortPolicy()
适合希望调用方明确知道系统已超过容量的场景,但必须在上层处理异常。
CallerRunsPolicy
由提交任务的线程直接执行任务:
java
new ThreadPoolExecutor.CallerRunsPolicy()
它能够形成反向压力:提交速度越快,调用线程越容易被任务占用。但如果调用线程是 HTTP 工作线程,可能导致入口线程也被拖慢。
DiscardPolicy
直接丢弃任务,不抛异常。除非任务允许丢失且有其他补偿机制,否则不建议使用。
DiscardOldestPolicy
丢弃队列中最老的任务,再尝试提交新任务。适合"最新请求比旧请求更有价值"的特殊场景,但需要确认业务语义。
5. 线程池隔离
不要让所有业务共用一个线程池:
text
订单线程池
-> 订单创建、订单查询
通知线程池
-> 短信、邮件、站内信
报表线程池
-> 报表计算、文件导出
外部调用线程池
-> 第三方 API
如果报表导出任务突然增加,不应该占满订单创建使用的线程。
隔离可以按以下维度进行:
- 业务类型;
- 下游服务;
- 租户;
- 任务优先级;
- 读写操作;
- 在线和离线任务。
6. 限流算法
固定窗口
把时间切分成固定长度窗口,统计每个窗口内的请求数:
text
10:00:00 - 10:00:59:最多 1000 次
10:01:00 - 10:01:59:最多 1000 次
优点是实现简单,缺点是窗口边界可能产生突发流量:
text
10:00:59 发送 1000 次
10:01:00 再发送 1000 次
两秒内实际通过 2000 次
滑动窗口
滑动窗口把时间分成更细的片段,统计最近一段时间内的总请求数,边界突发问题更小,但实现和存储成本更高。
令牌桶
系统以固定速率向桶中放入令牌,请求必须取得令牌才能通过。桶有容量上限,因此可以允许一定程度的突发流量:
#mermaid-svg-7aax2UBbJC74L7Ln{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7aax2UBbJC74L7Ln .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7aax2UBbJC74L7Ln .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7aax2UBbJC74L7Ln .error-icon{fill:#552222;}#mermaid-svg-7aax2UBbJC74L7Ln .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7aax2UBbJC74L7Ln .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7aax2UBbJC74L7Ln .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7aax2UBbJC74L7Ln .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7aax2UBbJC74L7Ln .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7aax2UBbJC74L7Ln .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7aax2UBbJC74L7Ln .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7aax2UBbJC74L7Ln .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7aax2UBbJC74L7Ln .marker.cross{stroke:#333333;}#mermaid-svg-7aax2UBbJC74L7Ln svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7aax2UBbJC74L7Ln p{margin:0;}#mermaid-svg-7aax2UBbJC74L7Ln .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7aax2UBbJC74L7Ln .cluster-label text{fill:#333;}#mermaid-svg-7aax2UBbJC74L7Ln .cluster-label span{color:#333;}#mermaid-svg-7aax2UBbJC74L7Ln .cluster-label span p{background-color:transparent;}#mermaid-svg-7aax2UBbJC74L7Ln .label text,#mermaid-svg-7aax2UBbJC74L7Ln span{fill:#333;color:#333;}#mermaid-svg-7aax2UBbJC74L7Ln .node rect,#mermaid-svg-7aax2UBbJC74L7Ln .node circle,#mermaid-svg-7aax2UBbJC74L7Ln .node ellipse,#mermaid-svg-7aax2UBbJC74L7Ln .node polygon,#mermaid-svg-7aax2UBbJC74L7Ln .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7aax2UBbJC74L7Ln .rough-node .label text,#mermaid-svg-7aax2UBbJC74L7Ln .node .label text,#mermaid-svg-7aax2UBbJC74L7Ln .image-shape .label,#mermaid-svg-7aax2UBbJC74L7Ln .icon-shape .label{text-anchor:middle;}#mermaid-svg-7aax2UBbJC74L7Ln .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7aax2UBbJC74L7Ln .rough-node .label,#mermaid-svg-7aax2UBbJC74L7Ln .node .label,#mermaid-svg-7aax2UBbJC74L7Ln .image-shape .label,#mermaid-svg-7aax2UBbJC74L7Ln .icon-shape .label{text-align:center;}#mermaid-svg-7aax2UBbJC74L7Ln .node.clickable{cursor:pointer;}#mermaid-svg-7aax2UBbJC74L7Ln .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7aax2UBbJC74L7Ln .arrowheadPath{fill:#333333;}#mermaid-svg-7aax2UBbJC74L7Ln .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7aax2UBbJC74L7Ln .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7aax2UBbJC74L7Ln .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7aax2UBbJC74L7Ln .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7aax2UBbJC74L7Ln .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7aax2UBbJC74L7Ln .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7aax2UBbJC74L7Ln .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7aax2UBbJC74L7Ln .cluster text{fill:#333;}#mermaid-svg-7aax2UBbJC74L7Ln .cluster span{color:#333;}#mermaid-svg-7aax2UBbJC74L7Ln div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7aax2UBbJC74L7Ln .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7aax2UBbJC74L7Ln rect.text{fill:none;stroke-width:0;}#mermaid-svg-7aax2UBbJC74L7Ln .icon-shape,#mermaid-svg-7aax2UBbJC74L7Ln .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7aax2UBbJC74L7Ln .icon-shape p,#mermaid-svg-7aax2UBbJC74L7Ln .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7aax2UBbJC74L7Ln .icon-shape .label rect,#mermaid-svg-7aax2UBbJC74L7Ln .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7aax2UBbJC74L7Ln .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7aax2UBbJC74L7Ln .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7aax2UBbJC74L7Ln :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
按速率生成令牌
令牌桶
请求到达
桶中有令牌
取令牌并放行
拒绝或等待
漏桶
请求先进入桶,以固定速率流出。漏桶更强调平滑流量,但不适合所有实时接口,因为请求可能等待较长时间。
7. 信号量隔离
信号量适合限制同时执行的任务数量:
java
private final Semaphore semaphore = new Semaphore(20);
public ProductDetail queryProduct(Long productId) {
boolean acquired = semaphore.tryAcquire();
if (!acquired) {
throw new TooManyRequestsException(
"当前查询繁忙,请稍后重试"
);
}
try {
return productRepository.query(productId);
} finally {
semaphore.release();
}
}
与 QPS 限流不同,信号量限制的是"同时执行数量",特别适合保护数据库、外部接口或高耗时操作。
8. 超时、重试和熔断的关系
这三个机制必须一起设计:
text
超时:单次调用最多等待多久
重试:失败后是否再次调用
熔断:失败过多时是否暂时停止调用
降级:停止调用后返回什么结果
错误配置可能导致放大:
text
上游请求 100 次
每次请求重试 3 次
下游实际收到 400 次请求
如果调用链有三层,每层都重试三次,最坏情况下请求数量会指数增长。因此通常建议:
- 只在最接近故障源的一层重试;
- 限制重试次数;
- 只重试明确的临时错误;
- 重试使用指数退避;
- 一旦熔断,不再继续重试;
- 对写操作谨慎重试,优先保证幂等。
9. 熔断器状态机
熔断器的基本状态转换:
#mermaid-svg-rSRVpbVkp3TEqWS1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rSRVpbVkp3TEqWS1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rSRVpbVkp3TEqWS1 .error-icon{fill:#552222;}#mermaid-svg-rSRVpbVkp3TEqWS1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rSRVpbVkp3TEqWS1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rSRVpbVkp3TEqWS1 .marker.cross{stroke:#333333;}#mermaid-svg-rSRVpbVkp3TEqWS1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rSRVpbVkp3TEqWS1 p{margin:0;}#mermaid-svg-rSRVpbVkp3TEqWS1 defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-rSRVpbVkp3TEqWS1 g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-rSRVpbVkp3TEqWS1 g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-rSRVpbVkp3TEqWS1 g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-rSRVpbVkp3TEqWS1 g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-rSRVpbVkp3TEqWS1 g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-rSRVpbVkp3TEqWS1 .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-rSRVpbVkp3TEqWS1 .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-rSRVpbVkp3TEqWS1 .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rSRVpbVkp3TEqWS1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rSRVpbVkp3TEqWS1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rSRVpbVkp3TEqWS1 .edgeLabel .label text{fill:#333;}#mermaid-svg-rSRVpbVkp3TEqWS1 .label div .edgeLabel{color:#333;}#mermaid-svg-rSRVpbVkp3TEqWS1 .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-rSRVpbVkp3TEqWS1 .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-rSRVpbVkp3TEqWS1 .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-rSRVpbVkp3TEqWS1 .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-rSRVpbVkp3TEqWS1 .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-rSRVpbVkp3TEqWS1 .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rSRVpbVkp3TEqWS1 #statediagram-barbEnd{fill:#333333;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .cluster-label,#mermaid-svg-rSRVpbVkp3TEqWS1 .nodeLabel{color:#131300;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-rSRVpbVkp3TEqWS1 .note-edge{stroke-dasharray:5;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-note text{fill:black;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram-note .nodeLabel{color:black;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagram .edgeLabel{color:red;}#mermaid-svg-rSRVpbVkp3TEqWS1 #dependencyStart,#mermaid-svg-rSRVpbVkp3TEqWS1 #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-rSRVpbVkp3TEqWS1 .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rSRVpbVkp3TEqWS1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 失败率超阈值
慢调用率超阈值
等待窗口结束
探测请求成功
探测请求失败
CLOSED
OPEN
HALF_OPEN
熔断指标可以基于:
- 失败率;
- 慢调用率;
- 超时次数;
- 特定异常数量;
- 连续失败次数;
- 业务错误比例。
不能只使用"异常数量"判断,因为有些业务异常是正常的,比如库存不足,不一定说明库存服务故障。
四、实战示例
1. 创建业务线程池
在 Spring Boot 中,不建议直接使用默认异步线程池承载所有任务。可以显式定义线程池:
java
@Configuration
@EnableAsync
public class ExecutorConfig {
@Bean("orderExecutor")
public Executor orderExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("order-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
@Bean("notificationExecutor")
public Executor notificationExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(12);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("notify-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.AbortPolicy()
);
executor.initialize();
return executor;
}
}
业务方法按用途选择线程池:
java
@Async("notificationExecutor")
public CompletableFuture<Void> sendOrderNotification(
String orderNo
) {
notificationService.send(orderNo);
return CompletableFuture.completedFuture(null);
}
2. 自定义线程工厂
为线程设置有意义的名称,便于日志和线程转储分析:
java
public class NamedThreadFactory
implements ThreadFactory {
private final AtomicInteger sequence =
new AtomicInteger(1);
private final String prefix;
public NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable task) {
Thread thread = new Thread(
task,
prefix + sequence.getAndIncrement()
);
thread.setDaemon(false);
thread.setUncaughtExceptionHandler(
(t, ex) -> log.error(
"异步线程发生未处理异常: {}",
t.getName(),
ex
)
);
return thread;
}
}
线程名称示例:
text
order-1
order-2
inventory-client-3
report-export-1
排查线程池问题时,可以快速知道阻塞发生在哪类任务。
3. 暴露线程池指标
线程池至少应记录以下指标:
java
@Component
public class ExecutorMetrics {
private final ThreadPoolTaskExecutor orderExecutor;
public ExecutorMetrics(
@Qualifier("orderExecutor")
ThreadPoolTaskExecutor orderExecutor
) {
this.orderExecutor = orderExecutor;
}
@Scheduled(fixedDelay = 5000)
public void report() {
ThreadPoolExecutor executor =
orderExecutor.getThreadPoolExecutor();
log.info(
"order pool: active={}, pool={}, queue={}, completed={}",
executor.getActiveCount(),
executor.getPoolSize(),
executor.getQueue().size(),
executor.getCompletedTaskCount()
);
}
}
生产环境建议通过 Micrometer 或监控系统记录:
- active;
- pool size;
- queue size;
- queue remaining capacity;
- completed task count;
- rejected task count;
- task execution time;
- task wait time。
4. 使用 Redis 实现简单限流
下面以固定窗口为例,使用 Redis 记录某个用户在当前窗口中的访问次数:
java
@Service
public class UserRateLimiter {
private final StringRedisTemplate redisTemplate;
public UserRateLimiter(
StringRedisTemplate redisTemplate
) {
this.redisTemplate = redisTemplate;
}
public boolean allow(
String userId,
int limit,
Duration window
) {
long windowId =
Instant.now().getEpochSecond()
/ window.toSeconds();
String key = "rate:user:"
+ userId
+ ":"
+ windowId;
Long count = redisTemplate.opsForValue()
.increment(key);
if (count != null && count == 1) {
redisTemplate.expire(key, window);
}
return count != null && count <= limit;
}
}
控制器中使用:
java
@GetMapping("/products/{id}")
public ProductDetail getProduct(
@PathVariable Long id,
@RequestHeader("X-User-Id") String userId
) {
boolean allowed = rateLimiter.allow(
userId,
60,
Duration.ofMinutes(1)
);
if (!allowed) {
throw new TooManyRequestsException(
"访问过于频繁,请稍后再试"
);
}
return productService.getDetail(id);
}
生产实现还应考虑:
- Redis 网络异常时是放行还是拒绝;
- 自然时间窗口的边界问题;
- 多租户配额;
- 用户等级差异;
- 热点 Key;
- 限流规则动态变更;
- 监控被拒绝请求数量。
5. 使用本地信号量保护下游
如果需要限制同时访问某个第三方接口的请求数,可以使用信号量:
java
@Service
public class InventoryClientGuard {
private final Semaphore semaphore =
new Semaphore(30);
private final InventoryClient client;
public InventoryClientGuard(
InventoryClient client
) {
this.client = client;
}
public InventoryResult query(Long skuId) {
if (!semaphore.tryAcquire()) {
return InventoryResult.degraded(
"库存查询繁忙"
);
}
try {
return client.query(skuId);
} finally {
semaphore.release();
}
}
}
如果服务是多实例部署,本地信号量只限制单个实例。全局限制可以使用 Redis、网关或下游服务自身的限流能力。
6. 使用 Resilience4j 进行熔断和降级
在 Spring Boot 中,可以通过配置定义超时、重试和熔断。示例配置:
yaml
resilience4j:
circuitbreaker:
instances:
inventory:
sliding-window-type: COUNT_BASED
sliding-window-size: 50
minimum-number-of-calls: 20
failure-rate-threshold: 50
slow-call-rate-threshold: 60
slow-call-duration-threshold: 800ms
wait-duration-in-open-state: 10s
permitted-number-of-calls-in-half-open-state: 3
timelimiter:
instances:
inventory:
timeout-duration: 500ms
retry:
instances:
inventory:
max-attempts: 2
wait-duration: 100ms
retry-exceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
服务代码:
java
@Service
public class InventoryApplicationService {
private final InventoryClient inventoryClient;
public InventoryApplicationService(
InventoryClient inventoryClient
) {
this.inventoryClient = inventoryClient;
}
@CircuitBreaker(
name = "inventory",
fallbackMethod = "fallback"
)
@TimeLimiter(name = "inventory")
public CompletableFuture<InventoryResult> query(
Long skuId
) {
return CompletableFuture.supplyAsync(
() -> inventoryClient.query(skuId)
);
}
private CompletableFuture<InventoryResult> fallback(
Long skuId,
Throwable throwable
) {
log.warn(
"库存服务降级,skuId={}",
skuId,
throwable
);
return CompletableFuture.completedFuture(
InventoryResult.unknown()
);
}
}
需要注意,注解只是配置入口,真正的降级内容仍然需要结合业务设计。返回 unknown、cached 或 unavailable 的含义不同,前端和业务流程必须能够正确处理。
7. 设计商品详情页降级
假设商品详情由多个模块组成:
java
public ProductPage getProductPage(Long productId) {
Product product =
productService.getBasicInfo(productId);
InventoryResult inventory =
inventoryService.query(product.getSkuId());
List<Recommendation> recommendations =
recommendationService.recommend(productId);
List<Review> reviews =
reviewService.latest(productId);
return ProductPage.builder()
.product(product)
.inventory(inventory)
.recommendations(recommendations)
.reviews(reviews)
.build();
}
如果所有模块都同步调用,任何一个下游变慢都会拖慢整个页面。可以按重要性划分:
text
核心数据:
商品基本信息
可接受降级:
推荐商品
用户评价
实时库存
必须强校验:
下单时的最终库存
改造后:
java
public ProductPage getProductPage(Long productId) {
Product product =
productService.getBasicInfo(productId);
InventoryResult inventory =
inventoryService.queryOrCached(product.getSkuId());
List<Recommendation> recommendations =
recommendationService.safeRecommend(productId);
List<Review> reviews =
reviewService.safeLatest(productId);
return ProductPage.builder()
.product(product)
.inventory(inventory)
.recommendations(recommendations)
.reviews(reviews)
.build();
}
页面展示可以使用明确状态:
json
{
"inventory": {
"status": "UNKNOWN",
"message": "库存信息暂时无法获取"
},
"recommendations": {
"status": "DEGRADED",
"items": []
}
}
不要把降级结果伪装成精确数据,例如库存查询失败时直接显示"有货",可能会导致下单阶段失败。
8. 设置接口级超时
超时必须小于上游能够接受的最大响应时间,并为后续处理留出空间:
yaml
clients:
inventory:
connect-timeout: 100ms
read-timeout: 400ms
total-timeout: 500ms
coupon:
connect-timeout: 100ms
read-timeout: 300ms
total-timeout: 400ms
超时层次应该清晰:
text
数据库查询超时
< 下游 HTTP 调用超时
< 当前业务方法超时
< 网关超时
< 客户端超时
如果下游超时时间比上游更长,上游可能已经返回失败,下游仍然继续占用资源,容易产生无效工作。
9. 重试的代码边界
重试只适用于明确的临时错误:
java
public InventoryResult queryWithRetry(Long skuId) {
int maxAttempts = 2;
for (int attempt = 1;
attempt <= maxAttempts;
attempt++) {
try {
return inventoryClient.query(skuId);
} catch (ConnectException ex) {
if (attempt == maxAttempts) {
throw ex;
}
sleep(Duration.ofMillis(
100L * attempt
));
}
}
throw new IllegalStateException(
"unreachable"
);
}
不要对以下操作盲目重试:
- 扣款;
- 创建订单;
- 扣减库存;
- 发送不可撤回通知;
- 修改状态但没有幂等键的请求。
如果必须重试写操作,应先具备:
- 请求唯一号;
- 服务端幂等;
- 可查询的处理状态;
- 明确的超时后查询机制。
五、常见问题与实践建议
1. 线程池核心线程数应该设置多少
没有通用固定值。可以先根据任务类型和下游容量估算,再通过压测调整。
CPU 密集型任务:
text
线程数 ≈ CPU 核数 + 1
IO 密集型任务可以使用近似公式:
text
线程数
≈ CPU 核数 × 目标利用率
× (1 + 等待时间 / 计算时间)
例如 8 核 CPU,目标利用率 0.8,任务等待时间约为计算时间的 4 倍:
text
8 × 0.8 × (1 + 4) = 32
这只是起点,还要验证数据库连接、下游并发和内存是否能够承受。
2. 线程池队列是否越大越好
不是。大队列可以吸收短时间突发流量,但会导致:
- 请求等待时间变长;
- 超时请求仍然占用队列;
- 内存消耗增加;
- 故障暴露延迟;
- 恢复过程更慢。
对于在线请求,宁可快速拒绝一部分超出容量的请求,也不要让所有请求排队到超时。
对于离线任务,可以使用更大的持久化队列,但必须支持:
- 任务状态;
- 重试;
- 死信;
- 优先级;
- 取消;
- 监控。
3. 为什么不建议所有地方都使用 CompletableFuture
CompletableFuture 只是异步编程接口,不会自动解决资源隔离。如果不指定线程池,某些异步任务可能使用公共线程池,进而与其他任务互相影响。
错误示例:
java
CompletableFuture.supplyAsync(
() -> slowRemoteCall()
);
更明确的写法:
java
CompletableFuture.supplyAsync(
() -> slowRemoteCall(),
inventoryExecutor
);
同时还需要为异步调用设置超时、异常处理和取消策略。
4. CallerRunsPolicy 会不会拖垮请求线程
会。CallerRunsPolicy 的作用是让提交任务的线程承担执行压力。如果提交线程是 Tomcat 工作线程,那么当线程池满时,HTTP 请求线程会直接执行任务。
它的优点是形成反压,缺点是可能把业务耗时传递到入口层。因此使用前要判断:
- 任务是否允许在请求线程执行;
- 执行时间是否可控;
- 是否会占用数据库连接;
- 是否会阻塞网关线程;
- 是否需要直接返回限流错误。
对于不能在请求线程执行的任务,更适合使用 AbortPolicy 并返回明确的过载响应,或者写入持久化任务队列。
5. 重试和限流会不会互相冲突
如果每个失败请求都立即重试,重试流量可能绕过原有的限流逻辑,进一步放大压力。
建议:
- 重试请求也计入限流;
- 设置重试预算;
- 重试使用退避;
- 熔断打开后停止重试;
- 对同一个业务请求设置总截止时间;
- 记录原始请求和重试请求的关联 ID。
6. 熔断打开后是不是所有请求都返回错误
不一定。熔断的目的通常是保护系统并保留部分能力。不同业务可以有不同降级:
text
商品推荐:返回空列表
用户画像:使用缓存
库存展示:返回未知状态
订单查询:读取本地订单数据
支付提交:直接拒绝并提示稍后重试
降级策略必须避免伪造核心业务结果。对于支付、扣款、库存扣减等关键操作,无法确认状态时,通常应该返回处理中并引导查询,而不是简单判断失败或成功。
7. 限流放在哪一层最合适
通常需要多层限流:
text
CDN 或 WAF:
防止恶意流量和明显攻击
API 网关:
按路由、IP、租户做整体限制
应用服务:
按业务和用户做精细限制
业务资源:
对热点商品、账户和下游服务做局部限制
只在应用代码中限流,可能已经让大量请求穿过网关和网络层;只在网关限流,又无法表达复杂业务规则。
8. 分布式限流为什么会有误差
使用 Redis 进行分布式限流时,误差可能来自:
- 多次网络往返;
- 时钟不一致;
- Redis 主从延迟;
- Lua 脚本执行和过期时间;
- 多实例并发;
- 窗口边界;
- Key 热点。
对于大多数业务限流,允许小范围误差比为了绝对精确而引入复杂协调更合理。对于资金、库存等强约束资源,不应该只依赖通用限流,而应使用数据库约束、状态机和业务锁。
9. 怎样避免缓存击穿造成高并发访问数据库
热点 Key 失效时,大量请求可能同时查询数据库。常见方案:
- 互斥锁,只允许一个请求重建缓存;
- 逻辑过期,后台异步刷新;
- 随机过期时间,避免同时失效;
- 热点数据预热;
- 限制重建任务并发;
- 数据库查询超时;
- 查询失败时返回旧缓存。
缓存保护本质上也是一种资源隔离和限流。
10. 如何处理过载响应
当系统主动拒绝请求时,应该返回明确的协议和业务信息:
json
{
"code": "SYSTEM_BUSY",
"message": "当前访问人数较多,请稍后重试",
"retryable": true,
"request_id": "req-202609080001"
}
不要返回模糊的 500,让调用方误以为是服务内部错误并进行无限重试。
11. 如何进行压测
压测至少应覆盖:
- 正常流量;
- 突发流量;
- 持续高流量;
- 下游变慢;
- 下游错误;
- 数据库连接耗尽;
- 线程池队列满;
- Redis 限流异常;
- 服务重启恢复;
- 熔断打开和恢复。
重点观察:
- QPS;
- P50、P95、P99;
- 错误率;
- 拒绝率;
- 线程池活跃数;
- 队列长度;
- 数据库连接池;
- CPU 和内存;
- GC 停顿;
- 下游响应时间;
- 熔断状态;
- 重试次数。
压测结果不能只看吞吐量,还要看系统是否在过载时快速失败,以及恢复后能否自动回到正常状态。
六、进阶思考
1. 容量评估应该以瓶颈资源为准
系统容量不是由某一个接口的理论 QPS 决定,而是由最先达到瓶颈的资源决定:
text
应用层 QPS:1000
数据库最大承载:300
第三方接口配额:200
最终可用容量:不超过 200
如果一个请求需要调用两次第三方接口,那么接口层的可承载能力还需要进一步折算。
建议建立容量模型:
text
请求总量
-> 每类请求比例
-> 每类请求平均耗时
-> 每类请求数据库次数
-> 每类请求下游调用次数
-> 计算线程、连接和带宽需求
2. 线程池隔离和舱壁模式
舱壁模式的思想来自船舱隔离:某个舱室进水时,不应该让整艘船沉没。
在服务中可以这样实现:
text
核心订单线程池:16 个线程
推荐线程池:8 个线程
报表线程池:4 个线程
第三方支付线程池:8 个线程
即使报表任务把自己的队列占满,也不应该影响订单处理。
隔离还可以扩展到:
- 不同租户;
- 不同优先级;
- 不同机房;
- 不同连接池;
- 不同实例组。
3. 优先级与公平性
当系统容量不足时,不能让低价值任务占满所有资源。可以将请求分为:
text
P0:支付、下单、账户查询
P1:订单列表、库存展示
P2:推荐、画像、统计
P3:报表、批处理、导出
高优先级请求可以使用独立线程池或独立实例。低优先级任务在系统繁忙时暂停、降速或进入异步队列。
需要防止高优先级长期占满资源,可以设置最小配额和最大配额,维持基本公平性。
4. 超时传播和截止时间
复杂调用链中,每一层都单独设置超时可能造成总时间失控。更好的方式是为一次请求定义截止时间:
text
用户请求总预算:1000ms
-> 入口校验:50ms
-> 查询订单:300ms
-> 查询库存:400ms
-> 结果组装:100ms
-> 保留余量:150ms
下游调用时传递剩余时间,而不是总是从头开始等待。这样可以避免上游已经超时,下游仍然继续执行。
5. 异步化不是免费的
异步化可以减少同步等待,但会增加:
- 任务状态管理;
- 消息可靠投递;
- 失败重试;
- 顺序控制;
- 用户查询进度;
- 幂等;
- 数据最终一致;
- 调试复杂度。
适合异步化的场景:
- 邮件和短信;
- 报表生成;
- 大文件导出;
- 非核心统计;
- 搜索索引更新;
- 推荐计算;
- 批量通知。
不适合盲目异步化的场景:
- 需要立即返回最终结果;
- 业务必须保持强一致;
- 用户无法接受处理中状态;
- 任务失败后没有补偿能力。
6. 过载保护与系统自适应
静态限流值可能无法适应资源变化。可以根据 CPU、线程池队列、响应时间和错误率动态调整限流阈值:
text
P99 上升且队列增长
-> 降低入口限流值
下游恢复且队列下降
-> 逐步提高限流值
错误率持续升高
-> 打开熔断并进入降级
自适应保护需要防止振荡,通常采用:
- 分阶段调整;
- 设置最小和最大阈值;
- 连续多个窗口确认;
- 滑动平均;
- 恢复时缓慢放量。
7. 监控指标的分层
建议从四个层次建立监控。
入口层
- 请求量;
- 限流数;
- 4xx 和 5xx;
- 响应时间;
- 用户和租户分布。
执行层
- 线程池活跃数;
- 队列长度;
- 拒绝任务数;
- 任务执行耗时;
- CPU 和 GC。
依赖层
- 数据库连接池;
- Redis 延迟;
- HTTP 下游成功率;
- 超时和重试;
- 熔断状态。
业务层
- 下单成功率;
- 支付成功率;
- 库存扣减失败率;
- 降级次数;
- 用户取消率;
- 最终一致性补偿数量。
只有同时观察技术指标和业务指标,才能判断降级是否真的保护了系统,而不是简单地把错误隐藏起来。
8. 高并发设计中的一致性原则
线程池、限流和熔断主要解决可用性问题,但不能破坏业务正确性:
- 限流不能导致扣款重复;
- 重试不能导致订单重复创建;
- 降级不能把未知库存伪装成有库存;
- 熔断恢复不能绕过权限;
- 异步化不能丢失业务事件;
- 线程池拒绝不能静默丢弃核心任务;
- 超时后必须能够查询最终业务状态。
技术保护机制最终要服务于业务状态的正确性。
结论
Java 后端高并发设计的核心,是让系统在流量和故障超过预期时仍然能够控制损失、快速失败并逐步恢复。
线程池管理有限的执行资源,应该使用有界队列、合理的线程数、清晰的命名和业务隔离。限流控制进入系统的流量,应该结合网关、应用和业务资源多层实施。超时限制单次等待时间,重试处理明确的临时错误,熔断隔离持续失败的下游,降级则保留真正重要的核心能力。
一套可落地的高并发保护链路通常包括:
- 入口限流;
- 线程池隔离;
- 有界队列;
- 数据库和外部服务连接保护;
- 超时与截止时间;
- 有条件的重试;
- 熔断和半开探测;
- 业务降级;
- 监控、压测和容量评估;
- 故障后的恢复与复盘。
高并发系统不是追求所有请求都成功,而是在容量有限、依赖不稳定和流量突增时,优先保证核心请求、避免故障扩散,并让系统能够恢复到正常状态。
下一篇可以继续学习《从 JVM 内存模型到 GC 调优:Java 服务性能优化实战》,深入理解 Java 进程内部的内存、线程、垃圾回收和性能调优。