使用Java实现百万Excel数据导出

 更新时间:2024年03月07日 09:54:33   作者:梦尘啊  
这篇文章主要为大家详细介绍了如何使用Java实现百万Excel数据导出,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下

可能出现的问题

  • 同步导数据,接口很容易超时。
  • 如果把所有数据一次性装载到内存,很容易引起OOM。
  • 数据量太大sql语句慢。
  • 如果走异步,如何通知用户导出结果
  • 如果excel文件太大,目标用户打不开怎么办

解决方案

问题一 异步化 调用接口立即返回任务生产成功

问题二 分批查询 poi 禁止使用XSSFWorkbook 使用SXSSFWorkbook 或 easy Excel

问题三 分页通过滚动翻页查询

流式查询问题:容易长时间占用数据库链接池资源。

游标查询问题:应用指定每次查询获取的条数fetchSize,MySQL服务器每次只查询指定条数的数据,由于MySQL方不知道客户端什么时候将数据消费完,MySQL需要建立一个临时空间来存放每次查询出的数据,大数据量时MySQL服务器、磁盘占用都会飙升。

故使用滚动翻页查询

问题四 通过 页面或者沟通软件通知用户导出成功 ,并将导出结果上传至oss 后续可直接下载 无需重复导出

问题五 导出可用户设置最大条数

数据准备

CREATE TABLE `t_order`
(
    `id`           BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
    `creator`      VARCHAR(16)     NOT NULL DEFAULT 'admin' COMMENT '创建人',
    `editor`       VARCHAR(16)     NOT NULL DEFAULT 'admin' COMMENT '修改人',
    `create_time`  DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `edit_time`    DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    `version`      BIGINT          NOT NULL DEFAULT 1 COMMENT '版本号',
    `deleted`      TINYINT         NOT NULL DEFAULT 0 COMMENT '软删除标识',
    `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 '支付时间',
    `order_status` TINYINT         NOT NULL DEFAULT 0 COMMENT '订单状态,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();
    }
}

具体实现

easy Excel通过滚动翻页

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", "订单数据", 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);
      //可以添加导出条数限制
        if (list.isEmpty()) {
            writer.finish();
            Date dateAfter = new Date();
            System.out.println("导出列表共执行" + (dateAfter.getTime() - dateBefore.getTime()) + "ms");
            //todo 上传oss 发通知
            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;
                });
    }
}

总结

业务方面

做需求时刻先考虑是不是必须要做 、如果必须要做的情况需要考虑用户的体验和使用感受

技术方面

1 不需要立马返回结果的接口可以采用异步的方式让接口立刻返回结果,可以防止接口耗时过长导致tomcat线程池打满。

2 MySQL批量查询、数据同步、数据导出可以使用类似于分页查询的思路,但是鉴于LIMIT offset,size的效率太低,可以采用”滚动翻页”的实现方式 注意要用自增趋势的主键

3 数据导出需要注意由于大对象频繁创建导致的 full gc 和oom 如果导出较频繁可以考虑拆分单独服务专门做导出

到此这篇关于使用Java实现百万Excel数据导出的文章就介绍到这了,更多相关Java数据导出内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java操作ftp下载文件示例

    java操作ftp下载文件示例

    这篇文章主要介绍了java操作ftp下载文件的示例,需要的朋友可以参考下
    2014-02-02
  • Java单链表反转图文教程

    Java单链表反转图文教程

    这篇文章主要给大家介绍了关于Java单链表反转的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • 详细讲解springboot如何实现异步任务

    详细讲解springboot如何实现异步任务

    异步:异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。也就是说无论异步方法执行代码需要多长时间,跟主线程没有任何影响,主线程可以继续向下执行
    2022-04-04
  • Spring Boot小型项目如何使用异步任务管理器实现不同业务间的解耦

    Spring Boot小型项目如何使用异步任务管理器实现不同业务间的解耦

    这篇文章主要介绍了Spring Boot小型项目如何使用异步任务管理器实现不同业务间的解耦,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-08-08
  • java项目实现图片等比缩放

    java项目实现图片等比缩放

    这篇文章主要为大家详细介绍了java项目实现图片等比缩放,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • RabbitMQ开启SSL与SpringBoot连接测试的配置方法

    RabbitMQ开启SSL与SpringBoot连接测试的配置方法

    本文基于 CentOS 7 + Git + OpenSSL + yum 安装的 RabbitMQ,需要读者提交安装好。其他方式也可变通参考本文。对RabbitMQ开启SSL与SpringBoot连接测试相关知识感兴趣的朋友一起看看吧
    2022-01-01
  • Java如何实现简单的RPC框架

    Java如何实现简单的RPC框架

    这篇文章主要介绍了Java如何实现简单的RPC框架,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • DoytoQuery中的查询映射方案详解

    DoytoQuery中的查询映射方案详解

    这篇文章主要为大家介绍了DoytoQuery中的查询映射方案详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • java springmvc乱码解决归纳整理详解

    java springmvc乱码解决归纳整理详解

    本篇文章介绍了java 中spring mvc 解决乱码的问题方法实例,需要的朋友可以参考下
    2017-04-04
  • Jenkins+maven持续集成的实现

    Jenkins+maven持续集成的实现

    这篇文章主要介绍了Jenkins+maven持续集成的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04

最新评论