使用Java實(shí)現(xiàn)百萬(wàn)Excel數(shù)據(jù)導(dǎo)出
可能出現(xiàn)的問題
- 同步導(dǎo)數(shù)據(jù),接口很容易超時(shí)。
- 如果把所有數(shù)據(jù)一次性裝載到內(nèi)存,很容易引起OOM。
- 數(shù)據(jù)量太大sql語(yǔ)句慢。
- 如果走異步,如何通知用戶導(dǎo)出結(jié)果
- 如果excel文件太大,目標(biāo)用戶打不開怎么辦
解決方案
問題一 異步化 調(diào)用接口立即返回任務(wù)生產(chǎn)成功
問題二 分批查詢 poi 禁止使用XSSFWorkbook 使用SXSSFWorkbook 或 easy Excel
問題三 分頁(yè)通過滾動(dòng)翻頁(yè)查詢
流式查詢問題:容易長(zhǎng)時(shí)間占用數(shù)據(jù)庫(kù)鏈接池資源。
游標(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)翻頁(yè)查詢
問題四 通過 頁(yè)面或者溝通軟件通知用戶導(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)翻頁(yè)
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í)過長(zhǎng)導(dǎo)致tomcat線程池打滿。
2 MySQL批量查詢、數(shù)據(jù)同步、數(shù)據(jù)導(dǎo)出可以使用類似于分頁(yè)查詢的思路,但是鑒于LIMIT offset,size的效率太低,可以采用”滾動(dòng)翻頁(yè)”的實(shí)現(xiàn)方式 注意要用自增趨勢(shì)的主鍵
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)百萬(wà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ù)交換變得更加簡(jiǎn)單,允許服務(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-03Java如何對(duì)返回參數(shù)進(jìn)行處理
這篇文章主要介紹了Java如何對(duì)返回參數(shù)進(jìn)行處理問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07SpringCloud+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-12Java 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