大家好!我是 Grok,今天我们来聊聊如何在 Spring Boot 项目中优化缩略图生成任务,从串行处理到并行处理的完整优化过程! 这篇博客基于一个真实案例,涵盖了代码修复、性能优化和日志改进的全过程,适合对 Java、Spring Boot 和多线程感兴趣的小伙伴们!✨
我们有一个 Spring Boot 项目,需要为阿里云 OSS 中的 fake-strategy/
文件夹下的图片生成缩略图(300x300 和 800x800 两种尺寸)。最初的实现存在以下问题:
问题 | 描述 | 优化目标 |
---|---|---|
代码爆红 | OssUtil.java 中 ossClient.getObject(bucketName, imageKey, tempFile) 方法不存在,导致编译错误。 |
修复代码,使用正确的 OSS SDK 方法。 |
临时文件后缀限制 ❓ | File.createTempFile 使用固定后缀 .jpg ,可能误导开发者以为只支持 JPG 格式。 |
动态设置临时文件后缀,支持多种图片格式。 |
异步线程池未生效 | @Async("thumbnailThreadPool") 未生效,任务在 [main] 线程中运行。 |
修复异步逻辑,确保任务在 thumbnailThreadPool 中运行。 |
NoSuchKey 日志干扰 | doesObjectExist 检查时,NoSuchKey 日志以 INFO 级别输出,干扰核心日志。 |
降低 com.aliyun.oss 日志级别为 WARN ,减少干扰。 |
文件名冲突与中文问题 | 缩略图文件名直接使用原始文件名(含中文),可能导致冲突或编码问题。 | 使用 UUID 生成唯一文件名,避免冲突和中文编码问题。 |
串行处理性能瓶颈 | 任务串行执行,175 张图片耗时约 175 秒(每张 1 秒)。 | 改为并行处理,利用线程池提升性能,目标耗时降至 4-5 秒。 |
线程池配置不合理 ⚙️ | 核心线程数 15,最大线程数 30,无法支持每批 50 个任务全并行;拒绝策略不安全。 | 调整线程池参数(核心线程数 50,最大 100),改进拒绝策略,确保任务不丢失。 |
问题:ossClient.getObject(bucketName, imageKey, tempFile)
方法不存在,导致编译错误。
解决方案:
GetObjectRequest
重写下载逻辑:ossClient.getObject(new GetObjectRequest(bucketName, imageKey), tempFile);
com.aliyun.oss.model.GetObjectRequest
。结果:代码编译通过,图片成功下载到临时文件。
问题:File.createTempFile(UUID.randomUUID().toString(), ".jpg")
固定后缀为 .jpg
,可能误导开发者。
解决方案:
String extension = imageKey.substring(imageKey.lastIndexOf(".")).toLowerCase();
tempFile = File.createTempFile(UUID.randomUUID().toString(), extension);
if (!Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".bmp").contains(extension)) {
return BaseResult.failure("不支持的图片格式:" + extension, null);
}
结果:支持 JPG、PNG、GIF 等格式,临时文件后缀与原始文件一致。
问题:@Async("thumbnailThreadPool")
未生效,任务在 [main]
线程运行。
解决方案:
generateThumbnailsForFakeStrategyAsync
移到 ThumbnailService
类:@Service
public class ThumbnailService {
@Async("thumbnailThreadPool")
public void generateThumbnailsForFakeStrategyAsync() { ... }
}
ApiApplication
中通过注入的 ThumbnailService
调用:@Autowired
private ThumbnailService thumbnailService;
thumbnailService.generateThumbnailsForFakeStrategyAsync();
结果:任务运行在 thumbnail-pool-%d
线程,异步生效。
问题:NoSuchKey
日志以 INFO
级别输出,干扰核心日志。
解决方案:
application.yml
:logging:
level:
com.aliyun.oss: WARN
结果:NoSuchKey
日志不再以 INFO
输出,日志更清晰。
问题:缩略图文件名直接使用原始文件名(含中文),可能导致冲突或编码问题。
解决方案:
String baseFilename = UUID.randomUUID().toString() + extension;
String thumbFilename = "thumb_" + baseFilename;
String mediumFilename = "medium_" + baseFilename;
结果:文件名唯一(例如 thumb_550e8400-e29b-41d4-a716-446655440000.jpg
),避免冲突和编码问题。
问题:任务串行执行,175 张图片耗时 175 秒。
解决方案:
List<Runnable> tasks = new ArrayList<>();
for (OSSObjectSummary objectSummary : objectListing.getObjectSummaries()) {
String imageKey = objectSummary.getKey();
tasks.add(() -> {
BaseResult result = ossUtil.generateThumbnailsForExistingImage(imageKey, 1);
// 处理结果
});
}
for (Runnable task : tasks) {
thumbnailThreadPool.execute(task);
}
while (thumbnailThreadPool.getActiveCount() > 0 || !thumbnailThreadPool.getQueue().isEmpty()) {
Thread.sleep(100);
}
结果:175 张图片分 4 批处理(每批 50 张),总耗时约 4 秒。
问题:线程池核心线程数 15,最大线程数 30,无法支持每批 50 个任务全并行;拒绝策略不安全。
解决方案:
public static final int CORE_SIZE = 50; // 支持每批 50 个任务
public static final int MAX_SIZE = 100;
public static final int QUEUE_CAPACITY = 1000;
(r, executor) -> {
executor.getQueue().put(r); // 阻塞式放入队列
}
结果:线程池支持每批 50 个任务全并行,任务不丢失。
以下是缩略图生成优化的流程图,使用 Mermaid 绘制:
以下是并行处理的 Sequence Diagram,展示线程池如何处理任务:
以下是优化过程的思维导图,以 Markdown 格式呈现:
通过这次优化,我们成功将缩略图生成任务从串行转为并行,性能提升了 40 倍(从 175 秒降至 4 秒)! 同时,解决了代码错误、日志干扰和文件名冲突等问题,代码更健壮、更高效。以下是我的几点收获:
@Async
需要通过 Spring 代理调用,不能在同一类中直接调用。希望这篇博客对你有所帮助!如果有任何问题,欢迎留言讨论!