Redis高可用与集群模式:主从、Sentinel和Cluster

为什么要区分高可用与分片

Redis 一开始通常是从单机用起的:缓存商品详情、存登录 token、做计数器、做分布式锁。等系统访问量上来以后,问题就来了:单机内存不够怎么办?主节点挂了怎么办?读压力太高怎么办?业务需要更高可用时怎么做?

这时就要考虑 Redis 的集群模式。这里的“集群”不只是 Redis Cluster 一种,而是一组部署方式:主从复制、哨兵模式、Redis Cluster、代理分片,以及云托管 Redis。每种方式解决的问题不一样,优缺点也不一样。

项目选型时不要先问“哪种最先进”,而要先问:我的数据量有多大?QPS 有多高?能不能接受短暂不可用?团队有没有 Redis 运维经验?业务是否依赖跨 key 操作?

Redis集群模式选型流程

四种部署形态及其能力边界

Redis 常见部署方式可以按能力逐层理解。

单机模式最简单,适合开发环境、小系统、低风险缓存。但单点故障明显,内存和吞吐也受单机限制。

主从复制解决的是读扩展和数据备份。主节点负责写,从节点复制数据,可以承担部分读请求。但主节点挂了以后,是否自动切换取决于外部机制。

哨兵模式解决的是高可用。Sentinel 会监控主从节点,主节点异常时自动选举新的主节点,并通知客户端切换。它适合数据量还没有大到必须分片,但又需要自动故障转移的系统。

Redis Cluster 解决的是水平扩展。它把 key 映射到 16384 个 hash slot,再把 slot 分布到多个主节点上。容量和吞吐可以通过增加节点扩展,但客户端、跨 slot 操作和运维复杂度都会提高。

代理分片和云托管 Redis 解决的是工程复杂度。代理可以屏蔽后端分片,云托管可以减少运维成本。但要关注命令兼容性、网络延迟、成本和故障透明度。

选型方法与迁移路径

如果是一个普通后台系统,Redis 只用来缓存字典、商品详情、用户权限,数据量不大,故障后可以短暂回源数据库,那么主从或哨兵模式通常就够了。

例如 ERP 系统里,商品基础资料、供应商列表、组织架构权限可以放 Redis 缓存。即使 Redis 短暂不可用,也可以降级查数据库,只是慢一点。这种场景不一定要上 Redis Cluster。

如果系统对可用性要求高,但数据量还在单机内存范围内,可以选择 Sentinel。比如登录 token、会话信息、限流计数这类数据,业务希望节点故障后能自动切换,哨兵模式就比较合适。

如果数据量和访问量都明显增长,比如电商商品缓存、库存热点、活动库存、推荐结果缓存,单机内存和 QPS 已经吃紧,就要考虑 Redis Cluster。Cluster 的好处是容量可以横向扩展,坏处是开发要注意 hash slot。

比如下面这种多 key 操作:

1
MGET product:1001 product:1002 product:1003

在 Cluster 下,如果这些 key 落在不同 slot,可能无法像单机那样直接执行。可以通过 hash tag 把相关 key 放到同一个 slot:

1
2
stock:{sku1001}:available
stock:{sku1001}:locked

但 hash tag 也不能乱用。如果所有 key 都强行放到一个 slot,分片就失去意义。

如果团队 Redis 运维能力弱,又不想自己处理扩容、备份、故障切换,可以考虑云托管 Redis。云服务能减少大量运维工作,但成本、规格限制、网络链路、版本兼容要提前评估。

我做选型时一般按这个顺序问:

  1. Redis 只是缓存,还是承载关键状态?
  2. 单机内存能不能装下未来一年数据?
  3. 读写 QPS 是否需要水平扩展?
  4. 是否大量使用 Lua、多 key、事务、pipeline?
  5. 团队是否有能力处理故障切换和扩容?
  6. 可接受的成本和可用性目标是什么?

运维风险与一致性限制

第一个坑,是一上来就上 Cluster。Cluster 能扩容,但复杂度也高。小系统如果只是缓存几万条基础资料,用 Cluster 反而会增加开发和运维负担。

第二个坑,是把主从当高可用。主从复制不等于自动故障切换。没有哨兵或外部切换机制,主节点挂了仍然需要人工处理。

第三个坑,是忽略数据一致性。Redis 复制通常是异步的,主节点刚写入还没同步给从节点时发生故障,可能会丢一小段数据。关键业务不能把 Redis 当唯一事实来源。

第四个坑,是跨 slot 操作太多。Cluster 选型前要盘点代码里是否大量使用 MGETMSET、Lua、事务和批量删除。否则上线后会发现很多命令不再好用。

第五个坑,是只看 QPS 不看热点 key。即使是 Cluster,如果大量请求集中在一个超级热点 key 上,也可能打爆单个节点。热点数据要考虑本地缓存、拆 key、限流或预计算。

总结

Redis 集群模式没有绝对最好,只有适合当前阶段。数据量小、风险低,用单机或主从;需要自动故障切换,用 Sentinel;容量和吞吐需要横向扩展,用 Redis Cluster;团队想减少运维,可以考虑云托管。

选型时最重要的是把 Redis 在系统里的角色想清楚。它是缓存、锁、队列、会话,还是关键状态存储?角色不同,容灾、持久化、扩容和一致性要求也完全不同。

JVM线上排查:FullGC、OOM和CPU飙高怎么定位

JVM 知识最终要落到线上排查。供应链系统通常链路长、依赖多、峰值明显,一次订单高峰可能同时触发接口变慢、消息堆积、GC 频繁、CPU 飙高。排查时不能只看某一个指标,而要把 JVM、业务流量、线程、内存和日志串起来。

整体流程图

JVM 线上排查流程

必须掌握的工具

常用 JVM 排查工具包括:

  1. jps:查看 Java 进程。
  2. jstat:查看 GC、类加载、编译等统计。
  3. jstack:导出线程栈,定位死锁、阻塞、CPU 高。
  4. jmap:查看堆概要、导出 heap dump。
  5. jcmd:JDK 7 以后推荐的综合诊断工具。
  6. JFR:低开销运行时事件记录,适合分析 CPU、锁、IO、GC。
  7. Arthas:线上 Java 诊断工具,适合查看方法调用、耗时、类加载、对象信息。

生产环境使用这些工具要遵守权限和变更规范。导出堆 dump 可能造成磁盘压力,jmap -dump 也可能让进程短暂停顿,必须评估影响。

场景一:Full GC 频繁

假设订单服务在大促期间频繁 Full GC,接口 P99 明显升高。第一步先确认 GC 情况:

1
2
jps -l
jstat -gcutil <pid> 1000 10

重点看 YGCYGCTFGCFGCTO。如果老年代使用率长期接近 100%,并且 Full GC 后下降不明显,说明很多对象仍然可达。

继续导出堆直方图:

1
jcmd <pid> GC.class_histogram | head -n 40

如果发现大量订单上下文对象:

1
2
3
num     #instances         #bytes  class name
1 1250000 180000000 com.example.order.OrderContext
2 980000 120000000 com.example.promotion.PromotionResult

就要回到业务代码查缓存、静态集合、消息重试队列、批处理暂存区。供应链系统常见原因是把订单处理上下文放入本地缓存,但没有过期时间或清理机制。

1
2
3
4
5
6
7
public class BadOrderContextCache {
private static final Map<String, OrderContext> CACHE = new ConcurrentHashMap<>();

public void put(OrderContext context) {
CACHE.put(context.getOrderNo(), context);
}
}

这类代码应该替换成有容量和过期策略的缓存,或者把上下文持久化到 Redis、数据库,并按业务状态清理。

场景二:OOM

OOM 发生时,先看异常类型:

1
2
3
4
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: Metaspace
java.lang.OutOfMemoryError: Direct buffer memory
java.lang.OutOfMemoryError: unable to create native thread

建议生产默认开启:

1
2
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump

如果是 Java heap space,使用 MAT、VisualVM 或 JProfiler 分析 dump,重点看 Dominator Tree 和最大保留对象。如果是 Metaspace,检查动态类、代理、脚本、热部署。如果是 Direct buffer memory,检查 Netty、NIO、文件导入导出。如果是 unable to create native thread,检查线程池是否无界、容器 pids 限制和 -Xss

供应链报表导出里的典型 OOM:

1
2
3
4
5
public byte[] exportAllOutboundOrders() {
List<OutboundOrder> orders = outboundRepository.findAll();
Workbook workbook = excelWriter.write(orders);
return workbook.toByteArray();
}

这个实现同时把全量订单和完整 Excel 放在内存里。更合理的方式是流式查询和流式写出:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void exportOutboundOrders(OutputStream outputStream) {
try (StreamingExcelWriter writer = new StreamingExcelWriter(outputStream)) {
long lastId = 0L;
while (true) {
List<OutboundOrder> page = outboundRepository.findPage(lastId, 1000);
if (page.isEmpty()) {
break;
}
writer.writeRows(page);
lastId = page.get(page.size() - 1).getId();
}
}
}

场景三:CPU 飙高

CPU 高时,不能只重启。要先找到高 CPU 线程:

1
2
3
4
top -Hp <pid>
printf "%x\n" <tid>
jstack <pid> > /tmp/order-service.jstack
grep -n "<hexTid>" /tmp/order-service.jstack

如果线程栈显示大量时间消耗在价格计算、促销匹配、库存路由,可以继续结合业务日志和方法耗时定位。常见原因包括死循环、递归层级过深、正则表达式回溯、JSON 大对象序列化、锁竞争导致线程忙等。

例如库存路由中错误的递归:

1
2
3
4
5
6
7
public WarehouseRoute findRoute(String skuCode, String warehouseCode) {
WarehouseRoute route = routeRepository.find(skuCode, warehouseCode);
if (route == null) {
return findRoute(skuCode, fallbackWarehouse(warehouseCode));
}
return route;
}

如果 fallbackWarehouse() 在某些配置下返回原仓库编码,就会无限递归,最终 CPU 飙高或栈溢出。生产代码要增加访问集合和终止条件。

排查顺序建议

一次线上 JVM 故障可以按这个顺序处理:

  1. 先止血:限流、摘流量、扩容、暂停批处理,保护核心交易链路。
  2. 保留现场:保存 GC 日志、业务日志、线程 dump、必要时保存 heap dump。
  3. 判断类型:CPU、内存、GC、线程、类加载、IO 还是外部依赖。
  4. 缩小范围:结合发布时间、流量峰值、任务调度、业务操作。
  5. 验证根因:用 dump、日志、监控和代码路径互相印证。
  6. 修复并复盘:补监控、补限流、补压测、补容量评估。

JVM 排查能力的本质,是把现象变成证据,把证据映射到代码和业务。对供应链系统来说,订单高峰、仓储波次、库存同步、报表导出都可能触发 JVM 问题,排查时要同时看业务峰值和运行时状态。

JVM参数调优:供应链服务堆内存和GC怎么配置

JVM 调优不是把网上的参数复制到生产环境。真正的调优要从业务目标出发:接口延迟、吞吐量、内存成本、GC 停顿、故障恢复速度。供应链服务通常既有在线接口,也有批处理任务,二者对 JVM 参数的要求不同,不能使用同一套配置机械套用。

整体流程图

JVM 参数调优流程

调优前先明确目标

调 JVM 参数之前,至少要回答四个问题:

  1. 服务类型是什么:在线订单接口、库存查询、仓储波次、报表导出、定时批处理。
  2. 核心指标是什么:P99 延迟、吞吐量、任务完成时间、内存占用、稳定性。
  3. 当前瓶颈在哪里:GC 停顿、CPU、数据库、网络、锁竞争、对象分配过快。
  4. 有哪些证据:GC 日志、堆 dump、线程 dump、监控曲线、压测报告。

没有证据的调优很容易变成参数玄学。JVM 参数只能解决 JVM 层面的问题,不能替代 SQL 优化、缓存设计、批处理拆分和业务限流。

常用参数分类

堆内存相关:

1
2
3
4
-Xms2g
-Xmx2g
-XX:NewRatio=2
-XX:MaxMetaspaceSize=512m

GC 相关:

1
2
3
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45

线程栈和直接内存:

1
2
-Xss512k
-XX:MaxDirectMemorySize=512m

诊断相关:

1
2
3
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
-Xlog:gc*,safepoint:file=/data/logs/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100M

容器相关要关注 JVM 对 cgroup 的识别能力。JDK 8 早期版本和 JDK 10 以后的行为不同,生产环境要确认基础镜像和 JDK 版本。

在线订单服务配置示例

在线订单服务关注接口延迟和稳定性。可以采用固定堆大小,减少运行时扩缩容带来的抖动:

1
2
3
4
5
6
7
8
9
10
11
12
JAVA_OPTS="
-Xms2g
-Xmx2g
-Xss512k
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=40
-XX:MaxMetaspaceSize=512m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/order-service
-Xlog:gc*,safepoint:file=/data/logs/order-service/gc.log:time,uptime,level,tags:filecount=10,filesize=100M
"

这个配置适合中等规模的 Spring Boot 服务起步使用。实际值要根据容器内存、对象分配速率、流量峰值和压测结果调整。例如容器限制 3Gi 内存时,不能把 -Xmx 设置成 3g,因为元空间、线程栈、直接内存、JIT、JNI、系统开销也需要内存。

批处理服务配置示例

库存重算、订单归档、对账导出更关注吞吐和任务完成时间。批处理服务可以接受较长 GC 停顿,但要避免内存峰值导致 OOM:

1
2
3
4
5
6
7
8
9
10
11
JAVA_OPTS="
-Xms4g
-Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=500
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/reconcile-job
-Xlog:gc*:file=/data/logs/reconcile-job/gc.log:time,uptime,level,tags:filecount=5,filesize=200M
"

但批处理的第一优化点仍然是分批。下面这种一次性加载全量库存的写法不适合生产:

1
2
3
4
5
6
7
public void rebuildInventorySnapshot() {
List<InventoryRecord> all = inventoryRepository.findAll();
List<InventorySnapshot> snapshots = all.stream()
.map(this::toSnapshot)
.collect(Collectors.toList());
snapshotRepository.batchSave(snapshots);
}

更合理的是按主键或时间窗口拆分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void rebuildInventorySnapshot() {
long lastId = 0L;
while (true) {
List<InventoryRecord> page = inventoryRepository.findPage(lastId, 5000);
if (page.isEmpty()) {
break;
}
List<InventorySnapshot> snapshots = page.stream()
.map(this::toSnapshot)
.collect(Collectors.toList());
snapshotRepository.batchSave(snapshots);
lastId = page.get(page.size() - 1).getId();
}
}

参数调优和代码优化要配合。否则即使堆从 2g 调到 8g,也只是延后 OOM 或把 Full GC 停顿变得更长。

G1 调优注意事项

G1 常用调优思路:

  1. 先设置合理的 -Xms-Xmx,生产服务通常设置为相同值。
  2. 通过 MaxGCPauseMillis 给 JVM 一个目标,但它不是强制保证。
  3. 如果老年代增长过快,可以适当降低 InitiatingHeapOccupancyPercent,让并发标记更早开始。
  4. 注意 Humongous Object,大对象会占用连续 Region,可能引发额外回收压力。
  5. 不要轻易手动设置过多 G1 细节参数,先用 GC 日志验证问题。

调优闭环

一次合格的 JVM 调优应该形成闭环:

  1. 记录问题:接口 P99 从 200ms 上升到 2s,GC 停顿明显增加。
  2. 收集证据:GC 日志、堆使用曲线、对象分配热点、线程 dump。
  3. 提出假设:订单波次分组产生大量临时对象,新生代回收频繁。
  4. 小步调整:分页处理、降低对象峰值、调整堆大小或 G1 触发阈值。
  5. 压测验证:对比吞吐、延迟、GC 次数、停顿时间。
  6. 灰度发布:观察真实流量下的指标。

JVM 参数不是越多越专业。供应链系统的关键是把业务峰值、对象生命周期和 JVM 证据连起来,用最少的参数解决明确的问题。

垃圾收集器与GC日志:仓储高峰期如何看懂停顿

GC 日志是 JVM 调优和线上排查的第一手证据。接口变慢时,不能只看业务日志里的耗时,还要判断是否发生了 Stop The World、Full GC、老年代空间不足、晋升失败或大对象分配。供应链系统在大促、仓库波次下发、订单集中履约时对象创建速度很快,GC 日志能帮助我们把问题从“感觉慢”变成可量化的事实。

整体流程图

GC 日志分析流程

常见垃圾收集器

不同 JDK 版本和业务场景会使用不同收集器:

  1. Serial GC:单线程收集,适合小内存客户端程序,不适合高并发服务。
  2. Parallel GC:吞吐优先,适合批处理任务,例如夜间库存重算、历史订单归档。
  3. CMS:低停顿老年代收集器,JDK 9 后被标记废弃,JDK 14 移除。
  4. G1:面向服务端低停顿场景,JDK 9 以后成为默认收集器,适合大多数订单、库存、仓储服务。
  5. ZGC:低停顿收集器,适合大堆和低延迟服务,具体使用要结合 JDK 版本和生产验证。

如果没有特殊原因,现代 Spring Boot 服务通常优先使用 G1。它把堆划分为多个 Region,通过预测停顿时间选择回收集合,目标是在可控停顿下获得稳定吞吐。

仓储高峰期 Demo

假设 WMS 在晚上 8 点下发波次任务,订单服务要把 30 万个待出库订单按仓库、承运商、优先级分组:

1
2
3
4
5
6
7
8
9
10
11
public class WaveDispatchService {
public List<WaveGroup> buildWave(List<OrderLine> lines) {
Map<String, List<OrderLine>> grouped = lines.stream()
.collect(Collectors.groupingBy(line ->
line.getWarehouseCode() + ":" + line.getCarrierCode()));

return grouped.entrySet().stream()
.map(entry -> new WaveGroup(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
}

这段代码业务上清晰,但在高峰期会创建大量临时对象:分组 key、Map 节点、List、Stream 中间对象、WaveGroup。如果堆空间偏小或对象晋升过快,GC 停顿会明显增加。

如何打开 GC 日志

JDK 8 常用参数:

1
2
3
4
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintTenuringDistribution \
-Xloggc:/data/logs/order-service/gc.log

JDK 9 及以后推荐使用统一日志:

1
-Xlog:gc*,safepoint:file=/data/logs/order-service/gc.log:time,uptime,level,tags:filecount=10,filesize=100M

线上服务建议默认开启 GC 日志。日志文件滚动要配置好,否则长时间运行可能撑爆磁盘。

GC 日志重点看什么

分析 GC 日志时,不要只看有没有 GC,要看四类指标:

  1. 频率:Young GC、Mixed GC、Full GC 多久发生一次。
  2. 停顿时间:每次暂停多少毫秒,P95/P99 是否影响接口 SLA。
  3. 回收效果:GC 前后堆、老年代、元空间占用下降多少。
  4. 触发原因:Allocation Failure、Metadata GC Threshold、Humongous Allocation、System.gc() 等。

一段简化后的日志可能类似:

1
2
3
[2023-06-18T20:01:12.345+0800][info][gc] GC(42) Pause Young (Normal) 512M->180M(1024M) 35.7ms
[2023-06-18T20:03:44.210+0800][info][gc] GC(43) Pause Young (Concurrent Start) 760M->420M(1024M) 68.4ms
[2023-06-18T20:03:44.281+0800][info][gc] GC(44) Concurrent Mark Cycle

这说明 JVM 正在进行新生代回收,并启动并发标记。如果后面频繁出现 Full GC,且每次回收后老年代下降不明显,就要怀疑长生命周期对象过多或内存泄漏。

常见判断结论

GC 日志可以帮助形成明确结论:

  1. Young GC 频繁但停顿短:对象创建速度快,可能需要优化批处理对象分配,或者适当增大堆。
  2. Full GC 频繁且回收效果差:老年代长期占满,优先查缓存、静态集合、批量任务和大对象。
  3. 元空间触发 GC:检查动态代理、脚本引擎、热部署和类加载器泄漏。
  4. 大对象触发回收:检查一次性大数组、大 JSON、大 Excel 导出。
  5. 明确出现 System.gc():检查代码、第三方库或运维脚本是否主动触发 Full GC。

优化仓储波次的代码思路

对于前面的分组逻辑,可以从业务和代码两侧降压:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void dispatchWaveByPage(String waveNo) {
long lastId = 0L;
while (true) {
List<OrderLine> page = orderLineRepository.queryPage(waveNo, lastId, 2000);
if (page.isEmpty()) {
break;
}

Map<RouteKey, List<OrderLine>> grouped = groupByRoute(page);
waveRepository.saveGroups(grouped);
lastId = page.get(page.size() - 1).getId();

grouped.clear();
page.clear();
}
}

核心不是手动调用 GC,而是控制对象峰值:分页查询、分批落库、避免超大集合、减少无意义字符串拼接。GC 调优优先解决对象生命周期和分配速率,再调整 JVM 参数。

GC 日志的价值在于把性能问题证据化。对于供应链系统,订单高峰、仓储波次、库存同步都可能制造对象洪峰,只有把日志、业务峰值和代码路径结合起来看,才能得出可靠结论。

GC对象判定与回收算法:订单批处理对象如何被回收

GC 的第一步不是回收,而是判断哪些对象还活着。只有理解对象存活判定、引用类型和基础回收算法,才能解释为什么某些对象明明业务上已经不用了,却仍然无法被回收,也才能写出对批处理和高并发服务更友好的代码。

整体流程图

GC 对象判定与回收算法

对象如何被判定为存活

主流 JVM 使用可达性分析判断对象是否存活。它从一组 GC Roots 出发,沿着引用链向下搜索,能被搜索到的对象就是存活对象,搜索不到的对象就是可回收对象。

常见 GC Roots 包括:

  1. 虚拟机栈中局部变量引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. Native 方法引用的对象。
  5. 活跃线程对象、类加载器、同步锁持有对象等。

供应链服务中,一个订单导入任务创建了很多临时对象。如果这些对象只存在于方法局部变量里,任务结束后大概率可以被回收。但如果它们被放入静态集合、长生命周期缓存、线程本地变量中,就可能继续可达,导致内存无法释放。

订单批处理 Demo

下面是一个简化的订单导入任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OrderImportJob {
private final OrderRepository orderRepository;

public void importOrders(List<OrderRow> rows) {
List<OrderCommand> commands = new ArrayList<>(rows.size());

for (OrderRow row : rows) {
OrderCommand command = new OrderCommand(
row.getOrderNo(),
row.getSkuCode(),
row.getQuantity(),
row.getWarehouseCode()
);
commands.add(command);
}

orderRepository.batchCreate(commands);
}
}

commands 是局部变量。importOrders() 执行结束后,如果 batchCreate() 没有把这些对象保存到全局结构里,这批 OrderCommand 对象就会失去可达路径,后续 GC 可以回收。

下面这种写法就有风险:

1
2
3
4
5
6
7
8
9
10
public class BadOrderImportJob {
private static final List<OrderCommand> HISTORY = new ArrayList<>();

public void importOrders(List<OrderRow> rows) {
for (OrderRow row : rows) {
OrderCommand command = convert(row);
HISTORY.add(command);
}
}
}

HISTORY 是静态字段,属于 GC Roots 可达链的一部分。只要进程不退出,列表里的历史订单命令就一直可达。业务上即使导入完成,内存也不会自动释放。这类问题在导入、导出、报表、对账任务里很常见。

引用类型

Java 引用可以分为四类:

  1. 强引用:最常见,例如 new OrderCommand() 赋值给变量。只要强引用可达,GC 不会回收。
  2. 软引用:内存不足时可能被回收,可用于对内存敏感的缓存,但现代服务更推荐使用成熟缓存组件并设置容量。
  3. 弱引用:下一次 GC 发生时,只要没有强引用就会被回收,常见于 WeakHashMap
  4. 虚引用:不能通过引用拿到对象,主要用于跟踪对象回收状态。

供应链系统里不要简单依赖软引用实现核心缓存。库存、价格、路由规则这类数据更适合用 Caffeine、Redis、本地缓存加版本号等方式,并明确容量、过期和刷新策略。

基础回收算法

常见 GC 算法有三类:

  1. 标记-清除:先标记可回收对象,再清除它们。问题是会产生内存碎片。
  2. 标记-复制:把存活对象复制到另一块区域,再清理原区域。适合存活对象少的新生代。
  3. 标记-整理:标记后把存活对象向一端移动,减少碎片。适合老年代。

JVM 分代回收的基本依据是弱分代假说:大多数对象朝生夕死,少数对象会长期存活。供应链接口里的请求 DTO、计算中间对象、临时报表行通常很快死亡,适合在新生代快速回收;缓存、连接池、规则表、单例服务通常会长期存活,最终进入老年代。

让 GC 更轻松的编码方式

GC 可以自动回收内存,但不能替开发者修正错误的引用关系。实践中要注意:

  1. 批处理按批次提交并释放引用,不要把全量数据放进一个大集合。
  2. 静态集合只放真正需要全局共享的数据,并设置上限。
  3. ThreadLocal 使用后及时 remove(),尤其在线程池场景。
  4. 缓存必须有容量和过期策略。
  5. 大对象和大数组要谨慎,避免频繁进入老年代。

一个更稳妥的导入方式是分片处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void importOrders(Stream<OrderRow> rowStream) {
List<OrderCommand> batch = new ArrayList<>(1000);
rowStream.forEach(row -> {
batch.add(convert(row));
if (batch.size() == 1000) {
orderRepository.batchCreate(batch);
batch.clear();
}
});
if (!batch.isEmpty()) {
orderRepository.batchCreate(batch);
batch.clear();
}
}

这段代码把峰值对象数量限制在一个批次内,既降低堆内存压力,也减少 GC 扫描和复制的成本。对于订单导入、库存同步、仓储流水归档,这类批处理方式通常比一次性全量加载更稳定。