背景介绍
在跨境电商系统中,物流标签的批量生成是一个常见需求。我们的系统需要支持1x1、10x4、11x4等多种标签模板,每个标签包含商品信息、条形码等内容。随着业务量增长,原有的标签生成模块性能问题逐渐显现,特别是在批量生成场景下,响应时间明显延长。
问题分析
通过性能分析,发现主要瓶颈:
- 重复计算:每次生成标签都要重新计算布局参数
- 资源浪费:相同模板的配置信息重复创建
- 内存压力:频繁创建对象导致GC负担增加
原始代码示例:
public void createLabelPdf(String destFilePath, String productName,
String barcodeValue) {
// 每次都要计算这些参数
float pageWidth = 595;
float pageHeight = 700;
float margin = 20;
float xStep = (pageWidth - 2 * margin) / cols;
float yStep = (pageHeight - 2 * margin) / rows;
// 计算每个标签的位置
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
// 重复的位置计算...
}
}
}
优化方案
1. 静态动态分离
将标签生成过程分为静态配置和动态内容两部分:
@Data
public class TemplateConfig {
private final TemplateType type;
private final float pageWidth;
private final float pageHeight;
private final float margin;
private final float xStep;
private final float yStep;
private final List<LabelPosition> positions;
// 预计算所有标签位置
public TemplateConfig(TemplateType type) {
this.type = type;
// 初始化基础参数...
this.positions = calculatePositions();
}
}
2. 引入Caffeine缓存
选择Caffeine作为缓存方案,主要考虑:
- 超高性能:接近ConcurrentHashMap的读取速度
- 智能回收:采用Window TinyLFU算法
- 可观测性:完善的统计功能
public class LabelPdfCacheUtils {
private static final Cache<String, TemplateConfig> templateCache =
Caffeine.newBuilder()
.maximumSize(10)
.expireAfterWrite(1, TimeUnit.HOURS)
.recordStats()
.build();
public static void createLabelPdf(...) {
TemplateConfig config = templateCache.get(templateType.code,
k -> new TemplateConfig(templateType));
// 使用配置生成PDF...
}
}
3. 优化PDF生成流程
private static void renderLabel(Document doc, PdfDocument pdfDoc,
TemplateConfig config, PdfFont font, LabelPosition pos,
String title, String condition, String barcodeValue) {
// 1. 生成条形码
Barcode128 barcode = new Barcode128(pdfDoc);
barcode.setCode(barcodeValue);
// 2. 创建图像
Image barcodeImage = new Image(barcode.createFormXObject(pdfDoc));
barcodeImage.setFixedPosition(pos.x, pos.y);
// 3. 添加文本内容
Paragraph titlePara = new Paragraph(title)
.setFont(font)
.setFontSize(config.fontSize);
// 4. 设置位置并添加到文档
doc.add(barcodeImage);
doc.add(titlePara);
}
性能测试
编写全面的测试用例验证优化效果:
package com.sealinkin.oms.common.utils;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public class LabelPdfGenerationTest {
private static final String[] TEST_PRODUCTS = {
"GeschenPark Gifts for 3-12 Year Old Girls, Kids Microphones for Kids Toys",
"Cool Mini Karaoke Machine Toys: Kids Toys Birthday Gifts Age 3-12+",
"助光 顔美化/肌美化ライト 仕事/勉強/美容化粧/ビデオカメラ撮影用",
"Test Product with Normal Length Name",
"Short Name"
};
private static final String[] TEST_CONDITIONS = {
"NEW", "新品", "Used", "Refurbished"
};
private static final String[] TEST_BARCODES = {
"X001LXRZRT", "X004IF0F27", "X001ZVG01J", "X00TESTBAR"
};
private static final String BASE_OUTPUT_DIR = "D:\\workspace\\label-test-output";
private String outputPath;
@BeforeEach
void setUp() {
// 创建输出目录
File outputDir = new File(BASE_OUTPUT_DIR);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
// 为每次测试创建带时间戳的子目录
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
outputPath = new File(outputDir, timestamp).getAbsolutePath();
new File(outputPath).mkdirs();
log.info("Test output directory: {}", outputPath);
// 清除之前的缓存
LabelPdfCacheUtils.clearCache();
// 预热缓存
LabelPdfCacheUtils.warmupCache();
}
@Test
void testSingleLabelGeneration() {
String fileName = "single_label_test.pdf";
String filePath = new File(outputPath, fileName).getAbsolutePath();
log.info("Testing single label generation...");
// 使用原始方法生成
long startTime = System.nanoTime();
LabelPdfUtils.createLabelPdf1(filePath,
TEST_PRODUCTS[0],
TEST_CONDITIONS[0],
TEST_BARCODES[0]);
long originalTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
// 使用缓存方法生成
startTime = System.nanoTime();
LabelPdfCacheUtils.createLabelPdf(filePath,
TEST_PRODUCTS[0],
TEST_CONDITIONS[0],
TEST_BARCODES[0],
LabelPdfCacheUtils.TemplateType.TEMPLATE_1);
long cachedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
log.info("Single label generation comparison:");
log.info("Original implementation: {}ms", originalTime);
log.info("Cached implementation: {}ms", cachedTime);
log.info("Performance improvement: {:.2f}%",
((double)(originalTime - cachedTime) / originalTime) * 100);
}
@Test
void testBatchLabelGeneration() {
int batchSize = 5; // 每种模板生成5个文件
List<Long> original40Times = new ArrayList<>();
List<Long> cached40Times = new ArrayList<>();
List<Long> original44Times = new ArrayList<>();
List<Long> cached44Times = new ArrayList<>();
// 测试1出40标签
log.info("Testing 1x40 label generation...");
for (int i = 0; i < batchSize; i++) {
String fileName = String.format("batch_40_test_%d.pdf", i);
String filePath = new File(outputPath, fileName).getAbsolutePath();
// 原始方法
long startTime = System.nanoTime();
LabelPdfUtils.createLabelPdf40(filePath,
TEST_PRODUCTS[i % TEST_PRODUCTS.length],
TEST_CONDITIONS[i % TEST_CONDITIONS.length],
TEST_BARCODES[i % TEST_BARCODES.length]);
original40Times.add(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
// 缓存方法
startTime = System.nanoTime();
LabelPdfCacheUtils.createLabelPdf(filePath,
TEST_PRODUCTS[i % TEST_PRODUCTS.length],
TEST_CONDITIONS[i % TEST_CONDITIONS.length],
TEST_BARCODES[i % TEST_BARCODES.length],
LabelPdfCacheUtils.TemplateType.TEMPLATE_40);
cached40Times.add(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}
// 测试1出44标签
log.info("Testing 1x44 label generation...");
for (int i = 0; i < batchSize; i++) {
String fileName = String.format("batch_44_test_%d.pdf", i);
String filePath = new File(outputPath, fileName).getAbsolutePath();
// 原始方法
long startTime = System.nanoTime();
LabelPdfUtils.createLabelPdf44(filePath,
TEST_PRODUCTS[i % TEST_PRODUCTS.length],
TEST_CONDITIONS[i % TEST_CONDITIONS.length],
TEST_BARCODES[i % TEST_BARCODES.length]);
original44Times.add(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
// 缓存方法
startTime = System.nanoTime();
LabelPdfCacheUtils.createLabelPdf(filePath,
TEST_PRODUCTS[i % TEST_PRODUCTS.length],
TEST_CONDITIONS[i % TEST_CONDITIONS.length],
TEST_BARCODES[i % TEST_BARCODES.length],
LabelPdfCacheUtils.TemplateType.TEMPLATE_44);
cached44Times.add(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}
// 输出性能统计
printPerformanceStats("1x40 Labels", original40Times, cached40Times);
printPerformanceStats("1x44 Labels", original44Times, cached44Times);
// 输出缓存统计
printCacheStats();
}
@Test
void testConcurrentLabelGeneration() throws InterruptedException {
int threadCount = 4;
int labelsPerThread = 5;
Thread[] threads = new Thread[threadCount];
log.info("Testing concurrent label generation with {} threads, {} labels per thread...",
threadCount, labelsPerThread);
for (int i = 0; i < threadCount; i++) {
final int threadIndex = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < labelsPerThread; j++) {
String fileName = String.format("concurrent_test_thread%d_label%d.pdf",
threadIndex, j);
String filePath = new File(outputPath, fileName).getAbsolutePath();
LabelPdfCacheUtils.createLabelPdf(filePath,
TEST_PRODUCTS[j % TEST_PRODUCTS.length],
TEST_CONDITIONS[j % TEST_CONDITIONS.length],
TEST_BARCODES[j % TEST_BARCODES.length],
LabelPdfCacheUtils.TemplateType.TEMPLATE_44);
}
});
threads[i].start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
// 输出缓存统计
printCacheStats();
}
private void printPerformanceStats(String testName, List<Long> originalTimes,
List<Long> cachedTimes) {
double avgOriginal = calculateAverage(originalTimes);
double avgCached = calculateAverage(cachedTimes);
double improvement = ((avgOriginal - avgCached) / avgOriginal) * 100;
log.info("\nPerformance Statistics for {}:", testName);
log.info("Original Implementation:");
log.info(" Average: {:.2f}ms", avgOriginal);
log.info(" Min: {}ms", originalTimes.stream().mapToLong(Long::longValue).min().orElse(0));
log.info(" Max: {}ms", originalTimes.stream().mapToLong(Long::longValue).max().orElse(0));
log.info("Cached Implementation:");
log.info(" Average: {:.2f}ms", avgCached);
log.info(" Min: {}ms", cachedTimes.stream().mapToLong(Long::longValue).min().orElse(0));
log.info(" Max: {}ms", cachedTimes.stream().mapToLong(Long::longValue).max().orElse(0));
log.info("Performance Improvement: {:.2f}%", improvement);
}
private void printCacheStats() {
CacheStats stats = LabelPdfCacheUtils.getCacheStats();
log.info("\nCache Statistics:");
log.info("Hit Count: {}", stats.hitCount());
log.info("Miss Count: {}", stats.missCount());
log.info("Hit Rate: {:.2f}%", stats.hitRate() * 100);
log.info("Average Load Penalty: {:.2f}ms", stats.averageLoadPenalty() / 1_000_000);
log.info("Eviction Count: {}", stats.evictionCount());
}
private double calculateAverage(List<Long> times) {
return times.stream()
.mapToLong(Long::longValue)
.average()
.orElse(0.0);
}
@AfterEach
void tearDown() {
log.info("\n========================================");
log.info("Test output directory: {}", outputPath);
log.info("Please check the generated files in this directory");
log.info("========================================\n");
}
}
测试结果:
- 单标签生成:平均耗时从180ms降至110ms
- 批量生成(40张):总耗时从7.2s降至4.3s
- 内存使用:峰值降低约30%
- 缓存命中率:稳定在95%以上
实现要点
1. 线程安全保证
public class LabelPdfCacheUtils {
// Caffeine保证缓存操作的线程安全
private static final Cache<String, TemplateConfig> templateCache;
// 模板配置不可变
@Value
private static class TemplateConfig {
private final List<LabelPosition> positions;
// 其他final字段...
}
}
2. 流程图
参考资料
- iText 7文档:iText Core: an open-source PDF development library for Java and .NET
- Caffeine官方文档:https://github.com/ben-manes/caffeine
- Java性能优化实践:JDK 24 Documentation - Home
本文通过实际项目优化案例,展示了如何通过合理的技术方案提升系统性能。希望这些经验能够帮助到遇到类似问题的开发者。如果您有任何问题或建议,欢迎在评论区讨论交流。