From cd055f8ccdd0615aef36a30439b815b505a9d385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Fri, 11 Sep 2026 16:09:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=86=85=E5=AD=98):=20=E4=B8=A4=E5=A4=84?= =?UTF-8?q?=E6=97=A0=E7=95=8C=E5=86=85=E5=AD=98=E9=A3=8E=E9=99=A9=E5=8A=A0?= =?UTF-8?q?=E5=9B=BA=E2=80=94=E2=80=94brand-check=20=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E6=B1=A0=E6=94=B9=E6=9C=89=E7=95=8C=E9=98=9F=E5=88=97+CallerRu?= =?UTF-8?q?ns=EF=BC=88=E5=A4=A7=E6=89=B9=E9=87=8F=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=97=A0=E7=95=8C=E5=A0=86=E7=A7=AF=20OOM?= =?UTF-8?q?=EF=BC=89=EF=BC=9BOSS=20readObjectBytes=20=E5=8A=A0=2020MB=20?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E4=B8=8A=E9=99=90=E9=98=B2=E5=A4=A7=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E8=AF=AF=E7=94=A8=20OOM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../brand/client/BrandCheckClient.java | 10 ++++++-- .../file/service/oss/OssStorageService.java | 23 ++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/client/BrandCheckClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/client/BrandCheckClient.java index 4ca929cf..e3f2090d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/client/BrandCheckClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/client/BrandCheckClient.java @@ -44,8 +44,14 @@ public class BrandCheckClient { this.externalCallMetrics = externalCallMetrics; } - private final ExecutorService checkExecutor = Executors.newFixedThreadPool( - BRAND_CHECK_CONCURRENCY, namedThreadFactory("brand-check")); + // 有界队列 + CallerRuns:大批量(几千品牌)检查时队列洪峰不无界堆内存, + // 队列打满时提交线程自己执行一个检查任务,上游天然限速。 + private final ExecutorService checkExecutor = new java.util.concurrent.ThreadPoolExecutor( + BRAND_CHECK_CONCURRENCY, BRAND_CHECK_CONCURRENCY, + 60L, java.util.concurrent.TimeUnit.SECONDS, + new java.util.concurrent.LinkedBlockingQueue<>(1000), + namedThreadFactory("brand-check"), + new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()); @PreDestroy void shutdownCheckExecutor() { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java index cf0da2d9..591f9edc 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java @@ -41,6 +41,8 @@ public class OssStorageService { private static final String DIGITAL_HUMAN_PREFIX = "digital-human/versions/"; private static final String SOFTWARE_VERSION_PREFIX = "nanri-image/versions/"; private static final String LEGACY_MINIO_ENDPOINT = "http://47.110.241.161:9000"; + /** readObjectBytes 兜底读取上限(20MB):防止大对象一次性读入内存导致 OOM。 */ + private static final long DEFAULT_READ_MAX_BYTES = 20L * 1024 * 1024; private final OssProperties ossProperties; private final MinioClient minioClient; @@ -248,13 +250,32 @@ public class OssStorageService { } public byte[] readObjectBytes(String bucket, String objectKey) { + // 防御性上限:防止被误用到大对象时一次性读入内存导致 OOM(当前仅模板下载场景,模板为 xlsx KB 级) + return readObjectBytes(bucket, objectKey, DEFAULT_READ_MAX_BYTES); + } + + public byte[] readObjectBytes(String bucket, String objectKey, long maxBytes) { String normalizedBucket = requireStorageName(bucket, "bucket"); String normalizedObjectKey = requireStorageName(objectKey, "objectKey"); try (var stream = buildClient().getObject(GetObjectArgs.builder() .bucket(normalizedBucket) .object(normalizedObjectKey) .build())) { - return stream.readAllBytes(); + byte[] buffer = new byte[1024 * 1024]; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + long total = 0; + int read; + while ((read = stream.read(buffer)) != -1) { + total += read; + if (total > maxBytes) { + throw new IllegalArgumentException("对象过大,超过读取上限 " + maxBytes + " 字节: " + + normalizedBucket + "/" + normalizedObjectKey); + } + out.write(buffer, 0, read); + } + return out.toByteArray(); + } catch (IllegalArgumentException ex) { + throw ex; } catch (Exception ex) { throw storageFailure("read", normalizedBucket + "/" + normalizedObjectKey, ex); }