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); }