项目经验_之_高性能设计
前言
Github:https://github.com/HealerJean
一、高并发与高性能架构设计
- 技术高度,决定团队天花板:技术领导者的专业高度,在很大程度上决定了整个团队所能达到的技术天花板;
- 追求卓越,驱动持续突破:技术领导者对卓越的追求与对质量的坚持,是驱动团队持续突破、不断进化的关键引擎。
- 硬核领航,方能以身率众:技术领导者必须在技术深度上扎得稳、在技术广度上看得远、在技术敏感度上反应快、在技术创新度上敢为人先。唯有自身足够“硬核”,才能以身作则、凝聚人心,带领团队穿越复杂性迷雾,抵达技术与业务双赢的高地
1、根据流量规模设计系统
1)QPS 在 1万以下的系统设计
适用场景:内部系统、中后台服务、低频C端应用等。
对于中小流量场景(如几百至数千 QPS ),且对响应延迟要求不高的系统,推荐采用最简洁的架构:直接通过数据库进行 CRUD 操作。
若数据库面临较大读压力或存在性能瓶颈,可引入缓存层(如 Redis)作为前置缓冲,显著降低数据库负载。此外,适当使用多线程处理可进一步提升并发能力,满足基本性能需求。
2)QPS 达到数万至数十万的系统设计
关键策略:缓存前置、数据预热、读写分离。
当系统面临较高并发(如几万到几十万 QPS ),性能要求较高时,应优先考虑 以缓存为核心的读服务架构。
现代缓存集群(如 Redis Cluster)已具备支撑百万级 QPS 的能力。建议将热点数据提前预加载至缓存中,服务层直接从缓存获取数据,避免频繁访问数据库。
若一致性要求允许短暂延迟,完全可以通过缓存满足绝大部分请求。结合缓存 + 多线程处理,即可应对绝大多数高并发读场景。
3)QPS 超过百万级的系统设计
面对百万级甚至更高的请求流量,且对延迟极为敏感的“极限性能”场景,必须追求极致优化。此时,仅靠缓存已难以满足要求,需转向内存级数据访问。
具体做法:
- 在服务启动时,将必要数据全量或增量加载至
JVM堆内存或堆外内存; - 核心链路尽可能基于内存完成计算与查询,减少 I/O 开销;
- 对于依赖外部
RPC调用的数据,评估是否可提前同步至本地内存,规避远程调用延迟。
值得注意的是,在内存计算已能将单次处理时间控制在毫秒级(如 5ms 内)的情况下,不一定需要引入多线程。相反,采用串行化处理可能更稳定,避免因线程切换、锁竞争带来的系统抖动和复杂性。
2、性能提升
在高并发系统中,性能瓶颈往往不在于单点能力,而在于整体链路的累积延迟与资源消耗。以下是经过验证一些关键优化策略,按执行阶段与优化维度组织,逻辑不变,重在落地实效。
1)服务启动期充分初始化
充分利用服务启动窗口期,完成数据预加载与预计算,避免运行时重复计算,显著降低核心流程耗时。
2)精简网络传输报文
控制接口返回数据量,遵循“按需提供”原则。仅返回调用方申请的字段,杜绝冗余传输。结合字段裁剪、字符串长度压缩、内容序列化压缩(如 Protobuf、GZIP)等手段,有效降低网络带宽占用与反序列化开销。
3)减少远程网络调用
将高频访问且变更不频繁的 RPC 依赖数据,通过本地缓存或内存存储的方式固化。例如将配置表等数据加载至 JVM 内存,避免每次请求都发起远程调用,大幅降低依赖服务压力与响应延迟。
4)规避不必要的网络消耗
评估部分数据是否可通过本地化方式获取。例如将公共算法、规则引擎、码表信息打包为共享 Jar 包嵌入应用,替代远程查询接口。既消除网络往返,也减轻下游服务与中间件的负载。
5)优先同机房调用
确保全链路服务部署在同一可用区或机房内,包括缓存、数据库、依赖服务等。通过同机房调用规避跨机房网络延迟(RTT),尤其在多级调用链中,累计收益显著。
6)构建流量漏斗机制
前置过滤无效或非目标流量,防止其进入核心处理逻辑。
- 使用布隆过滤器(
BloomFilter)快速判断请求是否命中有效集合; - 对热点请求结果提前计算并缓存,实现“查表即得”;通过漏斗设计,减少下游模块的无效计算与资源争抢。
7)批量处理降低调用频次
对于缓存或远程服务调用,优先采用批量模式。例如使用 Redis Pipeline 批量获取多个 Key 的值,或将用户相关数据通过 HashTag 聚合到同一分片,实现一次批量拉取,显著降低网络往返次数(RTT)。
8)合理利用内存加速访问
将高频读取、低更新频率的数据常驻内存(如 ConcurrentHashMap、Caffeine 等)实现 O(1) 查询,彻底摆脱 I/O 依赖。
9)选用合适的数据结构与集合大小
避免盲目使用通用集合类,特别是在大数据量场景下。
- 应预估数据规模,合理设置初始容量,减少动态扩容开销;
- 必要时使用数组、位图、自定义结构替代标准集合,提升性能确定性。
- 减少
copy操作,减少序列化和反序列化操作——大字符串转对象; addAll、putAll等操作可能引发大量对象拷贝与扩容消耗大量CPU;- 采用
Trove库进行集合层优化等更好的内存结构:- 替换
HashMap→TIntObjectHashMap(减少50%entry内存开销) ArrayList→TIntArrayList(消除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);速度太慢→反向调整; - 替换点:把代码中
// 【替换这里】的部分换成你的实际业务逻辑(比如数据校验、入库等)。 -
总结
- 核心降
CPU手段是分批 + 休眠,通过Thread.sleep()主动释放 CPU; - 只需调整
batchSize和sleepMs两个参数,即可平衡 “速度” 和 “CPU 占用”;- 实际使用时仅需替换数据来源和业务逻辑部分,其余代码无需改动。
- 核心降
-
优化:建议用分批次 内部请求限流
维度 sleep分批Guava RateLimiterCPU 控制效果 好(但波动大) 更好(平滑稳定) 吞吐可控性 差(依赖 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(尤其在集合中大量存储时,空间差异显著) -
非必要不使用复杂结构如
Map、List嵌套,避免过度封装带来的内存膨胀与GC压力。 -
尽量减少对象中存在空值的情况:假设我们的业务对象是一个包含100个字段的聚合对象,其中30个字段是特定于a场景的。如果系统中有
100万个该对象的实例,并且其中70万个实例不属于a场景,那么这70万个实例将各自有30个字段值为空。以每个对象引用占用8字节的计算方式,空值字段将导致160.15MB的内存被无效占用。 -
**尽量不要用
NULL**-
NULL(或null)是一个特殊的值,表示“没有引用任何对象”。 -
引用变量占内存,只是不指向对象
-
在
64位JVM(启用压缩指针时):每个引用字段占 4 字节 -
数千万对象 ×
4字节 = 巨大开销Integer x = null;占多少?4 字节(引用变量) Integer x = 100;占多少?4 字节(引用) + 24 字节(堆对象) = 28 字节 int x - 100 4字节
-
-
2)避免不必要的对象复制与扩容
集合类的动态扩容是隐藏的“内存杀手”。例如
ArrayList、HashMap在容量不足时会触发数组重建,产生大量临时对象并引发年轻代GC。
- 合理预设初始容量(
initialCapacity),避免频繁扩容; - 尽量减少
addAll、putAll等批量拷贝操作,特别是大数据集场景; - 对只读集合,考虑使用不可变集合(如
Guava的ImmutableList)避免后续修改开销。
3)控制大对象的创建与生命周期
大对象(通常指超过普通对象大小阈值,如 >
5KB)直接进入老年代,极易加剧FullGC频率。
目标:让大对象“生得晚、死得早”。
- 拆分大对象:将一个巨型
POJO拆分为多个小对象,分散内存压力; - 缩短生命周期:确保大对象使用后尽快失去引用,避免长期驻留;
- 及时释放:使用完立即置空或通过作用域控制,帮助年轻代快速回收。
4)引入堆外内存(Off-Heap)技术
堆外内存不受
JVMGC管理,可有效隔离高频数据操作对GC的影响。
注意:需自行管理内存生命周期,防止内存泄漏。
- 使用
OHC(Off-HeapCache)替代堆内缓存,性能更高、内存占用更低; - 自研或引入成熟的堆外组件,如笔者团队开发的
64位堆外位图,支持存储十几亿用户标签数据,且不影响GC; - 适用于热点缓存、规则索引、状态存储等高频读写场景。
5)采用高效的数据压缩与编码方式
通过算法压缩数据体积,从根本上减少内存占用。
- 使用 布隆过滤器(
BloomFilter) 存储海量ID集合,空间效率极高; - 使用位图(
Bitmap)或RoaringBitmap表示布尔状态集合,压缩比可达数十倍; - 结合整型编码、差值压缩等技术进一步优化存储密度。
6)减少隐式类型转换与自动装箱
Java 中的自动装箱(Autoboxing)和类型转换会产生大量临时对象。
- 避免在循环中进行
int ↔ Integer转换; - 不要在泛型集合中混用基本类型与包装类;
- 使用原始类型流(如
IntStream)替代Stream<Integer>处理数值序列;
这些看似微小的操作,在高并发下会累积成显著的 GC 压力。
- 使用基本类型
long时,所有计算都在栈上或CPU寄存器中直接进行,不涉及任何对象的创建和销毁- 因此,整个循环过程的内存占用极低,并且执行速度也快得多。
- 通过有意识地在编码中规避自动装箱,你可以写出更高效、内存占用更低的
Java代码。
7)主动执行可控的 Full GC(谨慎使用)
在极少数对延迟极度敏感的场景下,可通过计划性
FullGC主动清理老年代,换取后续更长时间的稳定运行。
警告:此为“双刃剑”操作,仅建议在充分监控、明确收益的前提下由资深人员实施。
- 启动后清理:服务启动完成、数据预热结束后,执行一次
FullGC,清除初始化阶段产生的“一次性”垃圾; - 夜间低峰维护:每日凌晨4点左右,对生产机器分批随机执行
FullGC(需提前摘除流量,如下线JSF注册); - 执行期间必须确保无外部请求接入,否则会导致明显的
STW抖动。
8)对象池
对象池可以在某些情况下减轻
GC的压力,提高应用程序的性能和响应性。但同时也需要注意对象池可能带来的内存占用增加和并发GC的影响。合理设计和使用对象池是关键。
-
减少
GC压力:由于对象池中的对象被重复使用,而不是频繁创建和销毁,因此可以减少GC的工作量。特别是在高并发或频繁创建销毁对象的场景中,对象池可以显著降低GC的压力。 -
延迟
GC:对象池中的对象通常不会立即被GC回收,因为它们被保留在池中,以便在需要时重复使用。这可能会导致一些对象在内存中停留更长时间,直到池中没有更多可用对象为止。 -
内存占用:对象池需要在内存中维护一批对象,即使这些对象当前没有被使用。因此,对象池可能会增加应用程序的内存占用,特别是当池中对象的数量较大时。
-
并发
GC的影响:在使用并发GC(Concurrent Garbage Collection)时,对象池的效果可能会有所不同。- 并发
GC可以在应用程序运行时进行垃圾回收,从而减少停顿时间。 - 对象池可以帮助减少
GC的工作量,但如果对象池中的对象太多,可能会增加并发GC的压力。
- 并发
9)“系统升级”-升级高版本jdk
jdk21最新gc回收神器:分代ZGC
1)超低延迟垃圾回收器,分代 ZGC 实现大内存堆亚毫秒级 STW 停顿,不超过10ms
2)支持 TB 级别的堆
3)停顿时间(STW)不会随着堆的大小增加而增加
4)压缩指针(-XX:+UseCompressedOops)降低每个对象头开销从16B→12B


