大数据量报表导出设计方案
·
目录
背景
当前 SWMS 和 WMS 系统在导出数十万级数据时存在性能瓶颈,导致导出失败频发,建议优化导出机制。
解决方案
1、初期简易版
核心功能开发预计耗时约3天,后续新增报表可通过配置快速实现,仅需少量开发支持。

2、多数据源具体实现步骤
使用 Spring Boot + 多数据源配置
步骤 1:添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
步骤 2:配置文件 application.yml
spring:
datasource:
mysql:
url: jdbc:mysql://localhost:3306/db_mysql?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
postgres:
url: jdbc:postgresql://localhost:5432/db_postgres
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
sqlserver:
url: jdbc:sqlserver://localhost:1433;databaseName=db_sqlserver;encrypt=false;trustServerCertificate=true;
username: sa
password: YourStrong@Passw0rd
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
# 可选:启用 Hikari 连接池(Spring Boot 默认)
spring.datasource.hikari.maximum-pool-size: 10
步骤 3:配置多个 DataSource Bean
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.mysql")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.postgres")
public DataSource postgresDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.sqlserver")
public DataSource sqlServerDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
步骤 4:创建 JdbcTemplate Bean
@Configuration
public class JdbcTemplateConfig {
@Autowired
@Qualifier("mysqlDataSource")
private DataSource mysqlDataSource;
@Autowired
@Qualifier("postgresDataSource")
private DataSource postgresDataSource;
@Autowired
@Qualifier("sqlServerDataSource")
private DataSource sqlServerDataSource;
@Bean
@Primary
public JdbcTemplate mysqlJdbcTemplate() {
return new JdbcTemplate(mysqlDataSource);
}
@Bean
public JdbcTemplate postgresJdbcTemplate() {
return new JdbcTemplate(postgresDataSource);
}
@Bean
public JdbcTemplate sqlServerJdbcTemplate() {
return new JdbcTemplate(sqlServerDataSource);
}
}
步骤 5:使用
@Service
public class MultiDatabaseService {
private final JdbcTemplate mysqlTemplate;
private final JdbcTemplate postgresTemplate;
private final JdbcTemplate sqlServerTemplate;
public MultiDatabaseService(
@Qualifier("mysqlJdbcTemplate") JdbcTemplate mysqlTemplate,
@Qualifier("postgresJdbcTemplate") JdbcTemplate postgresTemplate,
@Qualifier("sqlServerJdbcTemplate") JdbcTemplate sqlServerTemplate) {
this.mysqlTemplate = mysqlTemplate;
this.postgresTemplate = postgresTemplate;
this.sqlServerTemplate = sqlServerTemplate;
}
// MySQL
public List<User> getUsersFromMysql() {
return mysqlTemplate.query(
"SELECT id, name, email FROM users",
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"), rs.getString("email"))
);
}
// PostgreSQL
public void saveOrderToPostgres(Order order) {
postgresTemplate.update(
"INSERT INTO orders (order_id, amount, created_at) VALUES (?, ?, ?)",
order.getOrderId(), order.getAmount(), order.getCreatedAt()
);
}
// SQL Server
public List<Customer> getCustomersFromSqlServer() {
return sqlServerTemplate.query(
"SELECT CustomerID, CompanyName, ContactName FROM Customers",
(rs, rowNum) -> new Customer(
rs.getInt("CustomerID"),
rs.getString("CompanyName"),
rs.getString("ContactName")
)
);
}
}
3、动态列+报表文件追加写入例子
| 特性 | CSV 追加写入 | EasyExcel |
| 追加写入 | ✅ 真正支持 | ⚠️ 逻辑追加(需复用Writer) |
| 动态列 | ✅ 支持 | ✅ 支持 |
| 内存占用 | 极低 | 低 |
| 格式丰富(样式、合并) | ❌ 不支持 | ✅ 支持 |
| Excel 兼容性 | 高(但无样式) | 高 |
| 中文乱码风险 | 有(需 BOM) | 无 |
CSV例子:
package io.renren.common.service.impl;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;
public class DynamicCsvExport {
// 临时 CSV 文件路径
private static final String OUTPUT_FILE = "E://dynamic_report.csv";
public static void main(String[] args) throws Exception {
// 删除旧文件
Files.deleteIfExists(Paths.get(OUTPUT_FILE));
// 定义动态列名(可从配置、数据库获取)
List<String> headers = Arrays.asList("用户ID", "姓名", "金额", "状态", "注册时间");
// 第一次写入:包含表头
writeCsvWithHeaders(OUTPUT_FILE, headers);
// 模拟分批写入数据(如从数据库分页查询)
for (int i = 0; i < 3; i++) {
List<List<Object>> pageData = generateDataPage(i * 1000 + 1, (i + 1) * 1000);
appendCsvData(OUTPUT_FILE, pageData);
System.out.println("✅ 已追加写入第 " + (i + 1) + " 批数据");
}
System.out.println("🎉 CSV 报表已生成:" + OUTPUT_FILE);
}
/**
* 第一次写入:包含表头(UTF-8 with BOM,确保 Excel 不乱码)
*/
public static void writeCsvWithHeaders(String filePath, List<String> headers) throws IOException {
Path path = Paths.get(filePath);
// 写入 BOM(\uFEFF)以确保 Excel 正确识别 UTF-8
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
writer.write("\uFEFF"); // UTF-8 BOM
writer.write(String.join(",", escapeCsvFields(headers)));
writer.newLine();
}
}
/**
* 追加写入数据(不包含表头)
*/
public static void appendCsvData(String filePath, List<List<Object>> data) throws IOException {
Path path = Paths.get(filePath);
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
for (List<Object> row : data) {
List<String> escapedRow = escapeCsvFields(convertToStringList(row));
writer.write(String.join(",", escapedRow));
writer.newLine();
}
}
}
/**
* 转换 Object 列表为 String 列表
*/
private static List<String> convertToStringList(List<Object> row) {
List<String> result = new ArrayList<>();
for (Object cell : row) {
if (cell == null) {
result.add("");
} else if (cell instanceof Date) {
result.add(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(cell));
} else {
result.add(cell.toString());
}
}
return result;
}
/**
* 转义 CSV 特殊字符:包含逗号、引号、换行符的字段需用双引号包围
*/
private static List<String> escapeCsvFields(List<String> fields) {
List<String> escaped = new ArrayList<>();
for (String field : fields) {
if (field == null) {
escaped.add("");
} else if (field.contains(",") || field.contains("\"") || field.contains("\n") || field.contains("\r")) {
// 转义双引号:"",并用双引号包围
escaped.add("\"" + field.replace("\"", "\"\"") + "\"");
} else {
escaped.add(field);
}
}
return escaped;
}
/**
* 模拟生成一页数据
*/
private static List<List<Object>> generateDataPage(int startId, int endId) {
List<List<Object>> data = new ArrayList<>();
Random random = new Random();
for (int i = startId; i <= endId; i++) {
List<Object> row = Arrays.asList(
i,
"用户_" + i,
String.format("%.2f", random.nextDouble() * 1000),
random.nextBoolean() ? "启用" : "禁用",
new Date()
);
data.add(row);
}
return data;
}
}
EasyExcel 例子:
package io.renren.common.service.impl;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import java.io.File;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class EasyExcelTest {
public static void main(String[] args) {
String filePath = "E://dynamic_report.xlsx";
File file = new File(filePath);
try (ExcelWriter excelWriter = EasyExcel.write(file).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("动态数据").build();
// 表头
List<List<String>> headers = Arrays.asList(
// Arrays.asList("ID", "姓名", "状态", "金额", "时间")
Arrays.asList("ID"),
Arrays.asList("姓名"),
Arrays.asList("状态"),
Arrays.asList("金额"),
Arrays.asList("时间")
);
// 第一批数据
List<List<Object>> data1 = Arrays.asList(
Arrays.asList(1L, "张三", "启用", "999.99", new Date()),
Arrays.asList(2L, "李四", "禁用", "888.88", new Date())
);
excelWriter.write(data1, writeSheet, EasyExcel.writerTable(0).head(headers).build());
// 追加第二批数据(不带表头)
List<List<Object>> data2 = Arrays.asList(
Arrays.asList(3L, "王五", "启用", "777.77", new Date())
);
excelWriter.write(data2, writeSheet, EasyExcel.writerTable(1).needHead(false).build());
}
System.out.println("✅ 动态报表已生成:" + filePath);
}
}
注意
1. 部署架构说明
初期部署单个服务用于报表文件生成。由于下载请求与临时文件生成服务分离,需确保生成服务器的文件可被其他服务访问,否则将导致下载失败。
2. 下载任务消息推送
- 通过消息队列推送下载请求,消息内容包含:唯一任务标识(Task ID)
- 来源系统(用于匹配数据库连接配置)
- 报表查询 SQL 及参数(初期 SQL 可预置,参数使用占位符,运行时动态替换)
3. 临时文件生成机制
在服务器本地创建临时文件,采用流式追加写入方式生成数据文件,避免一次性加载大量数据至内存,降低内存占用,提升处理大报表的稳定性。
4. 文件访问支持
通过 Nginx 配置静态资源代理,开放临时文件访问路径,使其他服务可通过 HTTP 下载生成的报表文件。
5. 下载状态管理
对下载任务状态建立缓存(如 Redis),记录任务进度与文件路径。前端通过轮询方式查询任务状态,获取下载链接或处理结果。
更多推荐

所有评论(0)