Java 后端高并发设计:线程池、限流、熔断与降级

摘要

当 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 进程内部的内存、线程、垃圾回收和性能调优。

相关推荐
天衍四九-1 小时前
MySQL中如何定位慢查询?如果SQL语句执行很慢,如何分析和优化?
数据库·sql·mysql
尚久龙1 小时前
mysql-5.7.26-winx64的安装步骤
数据库·mysql
医疗信息化王工1 小时前
DataForge:基于 Python 的数据库批量导出 Excel 工具——从架构到部署的全流程实战
数据库·python·excel
实心儿儿1 小时前
MySQL 的安装
数据库·mysql·adb
Seoyoneh1 小时前
Agentic Workflow编排架构:云客服从“被动响应”迈向“主动执行”的技术实现
java·开发语言·架构
海南java第二人1 小时前
对外 API 如何保证幂等性?插入数据 / 上传表格场景全方案汇总
java·幂等性
tryxr2 小时前
Chat2Excel 文件服务模块上传文件功能开发
java·项目·oss·easyexcel·文件服务
Tangyuewei2 小时前
Java 写 Agent:模型只是组件
java·开发语言
彧azz3 小时前
Java学习语法篇:变量
java·学习