前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、高并发与高性能架构设计

  • 技术高度,决定团队天花板:技术领导者的专业高度,在很大程度上决定了整个团队所能达到的技术天花板;
  • 追求卓越,驱动持续突破:技术领导者对卓越的追求与对质量的坚持,是驱动团队持续突破、不断进化的关键引擎。
  • 硬核领航,方能以身率众:技术领导者必须在技术深度上扎得稳、在技术广度上看得远、在技术敏感度上反应快、在技术创新度上敢为人先。唯有自身足够“硬核”,才能以身作则、凝聚人心,带领团队穿越复杂性迷雾,抵达技术与业务双赢的高地

1、根据流量规模设计系统

1)QPS 在 1万以下的系统设计

适用场景:内部系统、中后台服务、低频C端应用等。

对于中小流量场景(如几百至数千 QPS ),且对响应延迟要求不高的系统,推荐采用最简洁的架构:直接通过数据库进行 CRUD 操作

若数据库面临较大读压力或存在性能瓶颈,可引入缓存层(如 Redis)作为前置缓冲,显著降低数据库负载。此外,适当使用多线程处理可进一步提升并发能力,满足基本性能需求。

2)QPS 达到数万至数十万的系统设计

关键策略:缓存前置、数据预热、读写分离。

当系统面临较高并发(如几万到几十万 QPS ),性能要求较高时,应优先考虑 以缓存为核心的读服务架构

现代缓存集群(如 Redis Cluster)已具备支撑百万级 QPS 的能力。建议将热点数据提前预加载至缓存中,服务层直接从缓存获取数据,避免频繁访问数据库。

若一致性要求允许短暂延迟,完全可以通过缓存满足绝大部分请求。结合缓存 + 多线程处理,即可应对绝大多数高并发读场景。

3)QPS 超过百万级的系统设计

面对百万级甚至更高的请求流量,且对延迟极为敏感的“极限性能”场景,必须追求极致优化。此时,仅靠缓存已难以满足要求,需转向内存级数据访问

具体做法:

  • 在服务启动时,将必要数据全量或增量加载至 JVM 堆内存或堆外内存;
  • 核心链路尽可能基于内存完成计算与查询,减少 I/O 开销;
  • 对于依赖外部 RPC 调用的数据,评估是否可提前同步至本地内存,规避远程调用延迟。

值得注意的是,在内存计算已能将单次处理时间控制在毫秒级(如 5ms 内)的情况下,不一定需要引入多线程。相反,采用串行化处理可能更稳定,避免因线程切换、锁竞争带来的系统抖动和复杂性。

2、性能提升

在高并发系统中,性能瓶颈往往不在于单点能力,而在于整体链路的累积延迟与资源消耗。以下是经过验证一些关键优化策略,按执行阶段与优化维度组织,逻辑不变,重在落地实效。

1)服务启动期充分初始化

充分利用服务启动窗口期,完成数据预加载与预计算,避免运行时重复计算,显著降低核心流程耗时。

2)精简网络传输报文

控制接口返回数据量,遵循“按需提供”原则。仅返回调用方申请的字段,杜绝冗余传输。结合字段裁剪、字符串长度压缩、内容序列化压缩(如 ProtobufGZIP)等手段,有效降低网络带宽占用与反序列化开销。

3)减少远程网络调用

将高频访问且变更不频繁的 RPC 依赖数据,通过本地缓存或内存存储的方式固化。例如将配置表等数据加载至 JVM 内存,避免每次请求都发起远程调用,大幅降低依赖服务压力与响应延迟。

4)规避不必要的网络消耗

评估部分数据是否可通过本地化方式获取。例如将公共算法、规则引擎、码表信息打包为共享 Jar 包嵌入应用,替代远程查询接口。既消除网络往返,也减轻下游服务与中间件的负载。

5)优先同机房调用

确保全链路服务部署在同一可用区或机房内,包括缓存、数据库、依赖服务等。通过同机房调用规避跨机房网络延迟(RTT),尤其在多级调用链中,累计收益显著。

6)构建流量漏斗机制

前置过滤无效或非目标流量,防止其进入核心处理逻辑。

  • 使用布隆过滤器(Bloom Filter)快速判断请求是否命中有效集合;
  • 对热点请求结果提前计算并缓存,实现“查表即得”;通过漏斗设计,减少下游模块的无效计算与资源争抢。

7)批量处理降低调用频次

对于缓存或远程服务调用,优先采用批量模式。例如使用 Redis Pipeline 批量获取多个 Key 的值,或将用户相关数据通过 HashTag 聚合到同一分片,实现一次批量拉取,显著降低网络往返次数(RTT)。

8)合理利用内存加速访问

将高频读取、低更新频率的数据常驻内存(如 ConcurrentHashMapCaffeine 等)实现 O(1) 查询,彻底摆脱 I/O 依赖。

9)选用合适的数据结构与集合大小

避免盲目使用通用集合类,特别是在大数据量场景下。

  • 应预估数据规模,合理设置初始容量,减少动态扩容开销;
  • 必要时使用数组、位图、自定义结构替代标准集合,提升性能确定性。
  • 减少 copy 操作,减少序列化和反序列化操作——大字符串转对象;
  • addAllputAll 等操作可能引发大量对象拷贝与扩容消耗大量 CPU
  • 采用 Trove 库进行集合层优化等更好的内存结构:
    • 替换 HashMapTIntObjectHashMap(减少 50% entry 内存开销)
    • ArrayListTIntArrayList(消除 Integer 装箱消耗) 经压测验证,集合操作性能提升40%YGC 频率降低 25%
数据结构/次数 1w次匹配 2w次匹配 3w次匹配 4w匹配
list 37.81ms 71.69ms 93.41ms 127.63ms
bitMap 10.86ms 20.53ms 29.25ms 31.29ms

10)建立快速失败机制

尽早识别并拦截无法继续处理的请求,避免资源浪费。

  • 将校验逻辑前移(如参数合法性、权限、限流);
  • 结合流量漏斗,在入口层完成无效请求过滤;
  • 设计中断路径,允许流程在任意环节快速退出,提升系统整体吞吐。

11)合理使用并行处理

对可并行化的任务(如多数据源查询、独立规则判断),采用多线程或 Java 21 虚拟线程(Virtual Threads)并发执行。 但需注意:避免过度创建线程,防止主线程被阻塞或上下文切换开销过大,应结合任务类型与资源配比审慎设计。

12)异步化非关键路径

将非核心、非强依赖的操作异步化处理,如日志记录、缓存更新、消息通知等,通过补偿机制应对:

  • 缓存未更新 → 下次读取时回源重建;
  • 消息丢失 → 基于定时任务兜底补发;异步化是解耦与提效的重要手段,不应因惧怕失败而放弃。

13)实现链路透传

在分布式调用链中传递上下文信息(如 traceId、userId、requestId),便于全链路追踪、监控定位与灰度控制。这是可观测性建设的基础能力,也是高效排障的关键保障。

14)以空间换时间

通过本地存储换取计算或调用时间。典型场景:外部公安接口查询用户身份信息 → 在本地缓存或数据库中保存已调用结果,下次直接命中返回,避免重复调用高延迟外部系统。

15)复用临时对象

  • 频繁创建 → 高频 GC:每次 new 都会在堆上分配内存,尤其在新生代(Young Generation)中快速堆积对象。
  • 临时对象生命周期短:使用一次后即无引用,很快成为垃圾,但数量多时会触发 Young GC,甚至 Full GC
  • CPU资源竞争GC 线程与业务线程争抢 CPU,导致吞吐下降、响应延迟。

反例:

//模拟循环处理1000条数据
for (int i = 0; i < 1000; i++) {
    //致命问题:每次循环都new一个新的Order对象,堆内存创建1000个对象
    Order order = new Order();
    order.setId(i);
    order.setOrderName("订单-" + i);
    //业务处理:仅使用一次,之后order引用失效,对象变为垃圾
    processOrder(order);
}

正例:对于循环中功能不变、内容可复用的对象,将对象的创建语句从循环内部提取到循环外部;只创建1个对象实例,在整个循环中反复复用这同一个对象,从创建 N 个对象变为创建1个对象,堆内存占用直接减少 N-1 个,彻底杜绝这类内存浪费。

//核心优化:把对象创建放在循环外面,只创建1次
Order order = new Order();
for (int i = 0; i < 1000; i++) {
    // 复用同一个对象,仅修改对象的属性值即可
    order.setId(i);
    order.setOrderName("订单-" + i);
    processOrder(order);
}

16)千万级遍历减少 ` CPU` 占用

  • 核心逻辑:把千万条数据拆成小批次(比如 1000 条 / 批),每处理完一批就休眠 10ms,让线程放弃 CPU,避免持续占满核心;
  • 参数调整CPU 仍高→减小batchSize(比如 500)或增大 sleepMs(比如 20);速度太慢→反向调整;
  • 替换点:把代码中// 【替换这里】的部分换成你的实际业务逻辑(比如数据校验、入库等)。
  • 总结

    1. 核心降 CPU 手段是分批 + 休眠,通过 Thread.sleep()主动释放 CPU;
    2. 只需调整batchSize sleepMs两个参数,即可平衡 “速度” 和 “CPU 占用”;
      1. 实际使用时仅需替换数据来源和业务逻辑部分,其余代码无需改动。
  • 优化:建议用分批次 内部请求限流

    维度 sleep 分批 Guava RateLimiter
    CPU 控制效果 好(但波动大) 更好(平滑稳定)
    吞吐可控性 差(依赖 batch/sleep 比例) 高(直接指定 QPS)
    实现复杂度 极简 简单(几行代码)
    适用场景 离线批处理、容忍延迟波动 在线服务、需稳定负载
import java.util.ArrayList;
import java.util.List;

public class LowCpuTraversal {
    public static void main(String[] args) {
        // 1. 模拟千万级数据(替换成你的实际数据源)
        List<Long> dataList = generateData(10_000_000);
        
        // 2. 核心:低CPU遍历(批次大小1000,每批休眠10ms)
        lowCpuTraverse(dataList, 1000, 10);
    }

    // 生成模拟数据(实际场景替换为你的数据获取逻辑)
    private static List<Long> generateData(int size) {
        List<Long> list = new ArrayList<>(size);
        for (long i = 0; i < size; i++) list.add(i);
        return list;
    }

    /**
     * 低CPU遍历核心方法
     * @param dataList 待遍历数据
     * @param batchSize 每批处理条数(越小越省CPU)
     * @param sleepMs 每批休眠毫秒数(越大越省CPU)
     */
    private static void lowCpuTraverse(List<Long> dataList, int batchSize, long sleepMs) {
        int total = dataList.size();
        int processed = 0;

        while (processed < total) {
            // 处理当前批次
            int end = Math.min(processed + batchSize, total);
            for (int i = processed; i < end; i++) {
                // 【替换这里】你的实际业务逻辑
                long data = dataList.get(i);
                if (i % 1000000 == 0) System.out.println("处理到:" + i + "/" + total);
            }
            processed = end;

            // 核心:每批处理完休眠,释放CPU
            Uninterruptibles.sleepUninterruptibly(sleepMs, TimeUnit.MILLISECONDS);
        }
        System.out.println("遍历完成!");
    }
}

3、减少 GC 基本:减少系统抖动,提升运行稳定性

垃圾回收(GC)是 JVM 内存管理的基石,但频繁或长时间的 GC 停顿会直接导致系统 延迟升高、吞吐下降、服务抖动。除了常规的JVM 参数调优(如选择合适的 GC 算法、调整堆大小等),我们还需从代码和架构层面主动减少对象分配与生命周期负担。

手段:少创建、少复制、短存活、控范围少创建一点,GC 就轻松一分;早回收一秒,系统就稳定十分。

1)选用简洁高效的数据结构

优先使用轻量级、低开销的数据类型与容器。 原则:越简单的数据结构,GC 成本越低。

  • 能用基本类型(int)就不用包装类(Integer);
    • 设置合适大小的数据类型,例如 字段范围值只有1-10,设置字段类型时设置为byte类型,而非Long类型或Integer类型。
    • 基本类型是为了高效:直接操作数值,避免对象开销。
    • 只有对象才有引用:引用的本质是“指向堆中某个对象的句柄或地址”。
  • 能用 int 就不必盲目升级为 long(尤其在集合中大量存储时,空间差异显著)

  • 非必要不使用复杂结构如 MapList 嵌套,避免过度封装带来的内存膨胀与 GC 压力。

  • 尽量减少对象中存在空值的情况:假设我们的业务对象是一个包含100个字段的聚合对象,其中30个字段是特定于a场景的。如果系统中有 100 万个该对象的实例,并且其中70万个实例不属于a场景,那么这70万个实例将各自有30个字段值为空。以每个对象引用占用8字节的计算方式,空值字段将导致160.15MB的内存被无效占用。

  • **尽量不要用 NULL **

    • NULL(或 null)是一个特殊的值,表示“没有引用任何对象”。

    • 引用变量占内存,只是不指向对象

      • 64JVM(启用压缩指针时):每个引用字段占 4 字节

      • 数千万对象 × 4 字节 = 巨大开销

        Integer x = null; 占多少? 4 字节(引用变量)
        Integer x = 100; 占多少? 4 字节(引用) + 24 字节(堆对象) = 28 字节
        int x - 100 4字节

2)避免不必要的对象复制与扩容

集合类的动态扩容是隐藏的“内存杀手”。例如 ArrayListHashMap 在容量不足时会触发数组重建,产生大量临时对象并引发年轻代 GC

  • 合理预设初始容量( initialCapacity ),避免频繁扩容;
  • 尽量减少 addAllputAll 等批量拷贝操作,特别是大数据集场景;
  • 对只读集合,考虑使用不可变集合(如 GuavaImmutableList)避免后续修改开销。

3)控制大对象的创建与生命周期

大对象(通常指超过普通对象大小阈值,如 > 5KB )直接进入老年代,极易加剧 Full GC 频率。

目标:让大对象“生得晚、死得早”。

  • 拆分大对象:将一个巨型 POJO 拆分为多个小对象,分散内存压力;
  • 缩短生命周期:确保大对象使用后尽快失去引用,避免长期驻留;
  • 及时释放:使用完立即置空或通过作用域控制,帮助年轻代快速回收。

4)引入堆外内存(Off-Heap)技术

堆外内存不受 JVM GC管理,可有效隔离高频数据操作对GC的影响。

注意:需自行管理内存生命周期,防止内存泄漏。

  • 使用 OHCOff-Heap Cache)替代堆内缓存,性能更高、内存占用更低;
  • 自研或引入成熟的堆外组件,如笔者团队开发的 64 位堆外位图,支持存储十几亿用户标签数据,且不影响GC;
  • 适用于热点缓存、规则索引、状态存储等高频读写场景。

5)采用高效的数据压缩与编码方式

通过算法压缩数据体积,从根本上减少内存占用。

  • 使用 布隆过滤器(Bloom Filter 存储海量ID集合,空间效率极高;
  • 使用位图(Bitmap)或 Roaring Bitmap 表示布尔状态集合,压缩比可达数十倍;
  • 结合整型编码、差值压缩等技术进一步优化存储密度。

6)减少隐式类型转换与自动装箱

Java 中的自动装箱(Autoboxing)和类型转换会产生大量临时对象。

  • 避免在循环中进行 int ↔ Integer 转换;
  • 不要在泛型集合中混用基本类型与包装类;
  • 使用原始类型流(如 IntStream)替代 Stream<Integer> 处理数值序列;

这些看似微小的操作,在高并发下会累积成显著的 GC 压力。

  • 使用基本类型 long时,所有计算都在栈上或 CPU 寄存器中直接进行,不涉及任何对象的创建和销毁
    • 因此,整个循环过程的内存占用极低,并且执行速度也快得多。
  • 通过有意识地在编码中规避自动装箱,你可以写出更高效、内存占用更低的 Java 代码。

7)主动执行可控的 Full GC(谨慎使用)

在极少数对延迟极度敏感的场景下,可通过计划性 Full GC 主动清理老年代,换取后续更长时间的稳定运行。

警告:此为“双刃剑”操作,仅建议在充分监控、明确收益的前提下由资深人员实施。

  • 启动后清理:服务启动完成、数据预热结束后,执行一次 Full GC,清除初始化阶段产生的“一次性”垃圾;
  • 夜间低峰维护:每日凌晨4点左右,对生产机器分批随机执行 Full GC(需提前摘除流量,如下线 JSF 注册);
  • 执行期间必须确保无外部请求接入,否则会导致明显的 STW 抖动。

8)对象池

对象池可以在某些情况下减轻 GC 的压力,提高应用程序的性能和响应性。但同时也需要注意对象池可能带来的内存占用增加和并发GC 的影响。合理设计和使用对象池是关键。

  • 减少 GC 压力:由于对象池中的对象被重复使用,而不是频繁创建和销毁,因此可以减少 GC 的工作量。特别是在高并发或频繁创建销毁对象的场景中,对象池可以显著降低 GC 的压力。

  • 延迟 GC:对象池中的对象通常不会立即被 GC 回收,因为它们被保留在池中,以便在需要时重复使用。这可能会导致一些对象在内存中停留更长时间,直到池中没有更多可用对象为止。

  • 内存占用:对象池需要在内存中维护一批对象,即使这些对象当前没有被使用。因此,对象池可能会增加应用程序的内存占用,特别是当池中对象的数量较大时。

  • 并发GC的影响:在使用并发 GCConcurrent Garbage Collection)时,对象池的效果可能会有所不同。

    • 并发 GC 可以在应用程序运行时进行垃圾回收,从而减少停顿时间。
    • 对象池可以帮助减少 GC 的工作量,但如果对象池中的对象太多,可能会增加并发 GC的压力。

9)“系统升级”-升级高版本jdk

jdk21 最新 gc 回收神器:分代 ZGC

1)超低延迟垃圾回收器,分代 ZGC 实现大内存堆亚毫秒级 STW 停顿,不超过10ms

2)支持 TB 级别的堆

3)停顿时间(STW)不会随着堆的大小增加而增加

4)压缩指针(-XX:+UseCompressedOops)降低每个对象头开销从16B12B

ContactAuthor