使用Java實(shí)現(xiàn)百萬Excel數(shù)據(jù)導(dǎo)出
可能出現(xiàn)的問題
- 同步導(dǎo)數(shù)據(jù),接口很容易超時(shí)。
- 如果把所有數(shù)據(jù)一次性裝載到內(nèi)存,很容易引起OOM。
- 數(shù)據(jù)量太大sql語句慢。
- 如果走異步,如何通知用戶導(dǎo)出結(jié)果
- 如果excel文件太大,目標(biāo)用戶打不開怎么辦
解決方案
問題一 異步化 調(diào)用接口立即返回任務(wù)生產(chǎn)成功
問題二 分批查詢 poi 禁止使用XSSFWorkbook 使用SXSSFWorkbook 或 easy Excel
問題三 分頁通過滾動(dòng)翻頁查詢
流式查詢問題:容易長時(shí)間占用數(shù)據(jù)庫鏈接池資源。
游標(biāo)查詢問題:應(yīng)用指定每次查詢獲取的條數(shù)fetchSize,MySQL服務(wù)器每次只查詢指定條數(shù)的數(shù)據(jù),由于MySQL方不知道客戶端什么時(shí)候?qū)?shù)據(jù)消費(fèi)完,MySQL需要建立一個(gè)臨時(shí)空間來存放每次查詢出的數(shù)據(jù),大數(shù)據(jù)量時(shí)MySQL服務(wù)器、磁盤占用都會(huì)飆升。
故使用滾動(dòng)翻頁查詢
問題四 通過 頁面或者溝通軟件通知用戶導(dǎo)出成功 ,并將導(dǎo)出結(jié)果上傳至oss 后續(xù)可直接下載 無需重復(fù)導(dǎo)出
問題五 導(dǎo)出可用戶設(shè)置最大條數(shù)
數(shù)據(jù)準(zhǔn)備
CREATE TABLE `t_order`
(
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
`creator` VARCHAR(16) NOT NULL DEFAULT 'admin' COMMENT '創(chuàng)建人',
`editor` VARCHAR(16) NOT NULL DEFAULT 'admin' COMMENT '修改人',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
`edit_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時(shí)間',
`version` BIGINT NOT NULL DEFAULT 1 COMMENT '版本號(hào)',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '軟刪除標(biāo)識(shí)',
`order_id` VARCHAR(32) NOT NULL COMMENT '訂單ID',
`amount` DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '訂單金額',
`payment_time` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '支付時(shí)間',
`order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '訂單狀態(tài),0:處理中,1:支付成功,2:支付失敗',
UNIQUE uniq_order_id (`order_id`),
INDEX idx_payment_time (`payment_time`)
) COMMENT '訂單表';
public class OrderServiceTest {
private static final Random OR = new Random();
private static final Random AR = new Random();
private static final Random DR = new Random();
@Test
public void testGenerateTestOrderSql() throws Exception {
HikariConfig config = new HikariConfig();
config.setUsername("root");
config.setPassword("root");
config.setJdbcUrl("jdbc:mysql://localhost:3306/local?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false");
config.setDriverClassName("com.mysql.jdbc.Driver");
HikariDataSource hikariDataSource = new HikariDataSource(config);
JdbcTemplate jdbcTemplate = new JdbcTemplate(hikariDataSource);
for (int d = 0; d < 100; d++) {
String item = "('%s','%d','2020-07-%d 00:00:00','%d')";
StringBuilder sql = new StringBuilder("INSERT INTO t_order(order_id,amount,payment_time,order_status) VALUES ");
for (int i = 0; i < 20_000; i++) {
sql.append(String.format(item, UUID.randomUUID().toString().replace("-", ""),
AR.nextInt(100000) + 1, DR.nextInt(31) + 1, OR.nextInt(3))).append(",");
}
jdbcTemplate.update(sql.substring(0, sql.lastIndexOf(",")));
}
hikariDataSource.close();
}
}
具體實(shí)現(xiàn)
easy Excel通過滾動(dòng)翻頁

Controller
@GetMapping(path = "/export")
public void export(@RequestParam(name = "paymentDateStart") String paymentDateStart,
@RequestParam(name = "paymentDateEnd") String paymentDateEnd,
) throws Exception {
orderService.export(paymentDateStart, paymentDateEnd);
}
Service
@Async
public void export(String paymentDateStart, String paymentDateEnd) throws IOException {
Date dateBefore = new Date();
String fileName = URLEncoder.encode(String.format("%s-(%s).xlsx", "訂單數(shù)據(jù)", UUID.randomUUID()),
StandardCharsets.UTF_8.toString());
BufferedOutputStream outputStream = FileUtil.getOutputStream(FileUtil.file("/Users/Documents/github/"+fileName));
ExcelWriter writer = new ExcelWriterBuilder()
.autoCloseStream(true)
.excelType(ExcelTypeEnum.XLSX)
.file(outputStream)
.head(OrderDTO.class)
.build();
WriteSheet writeSheet = new WriteSheet();
writeSheet.setSheetName("target");
long lastBatchMaxId = 0L;
int limit = 3000;
for (; ; ) {
List<OrderDTO> list = queryByScrollingPagination(paymentDateTimeStart, paymentDateTimeEnd, lastBatchMaxId, limit);
//可以添加導(dǎo)出條數(shù)限制
if (list.isEmpty()) {
writer.finish();
Date dateAfter = new Date();
System.out.println("導(dǎo)出列表共執(zhí)行" + (dateAfter.getTime() - dateBefore.getTime()) + "ms");
//todo 上傳oss 發(fā)通知
break;
} else {
lastBatchMaxId = list.stream().map(OrderDTO::getId).max(Long::compareTo).orElse(Long.MAX_VALUE);
writer.write(list, writeSheet);
}
}
}
public List<OrderDTO> queryByScrollingPagination(String paymentDateTimeStart,
String paymentDateTimeEnd,
long lastBatchMaxId,
int limit) {
LocalDateTime start = LocalDateTime.parse(paymentDateTimeStart, formatter);
LocalDateTime end = LocalDateTime.parse(paymentDateTimeEnd, formatter);
return orderDao.queryByScrollingPagination(lastBatchMaxId, limit, start, end).stream().map(order -> {
OrderDTO dto = new OrderDTO();
dto.setId(order.getId());
dto.setAmount(order.getAmount());
dto.setOrderId(order.getOrderId());
dto.setCreator(order.getCreator());
return dto;
}).collect(Collectors.toList());
}
Repository
@Repository
public class OrderDao {
@Resource
private JdbcTemplate jdbcTemplate;
public List<Order> queryByScrollingPagination(long lastBatchMaxId,
int limit,
LocalDateTime paymentDateTimeStart,
LocalDateTime paymentDateTimeEnd) {
return jdbcTemplate.query("SELECT id,creator,editor ,version,deleted,order_id,amount,order_status FROM t_order WHERE id > ? AND payment_time >= ? AND payment_time <= ? " +
"ORDER BY id ASC LIMIT ?",
p -> {
p.setLong(1, lastBatchMaxId);
p.setTimestamp(2, Timestamp.valueOf(paymentDateTimeStart));
p.setTimestamp(3, Timestamp.valueOf(paymentDateTimeEnd));
p.setInt(4, limit);
},
rs -> {
List<Order> orders = new ArrayList<>();
while (rs.next()) {
Order order = new Order();
order.setId(rs.getLong("id"));
order.setCreator(rs.getString("creator"));
order.setEditor(rs.getString("editor"));
order.setVersion(rs.getLong("version"));
order.setDeleted(rs.getInt("deleted"));
order.setOrderId(rs.getString("order_id"));
order.setAmount(rs.getBigDecimal("amount"));
order.setOrderStatus(rs.getInt("order_status"));
orders.add(order);
}
return orders;
});
}
}
總結(jié)
業(yè)務(wù)方面
做需求時(shí)刻先考慮是不是必須要做 、如果必須要做的情況需要考慮用戶的體驗(yàn)和使用感受
技術(shù)方面
1 不需要立馬返回結(jié)果的接口可以采用異步的方式讓接口立刻返回結(jié)果,可以防止接口耗時(shí)過長導(dǎo)致tomcat線程池打滿。
2 MySQL批量查詢、數(shù)據(jù)同步、數(shù)據(jù)導(dǎo)出可以使用類似于分頁查詢的思路,但是鑒于LIMIT offset,size的效率太低,可以采用”滾動(dòng)翻頁”的實(shí)現(xiàn)方式 注意要用自增趨勢的主鍵
3 數(shù)據(jù)導(dǎo)出需要注意由于大對(duì)象頻繁創(chuàng)建導(dǎo)致的 full gc 和oom 如果導(dǎo)出較頻繁可以考慮拆分單獨(dú)服務(wù)專門做導(dǎo)出
到此這篇關(guān)于使用Java實(shí)現(xiàn)百萬Excel數(shù)據(jù)導(dǎo)出的文章就介紹到這了,更多相關(guān)Java數(shù)據(jù)導(dǎo)出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java開發(fā)中常用的 Websocket 技術(shù)參考
WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù),當(dāng)然也支持客戶端發(fā)送數(shù)據(jù)到服務(wù)端。2020-09-09
淺談Android開發(fā)中項(xiàng)目的文件結(jié)構(gòu)及規(guī)范化部署建議
這篇文章主要介紹了Android開發(fā)中項(xiàng)目的文件結(jié)構(gòu)及規(guī)范化部署建議,組織好代碼文件的結(jié)構(gòu)有利于維護(hù)團(tuán)隊(duì)合作的效率,需要的朋友可以參考下2016-03-03
從log4j切換到logback后項(xiàng)目無法啟動(dòng)的問題及解決方法
這篇文章主要介紹了從log4j切換到logback后項(xiàng)目無法啟動(dòng)的問題及解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01
異常解決SpringBoot項(xiàng)目啟動(dòng)卡住,無任何異常信息問題
這篇文章主要介紹了異常解決SpringBoot項(xiàng)目啟動(dòng)卡住,無任何異常信息問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
Java如何對(duì)返回參數(shù)進(jìn)行處理
這篇文章主要介紹了Java如何對(duì)返回參數(shù)進(jìn)行處理問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
SpringCloud+Tornado基于jwt實(shí)現(xiàn)請(qǐng)求安全校驗(yàn)功能
這篇文章主要介紹了SpringCloud+Tornado基于jwt實(shí)現(xiàn)請(qǐng)求安全校驗(yàn),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12
Java Web項(xiàng)目部署在Tomcat運(yùn)行出錯(cuò)與解決方法示例
這篇文章主要介紹了Java Web項(xiàng)目部署在Tomcat運(yùn)行出錯(cuò)與解決方法,結(jié)合具體實(shí)例形式分析了Java Web項(xiàng)目部署在Tomcat過程中由于xml配置文件導(dǎo)致的錯(cuò)誤問題常見提示與解決方法,需要的朋友可以參考下2017-03-03

