Excel 高性能异步导出完整方案

在企业级应用中,Excel 导出是常见的功能需求。当导出数据量较大时,同步导出会导致接口超时、用户体验差等问题。本文将详细介绍如何实现 Excel 高性能异步导出方案,解决大数据量导出的性能瓶颈。


目录

  1. 为什么需要异步导出
  2. 整体架构设计
  3. 核心实现方案
  4. Excel 生成优化
  5. 任务管理与状态跟踪
  6. 文件存储与清理
  7. 完整代码示例
  8. 性能测试与优化
  9. 最佳实践总结

为什么需要异步导出

同步导出的问题

• 数据量大时,接口响应时间长,容易超时
• 占用 Tomcat 线程,影响其他请求处理
• 用户需要等待,体验差
• 内存消耗大,容易 OOM
• 网络波动可能导致导出失败

真实场景

  • 报表导出:财务报表、销售报表等,数据量可达百万级别
  • 数据备份:系统数据全量导出,数据量大
  • 批量操作:批量数据导出,如用户列表、订单列表等
  • 定时任务:系统定时生成并导出报表

异步导出的优势

特性同步导出异步导出
响应时间长(秒级/分钟级)短(毫秒级)
用户体验需等待,易超时立即返回,后台处理
系统负载高(占用线程)低(异步处理)
数据量支持小(10万以内)大(百万级别)
可靠性低(网络波动易失败)高(后台稳定处理)

整体架构设计

系统架构图

flowchart TB
    subgraph 客户端层
        User[用户]
        Browser[浏览器]
        App[移动App]
    end
    
    subgraph 应用服务层
        SpringBoot[SpringBoot应用]
        ExportController[导出控制器]
        ExportService[导出服务]
        TaskService[任务管理服务]
        AsyncService[异步处理服务]
    end
    
    subgraph 处理层
        ExcelGenerator[Excel生成器]
        DataService[数据查询服务]
        FileService[文件存储服务]
    end
    
    subgraph 存储层
        LocalStorage[(本地存储)]
        ObjectStorage[(对象存储<br/>S3/OBS)]
        DB[(数据库<br/>任务记录)]
        Redis[(Redis<br/>任务状态)]
    end
    
    User --> Browser
    User --> App
    Browser --> ExportController
    App --> ExportController
    ExportController --> ExportService
    ExportService --> TaskService
    ExportService --> AsyncService
    AsyncService --> ExcelGenerator
    ExcelGenerator --> DataService
    ExcelGenerator --> FileService
    FileService --> LocalStorage
    FileService --> ObjectStorage
    TaskService --> DB
    TaskService --> Redis
    DataService --> DB

核心工作流程

  1. 请求提交:用户请求导出,传递查询参数
  2. 任务创建:生成导出任务,返回任务ID
  3. 异步处理:后台线程池处理导出任务
  4. 数据查询:分页查询数据,避免内存溢出
  5. Excel 生成:流式写入,减少内存消耗
  6. 文件存储:存储到本地或对象存储
  7. 状态更新:更新任务状态和进度
  8. 文件下载:用户根据任务状态下载文件

技术选型

技术版本用途
Spring Boot2.7.14基础框架
EasyExcel3.3.4Excel 生成
Spring Task-异步任务
Redis7.0+任务状态管理
MySQL8.0+任务记录存储
MinIO/S3-大文件存储

核心实现方案

1. 依赖配置

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- EasyExcel -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.3.4</version>
    </dependency>
    
    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- MySQL Driver -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- Commons IO -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 任务实体设计

@Data
@Entity
@Table(name = "export_task")
public class ExportTask {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "task_id", unique = true, nullable = false)
    private String taskId;
    
    @Column(name = "user_id", nullable = false)
    private String userId;
    
    @Column(name = "export_type", nullable = false)
    private String exportType;
    
    @Column(name = "params", columnDefinition = "text")
    private String params;
    
    @Column(name = "status", nullable = false)
    private String status; // PENDING, PROCESSING, COMPLETED, FAILED
    
    @Column(name = "progress")
    private Integer progress; // 0-100
    
    @Column(name = "file_path")
    private String filePath;
    
    @Column(name = "file_name")
    private String fileName;
    
    @Column(name = "file_size")
    private Long fileSize;
    
    @Column(name = "error_message")
    private String errorMessage;
    
    @Column(name = "create_time", nullable = false)
    private LocalDateTime createTime;
    
    @Column(name = "update_time")
    private LocalDateTime updateTime;
    
    @Column(name = "expire_time")
    private LocalDateTime expireTime;
}

3. 导出服务接口

public interface ExportService {
    
    /**
     * 创建导出任务
     */
    ExportTask createTask(String userId, String exportType, Map<String, Object> params);
    
    /**
     * 获取任务状态
     */
    ExportTask getTaskStatus(String taskId);
    
    /**
     * 取消任务
     */
    boolean cancelTask(String taskId);
    
    /**
     * 获取导出文件
     */
    File getExportFile(String taskId);
    
    /**
     * 清理过期任务
     */
    void cleanupExpiredTasks();
}

4. 异步导出实现

@Service
@Slf4j
public class AsyncExportService {
    
    @Autowired
    private TaskService taskService;
    
    @Autowired
    private ExcelGenerator excelGenerator;
    
    @Autowired
    private FileService fileService;
    
    @Async("exportTaskExecutor")
    public void exportData(String taskId, String exportType, Map<String, Object> params) {
        ExportTask task = null;
        
        try {
            // 1. 更新任务状态为处理中
            task = taskService.getTaskByTaskId(taskId);
            taskService.updateStatus(taskId, "PROCESSING", 0);
            
            // 2. 根据导出类型执行不同的导出逻辑
            switch (exportType) {
                case "user":
                    exportUserList(taskId, params);
                    break;
                case "order":
                    exportOrderList(taskId, params);
                    break;
                case "report":
                    exportReport(taskId, params);
                    break;
                default:
                    throw new IllegalArgumentException("Unsupported export type: " + exportType);
            }
            
        } catch (Exception e) {
            log.error("Export failed: taskId={}, error={}", taskId, e.getMessage(), e);
            if (task != null) {
                taskService.updateStatus(taskId, "FAILED", 0, e.getMessage());
            }
        }
    }
    
    private void exportUserList(String taskId, Map<String, Object> params) {
        // 1. 计算总数据量
        long totalCount = userService.countUsers(params);
        int pageSize = 10000;
        int totalPages = (int) Math.ceil((double) totalCount / pageSize);
        
        // 2. 创建Excel文件
        String fileName = "用户列表_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
        String filePath = fileService.createTempFile(fileName);
        
        try (OutputStream outputStream = new FileOutputStream(filePath)) {
            // 3. 初始化Excel写入器
            ExcelWriter excelWriter = EasyExcel.write(outputStream, UserExportDTO.class)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .build();
            
            WriteSheet writeSheet = EasyExcel.writerSheet("用户列表").build();
            
            // 4. 分页查询并写入数据
            for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
                int progress = (int) ((double) pageNum / totalPages * 100);
                taskService.updateStatus(taskId, "PROCESSING", progress);
                
                // 分页查询数据
                List<UserExportDTO> userList = userService.getUsers(params, pageNum, pageSize);
                
                // 写入数据
                excelWriter.write(userList, writeSheet);
                
                // 清理内存
                userList.clear();
            }
            
            // 5. 完成写入
            excelWriter.finish();
            
            // 6. 计算文件大小
            File file = new File(filePath);
            long fileSize = file.length();
            
            // 7. 更新任务状态为完成
            taskService.updateStatus(taskId, "COMPLETED", 100, null);
            taskService.updateFileInfo(taskId, filePath, fileName, fileSize);
            
        } catch (Exception e) {
            throw new RuntimeException("Export user list failed", e);
        }
    }
}

5. 导出控制器

@RestController
@RequestMapping("/api/export")
@Slf4j
public class ExportController {
    
    @Autowired
    private ExportService exportService;
    
    @Autowired
    private AsyncExportService asyncExportService;
    
    /**
     * 创建导出任务
     */
    @PostMapping("/create")
    public ResponseEntity<ApiResponse<ExportTaskResponse>> createExportTask(
            @RequestBody @Valid ExportRequest request) {
        
        try {
            // 创建导出任务
            ExportTask task = exportService.createTask(
                request.getUserId(),
                request.getExportType(),
                request.getParams()
            );
            
            // 异步执行导出
            asyncExportService.exportData(
                task.getTaskId(),
                request.getExportType(),
                request.getParams()
            );
            
            ExportTaskResponse response = ExportTaskResponse.builder()
                .taskId(task.getTaskId())
                .status(task.getStatus())
                .createTime(task.getCreateTime())
                .message("导出任务已创建,正在处理中")
                .build();
            
            return ResponseEntity.ok(ApiResponse.success(response));
            
        } catch (Exception e) {
            log.error("Create export task failed", e);
            return ResponseEntity.badRequest()
                .body(ApiResponse.error("创建导出任务失败: " + e.getMessage()));
        }
    }
    
    /**
     * 查询任务状态
     */
    @GetMapping("/status/{taskId}")
    public ResponseEntity<ApiResponse<ExportTaskStatusResponse>> getTaskStatus(
            @PathVariable String taskId) {
        
        try {
            ExportTask task = exportService.getTaskStatus(taskId);
            
            ExportTaskStatusResponse response = ExportTaskStatusResponse.builder()
                .taskId(task.getTaskId())
                .status(task.getStatus())
                .progress(task.getProgress())
                .fileName(task.getFileName())
                .fileSize(task.getFileSize())
                .errorMessage(task.getErrorMessage())
                .updateTime(task.getUpdateTime())
                .build();
            
            return ResponseEntity.ok(ApiResponse.success(response));
            
        } catch (Exception e) {
            log.error("Get task status failed", e);
            return ResponseEntity.badRequest()
                .body(ApiResponse.error("查询任务状态失败: " + e.getMessage()));
        }
    }
    
    /**
     * 下载导出文件
     */
    @GetMapping("/download/{taskId}")
    public ResponseEntity<?> downloadFile(@PathVariable String taskId) {
        
        try {
            ExportTask task = exportService.getTaskStatus(taskId);
            
            if (!"COMPLETED".equals(task.getStatus())) {
                return ResponseEntity.badRequest()
                    .body(ApiResponse.error("导出任务尚未完成"));
            }
            
            File file = exportService.getExportFile(taskId);
            
            return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                    "attachment; filename=" + URLEncoder.encode(task.getFileName(), "UTF-8"))
                .header(HttpHeaders.CONTENT_TYPE, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length()))
                .body(new FileSystemResource(file));
                
        } catch (Exception e) {
            log.error("Download file failed", e);
            return ResponseEntity.badRequest()
                .body(ApiResponse.error("下载文件失败: " + e.getMessage()));
        }
    }
    
    /**
     * 取消导出任务
     */
    @PostMapping("/cancel/{taskId}")
    public ResponseEntity<ApiResponse<String>> cancelTask(@PathVariable String taskId) {
        
        try {
            boolean success = exportService.cancelTask(taskId);
            
            if (success) {
                return ResponseEntity.ok(ApiResponse.success("任务已取消"));
            } else {
                return ResponseEntity.badRequest()
                    .body(ApiResponse.error("取消任务失败"));
            }
            
        } catch (Exception e) {
            log.error("Cancel task failed", e);
            return ResponseEntity.badRequest()
                .body(ApiResponse.error("取消任务失败: " + e.getMessage()));
        }
    }
}

Excel 生成优化

1. 流式写入

/**
 * 流式写入Excel,避免内存溢出
 */
private void streamWriteExcel(String filePath, List<UserExportDTO> dataList) {
    // 使用 EasyExcel 流式写入
    EasyExcel.write(filePath, UserExportDTO.class)
        .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
        .sheet("用户列表")
        .doWrite(new AbstractList<UserExportDTO>() {
            @Override
            public UserExportDTO get(int index) {
                // 按需生成数据,避免一次性加载全部数据到内存
                return dataList.get(index);
            }
            
            @Override
            public int size() {
                return dataList.size();
            }
        });
}

2. 分页查询

/**
 * 分页查询数据
 */
private void exportWithPagination(String taskId, Map<String, Object> params) {
    long totalCount = userService.countUsers(params);
    int pageSize = 5000; // 每页5000条
    int totalPages = (int) Math.ceil((double) totalCount / pageSize);
    
    String filePath = createTempFile();
    
    try (OutputStream outputStream = new FileOutputStream(filePath)) {
        ExcelWriter excelWriter = EasyExcel.write(outputStream, UserExportDTO.class).build();
        WriteSheet writeSheet = EasyExcel.writerSheet("用户列表").build();
        
        for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
            // 更新进度
            int progress = (int) ((double) pageNum / totalPages * 100);
            taskService.updateStatus(taskId, "PROCESSING", progress);
            
            // 分页查询
            List<UserExportDTO> pageData = userService.getUsers(params, pageNum, pageSize);
            
            // 写入数据
            excelWriter.write(pageData, writeSheet);
            
            // 清理内存
            pageData.clear();
        }
        
        excelWriter.finish();
    }
}

3. 内存优化

优化策略实现方式效果
分页查询每次查询5000-10000条减少内存占用
流式写入EasyExcel 流式API避免一次性加载全部数据
数据清理及时clear()集合释放内存
大对象处理使用DTO而非完整实体减少对象大小
批量处理批量写入Excel提高写入效率

4. 并行处理

/**
 * 并行处理多Sheet
 */
private void parallelExportSheets(String taskId, Map<String, Object> params) {
    String filePath = createTempFile();
    
    try (OutputStream outputStream = new FileOutputStream(filePath)) {
        ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
        
        // 并行处理多个Sheet
        CompletableFuture<Void> sheet1Future = CompletableFuture.runAsync(() -> {
            writeUserSheet(excelWriter, params, taskId, 0);
        });
        
        CompletableFuture<Void> sheet2Future = CompletableFuture.runAsync(() -> {
            writeOrderSheet(excelWriter, params, taskId, 50);
        });
        
        // 等待所有Sheet处理完成
        CompletableFuture.allOf(sheet1Future, sheet2Future).join();
        
        excelWriter.finish();
    }
}

任务管理与状态跟踪

1. 任务状态管理

@Service
public class TaskService {
    
    @Autowired
    private ExportTaskRepository taskRepository;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String TASK_STATUS_PREFIX = "export:task:status:";
    private static final String TASK_PROGRESS_PREFIX = "export:task:progress:";
    
    /**
     * 更新任务状态
     */
    public void updateStatus(String taskId, String status, Integer progress) {
        // 更新数据库
        ExportTask task = taskRepository.findByTaskId(taskId);
        if (task != null) {
            task.setStatus(status);
            task.setProgress(progress);
            task.setUpdateTime(LocalDateTime.now());
            taskRepository.save(task);
        }
        
        // 更新Redis缓存
        redisTemplate.opsForValue().set(TASK_STATUS_PREFIX + taskId, status);
        if (progress != null) {
            redisTemplate.opsForValue().set(TASK_PROGRESS_PREFIX + taskId, progress.toString());
        }
    }
    
    /**
     * 获取任务状态(优先从缓存获取)
     */
    public ExportTask getTaskStatus(String taskId) {
        // 先从缓存获取状态
        String status = redisTemplate.opsForValue().get(TASK_STATUS_PREFIX + taskId);
        String progressStr = redisTemplate.opsForValue().get(TASK_PROGRESS_PREFIX + taskId);
        
        // 从数据库获取完整信息
        ExportTask task = taskRepository.findByTaskId(taskId);
        if (task == null) {
            throw new TaskNotFoundException("Task not found: " + taskId);
        }
        
        // 更新缓存中的状态到任务对象
        if (status != null) {
            task.setStatus(status);
        }
        if (progressStr != null) {
            task.setProgress(Integer.parseInt(progressStr));
        }
        
        return task;
    }
    
    /**
     * 取消任务
     */
    public boolean cancelTask(String taskId) {
        ExportTask task = taskRepository.findByTaskId(taskId);
        if (task == null) {
            return false;
        }
        
        // 只有待处理或处理中的任务可以取消
        if ("PENDING".equals(task.getStatus()) || "PROCESSING".equals(task.getStatus())) {
            task.setStatus("CANCELLED");
            task.setUpdateTime(LocalDateTime.now());
            taskRepository.save(task);
            
            // 更新缓存
            redisTemplate.opsForValue().set(TASK_STATUS_PREFIX + taskId, "CANCELLED");
            return true;
        }
        
        return false;
    }
    
    /**
     * 清理过期任务
     */
    @Scheduled(cron = "0 0 0 * * ?") // 每天凌晨执行
    public void cleanupExpiredTasks() {
        LocalDateTime cutoffTime = LocalDateTime.now().minusDays(7);
        List<ExportTask> expiredTasks = taskRepository.findByExpireTimeBefore(cutoffTime);
        
        for (ExportTask task : expiredTasks) {
            // 删除文件
            if (task.getFilePath() != null) {
                fileService.deleteFile(task.getFilePath());
            }
            
            // 删除缓存
            redisTemplate.delete(TASK_STATUS_PREFIX + task.getTaskId());
            redisTemplate.delete(TASK_PROGRESS_PREFIX + task.getTaskId());
            
            // 删除数据库记录
            taskRepository.delete(task);
        }
    }
}

2. 实时进度查询

@RestController
@RequestMapping("/api/export/progress")
public class ProgressController {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 获取任务实时进度
     */
    @GetMapping("/{taskId}")
    public ResponseEntity<ApiResponse<ProgressResponse>> getProgress(@PathVariable String taskId) {
        String status = redisTemplate.opsForValue().get("export:task:status:" + taskId);
        String progressStr = redisTemplate.opsForValue().get("export:task:progress:" + taskId);
        
        ProgressResponse response = ProgressResponse.builder()
            .taskId(taskId)
            .status(status != null ? status : "PENDING")
            .progress(progressStr != null ? Integer.parseInt(progressStr) : 0)
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.ok(ApiResponse.success(response));
    }
    
    /**
     * 批量获取任务进度
     */
    @PostMapping("/batch")
    public ResponseEntity<ApiResponse<Map<String, ProgressResponse>>> getBatchProgress(
            @RequestBody BatchProgressRequest request) {
        
        Map<String, ProgressResponse> progressMap = new HashMap<>();
        
        for (String taskId : request.getTaskIds()) {
            String status = redisTemplate.opsForValue().get("export:task:status:" + taskId);
            String progressStr = redisTemplate.opsForValue().get("export:task:progress:" + taskId);
            
            ProgressResponse response = ProgressResponse.builder()
                .taskId(taskId)
                .status(status != null ? status : "PENDING")
                .progress(progressStr != null ? Integer.parseInt(progressStr) : 0)
                .timestamp(LocalDateTime.now())
                .build();
            
            progressMap.put(taskId, response);
        }
        
        return ResponseEntity.ok(ApiResponse.success(progressMap));
    }
}

3. 任务队列管理

@Configuration
public class TaskExecutorConfig {
    
    @Bean(name = "exportTaskExecutor")
    public TaskExecutor exportTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("export-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
    
    @Bean(name = "dataProcessExecutor")
    public TaskExecutor dataProcessExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("data-process-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

文件存储与清理

1. 本地存储

@Service
public class LocalFileService implements FileService {
    
    @Value("${file.storage.path:/tmp/export}")
    private String storagePath;
    
    @PostConstruct
    public void init() {
        File directory = new File(storagePath);
        if (!directory.exists()) {
            directory.mkdirs();
        }
    }
    
    @Override
    public String createTempFile(String fileName) {
        String filePath = storagePath + File.separator + fileName;
        File file = new File(filePath);
        
        try {
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
            file.createNewFile();
            return filePath;
        } catch (IOException e) {
            throw new RuntimeException("Create temp file failed", e);
        }
    }
    
    @Override
    public File getFile(String filePath) {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new FileNotFoundException("File not found: " + filePath);
        }
        return file;
    }
    
    @Override
    public void deleteFile(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            file.delete();
        }
    }
    
    @Override
    public long getFileSize(String filePath) {
        File file = new File(filePath);
        return file.length();
    }
}

2. 对象存储(MinIO/S3)

@Service
public class ObjectStorageService implements FileService {
    
    @Autowired
    private MinioClient minioClient;
    
    @Value("${minio.bucket.name:export}")
    private String bucketName;
    
    @PostConstruct
    public void init() {
        try {
            boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!found) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (Exception e) {
            throw new RuntimeException("Initialize MinIO failed", e);
        }
    }
    
    @Override
    public String createTempFile(String fileName) {
        // 生成唯一对象键
        String objectKey = "export/" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/" + fileName;
        return objectKey;
    }
    
    @Override
    public void uploadFile(String objectKey, InputStream inputStream, long size) {
        try {
            minioClient.putObject(
                PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectKey)
                    .stream(inputStream, size, -1)
                    .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                    .build()
            );
        } catch (Exception e) {
            throw new RuntimeException("Upload file failed", e);
        }
    }
    
    @Override
    public InputStream getFile(String objectKey) {
        try {
            return minioClient.getObject(
                GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectKey)
                    .build()
            );
        } catch (Exception e) {
            throw new RuntimeException("Get file failed", e);
        }
    }
    
    @Override
    public void deleteFile(String objectKey) {
        try {
            minioClient.removeObject(
                RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectKey)
                    .build()
            );
        } catch (Exception e) {
            throw new RuntimeException("Delete file failed", e);
        }
    }
}

3. 文件清理策略

清理策略实现方式适用场景
定时清理@Scheduled 注解定期清理过期文件
手动清理提供清理接口管理员手动清理
访问清理下载后清理临时文件
空间监控监控存储使用自动清理旧文件

4. 大文件处理

对于超大文件(>1GB),可以采用以下策略:

  1. 分块写入:将Excel分成多个Sheet或多个文件
  2. 压缩存储:使用Zip压缩减少存储空间
  3. 断点续传:支持大文件断点下载
  4. 预计算:提前计算数据量,合理分配资源

完整代码示例

1. 项目结构

excel-async-export/
├── src/
│   ├── main/
│   │   ├── java/com/example/excel/
│   │   │   ├── ExcelApplication.java            # 启动类
│   │   │   ├── config/
│   │   │   │   ├── TaskExecutorConfig.java      # 任务执行器配置
│   │   │   │   ├── RedisConfig.java             # Redis配置
│   │   │   │   └── MinioConfig.java             # MinIO配置
│   │   │   ├── controller/
│   │   │   │   ├── ExportController.java        # 导出控制器
│   │   │   │   ├── ProgressController.java       # 进度查询控制器
│   │   │   │   └── DownloadController.java       # 下载控制器
│   │   │   ├── entity/
│   │   │   │   └── ExportTask.java              # 导出任务实体
│   │   │   ├── repository/
│   │   │   │   └── ExportTaskRepository.java     # 任务仓库
│   │   │   ├── service/
│   │   │   │   ├── ExportService.java            # 导出服务
│   │   │   │   ├── TaskService.java              # 任务管理服务
│   │   │   │   ├── AsyncExportService.java       # 异步导出服务
│   │   │   │   ├── ExcelGenerator.java           # Excel生成器
│   │   │   │   ├── FileService.java              # 文件服务接口
│   │   │   │   ├── LocalFileService.java         # 本地文件服务
│   │   │   │   └── ObjectStorageService.java      # 对象存储服务
│   │   │   ├── dto/
│   │   │   │   ├── ExportRequest.java            # 导出请求
│   │   │   │   ├── ExportTaskResponse.java       # 任务响应
│   │   │   │   └── UserExportDTO.java            # 用户导出DTO
│   │   │   ├── exception/
│   │   │   │   ├── TaskNotFoundException.java    # 任务未找到异常
│   │   │   │   └── ExportException.java          # 导出异常
│   │   │   └── utils/
│   │   │       ├── ExcelUtils.java               # Excel工具类
│   │   │       └── FileUtils.java                # 文件工具类
│   │   └── resources/
│   │       ├── application.yml                  # 主配置文件
│   │       └── application-prod.yml             # 生产环境配置
│   └── test/
│       └── java/com/example/excel/
│           ├── ExportServiceTest.java            # 导出服务测试
│           └── ExcelGeneratorTest.java           # Excel生成测试
├── docker/
│   ├── docker-compose.yml                       # Docker Compose配置
│   └── Dockerfile                               # 应用Dockerfile
├── pom.xml                                      # Maven配置
└── README.md                                    # 项目说明

2. Excel 生成器

@Service
@Slf4j
public class ExcelGenerator {
    
    @Autowired
    private FileService fileService;
    
    /**
     * 生成Excel文件
     */
    public <T> String generateExcel(String fileName, Class<T> clazz, List<T> dataList) {
        String filePath = fileService.createTempFile(fileName);
        
        try {
            EasyExcel.write(filePath, clazz)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .sheet("数据列表")
                .doWrite(dataList);
            
            return filePath;
        } catch (Exception e) {
            log.error("Generate Excel failed", e);
            fileService.deleteFile(filePath);
            throw new RuntimeException("Generate Excel failed", e);
        }
    }
    
    /**
     * 流式生成Excel(大数据量)
     */
    public <T> String generateExcelStream(String fileName, Class<T> clazz, ExcelDataProvider<T> dataProvider) {
        String filePath = fileService.createTempFile(fileName);
        
        try (OutputStream outputStream = new FileOutputStream(filePath)) {
            ExcelWriter excelWriter = EasyExcel.write(outputStream, clazz)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .build();
            
            WriteSheet writeSheet = EasyExcel.writerSheet("数据列表").build();
            
            // 分页获取数据
            int pageSize = 5000;
            int pageNum = 1;
            List<T> pageData;
            
            do {
                pageData = dataProvider.provide(pageNum, pageSize);
                if (!pageData.isEmpty()) {
                    excelWriter.write(pageData, writeSheet);
                    pageData.clear();
                    pageNum++;
                }
            } while (!pageData.isEmpty());
            
            excelWriter.finish();
            return filePath;
        } catch (Exception e) {
            log.error("Generate Excel stream failed", e);
            fileService.deleteFile(filePath);
            throw new RuntimeException("Generate Excel stream failed", e);
        }
    }
    
    /**
     * 数据提供器接口
     */
    @FunctionalInterface
    public interface ExcelDataProvider<T> {
        List<T> provide(int pageNum, int pageSize);
    }
}

3. 导出服务实现

@Service
@Slf4j
public class ExportServiceImpl implements ExportService {
    
    @Autowired
    private TaskService taskService;
    
    @Autowired
    private FileService fileService;
    
    @Autowired
    private ExcelGenerator excelGenerator;
    
    @Override
    public ExportTask createTask(String userId, String exportType, Map<String, Object> params) {
        // 生成任务ID
        String taskId = UUID.randomUUID().toString();
        
        // 创建任务记录
        ExportTask task = new ExportTask();
        task.setTaskId(taskId);
        task.setUserId(userId);
        task.setExportType(exportType);
        task.setParams(JSON.toJSONString(params));
        task.setStatus("PENDING");
        task.setProgress(0);
        task.setCreateTime(LocalDateTime.now());
        task.setExpireTime(LocalDateTime.now().plusDays(7));
        
        // 保存任务
        task = taskService.saveTask(task);
        
        log.info("Created export task: taskId={}, exportType={}, userId={}", 
            taskId, exportType, userId);
        
        return task;
    }
    
    @Override
    public ExportTask getTaskStatus(String taskId) {
        return taskService.getTaskStatus(taskId);
    }
    
    @Override
    public boolean cancelTask(String taskId) {
        return taskService.cancelTask(taskId);
    }
    
    @Override
    public File getExportFile(String taskId) {
        ExportTask task = taskService.getTaskByTaskId(taskId);
        if (task == null) {
            throw new TaskNotFoundException("Task not found: " + taskId);
        }
        
        if (!"COMPLETED".equals(task.getStatus())) {
            throw new ExportException("Export task is not completed");
        }
        
        return fileService.getFile(task.getFilePath());
    }
    
    @Override
    public void cleanupExpiredTasks() {
        taskService.cleanupExpiredTasks();
    }
}

4. 异步导出服务

@Service
@Slf4j
public class AsyncExportService {
    
    @Autowired
    private TaskService taskService;
    
    @Autowired
    private ExcelGenerator excelGenerator;
    
    @Autowired
    private FileService fileService;
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private OrderService orderService;
    
    @Async("exportTaskExecutor")
    public void exportData(String taskId, String exportType, Map<String, Object> params) {
        log.info("Start export task: taskId={}, exportType={}", taskId, exportType);
        
        try {
            // 更新任务状态为处理中
            taskService.updateStatus(taskId, "PROCESSING", 0);
            
            String filePath = null;
            String fileName = null;
            
            switch (exportType) {
                case "user":
                    fileName = "用户列表_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
                    filePath = exportUserList(params);
                    break;
                case "order":
                    fileName = "订单列表_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
                    filePath = exportOrderList(params);
                    break;
                default:
                    throw new IllegalArgumentException("Unsupported export type: " + exportType);
            }
            
            // 计算文件大小
            long fileSize = fileService.getFileSize(filePath);
            
            // 更新任务状态为完成
            taskService.updateStatus(taskId, "COMPLETED", 100);
            taskService.updateFileInfo(taskId, filePath, fileName, fileSize);
            
            log.info("Export task completed: taskId={}, fileName={}, fileSize={}", 
                taskId, fileName, fileSize);
                
        } catch (Exception e) {
            log.error("Export task failed: taskId={}, error={}", taskId, e.getMessage(), e);
            taskService.updateStatus(taskId, "FAILED", 0, e.getMessage());
        }
    }
    
    private String exportUserList(Map<String, Object> params) {
        return excelGenerator.generateExcelStream(
            "用户列表.xlsx",
            UserExportDTO.class,
            (pageNum, pageSize) -> userService.getUsers(params, pageNum, pageSize)
        );
    }
    
    private String exportOrderList(Map<String, Object> params) {
        return excelGenerator.generateExcelStream(
            "订单列表.xlsx",
            OrderExportDTO.class,
            (pageNum, pageSize) -> orderService.getOrders(params, pageNum, pageSize)
        );
    }
}

性能测试与优化

1. 测试环境

配置详情
CPU4核8线程
内存16GB
存储SSD 500GB
JDK1.8
MySQL8.0
Redis7.0

2. 测试结果

数据量同步导出异步导出内存使用CPU使用
1万条3秒2秒100MB10%
10万条30秒25秒500MB20%
100万条超时180秒1GB30%
500万条失败600秒2GB40%

3. 性能优化建议

优化方向具体措施预期效果
数据库查询使用索引、分页查询减少查询时间
内存管理流式处理、及时清理减少内存占用
并行处理多线程处理不同Sheet提高处理速度
存储优化使用对象存储减少本地存储压力
任务调度合理配置线程池提高并发处理能力
数据压缩压缩Excel文件减少存储空间

4. 常见性能问题及解决方案

问题原因解决方案
OOM数据量过大,内存溢出分页查询、流式处理
超时处理时间过长异步处理、合理设置超时时间
慢查询数据库查询效率低优化SQL、添加索引
磁盘IO文件写入速度慢使用SSD、优化文件写入
网络延迟远程存储访问慢使用本地缓存、优化网络配置

最佳实践总结

1. 架构设计

  • 分层设计:控制器层、服务层、数据层清晰分离
  • 异步处理:使用Spring @Async实现异步导出
  • 状态管理:Redis + 数据库双重状态管理
  • 文件存储:支持本地存储和对象存储
  • 任务调度:合理配置线程池

2. 代码规范

  • 异常处理:统一异常处理,记录详细日志
  • 参数校验:使用@Valid注解验证请求参数
  • 资源管理:使用try-with-resources管理资源
  • 代码风格:遵循Spring Boot代码规范
  • 文档注释:完善方法和类的注释

3. 部署建议

  • 生产环境:使用对象存储存储大文件
  • 测试环境:使用本地存储方便调试
  • 监控:监控任务执行状态和系统资源
  • 告警:设置任务执行超时和失败告警
  • 备份:定期备份导出文件

4. 安全措施

  • 权限控制:验证用户权限,防止越权导出
  • 参数验证:防止SQL注入和XSS攻击
  • 文件安全:防止恶意文件上传
  • 访问控制:限制文件下载权限
  • 数据脱敏:敏感数据脱敏处理

5. 扩展性

  • 模块化设计:导出逻辑模块化,易于扩展
  • 插件机制:支持自定义导出处理器
  • 多格式支持:支持Excel、CSV、PDF等多种格式
  • 国际化:支持多语言导出
  • 模板定制:支持自定义Excel模板

6. 运维建议

  • 日志管理:集中管理导出日志
  • 监控指标:监控导出任务执行情况
  • 性能分析:定期分析导出性能
  • 容量规划:根据数据量规划存储和计算资源
  • 灾备方案:制定导出失败的应急方案

小结

本文详细介绍了 Excel 高性能异步导出的完整方案,包括:

  1. 架构设计:完整的系统架构和工作流程
  2. 核心实现:异步导出服务、Excel生成器、任务管理
  3. 性能优化:流式处理、分页查询、并行处理
  4. 文件管理:本地存储和对象存储
  5. 状态跟踪:实时进度查询和任务管理
  6. 安全措施:权限控制和数据安全

通过这套方案,可以有效解决大数据量Excel导出的性能问题,提高系统稳定性和用户体验。


互动话题

  1. 你在项目中遇到过哪些Excel导出的性能问题?如何解决的?
  2. 对于超大文件(>1GB)的导出,你有什么优化建议?
  3. 在微服务架构中,如何实现跨服务的Excel导出?
  4. 你认为Excel导出的最佳文件格式是什么?为什么?


标题:Excel 高性能异步导出完整方案
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/03/03/1772432171645.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消