惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
人人都是产品经理
人人都是产品经理
Project Zero
Project Zero
T
Tailwind CSS Blog
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
L
LINUX DO - 热门话题
Spread Privacy
Spread Privacy
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
C
Cisco Blogs
T
The Exploit Database - CXSecurity.com
C
CXSECURITY Database RSS Feed - CXSecurity.com
Scott Helme
Scott Helme
Security Archives - TechRepublic
Security Archives - TechRepublic
H
Heimdal Security Blog
博客园 - 【当耐特】
W
WeLiveSecurity
J
Java Code Geeks
Latest news
Latest news
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Troy Hunt's Blog
博客园 - Franky
月光博客
月光博客
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
PCI Perspectives
PCI Perspectives
博客园_首页
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
P
Palo Alto Networks Blog
I
InfoQ
Security Latest
Security Latest
Hacker News: Ask HN
Hacker News: Ask HN
Microsoft Azure Blog
Microsoft Azure Blog
M
MIT News - Artificial intelligence
Help Net Security
Help Net Security
F
Full Disclosure
Cyberwarzone
Cyberwarzone
D
DataBreaches.Net
The Cloudflare Blog
S
Securelist
美团技术团队
C
Cybersecurity and Infrastructure Security Agency CISA
AI
AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events

博客园 - npe0

策略模式在项目中的应用 博文阅读密码验证 - 博客园 MyBatis-Plus 的动态SQL片段用法 EasyExcel的多级表头 docker镜像搬运命令 macbook绕过安全控制安装第三方软件 Python中使用列表、map和filter函数配合lambda表达式来操作集合 阿里通义千问大模型初探 java.io.IOException: Cannot run program “az”: CreateProcess error=2, 系统找不到指定的文件。 Git使用问题记录 seata的分布式事务处理机制 JVM常用命令 你可能不知道的Collectors用法 easyexcel导出Bigdecimal数据格式问题 arthas 常用命令 对find,xargs,grep和管道的一些深入理解 PlantUML 分库分表 jvm优化
百万千万级Excel导出
npe0 · 2023-03-20 · via 博客园 - npe0

一、前提

  • 一个Excel的sheet页最多104万行数据
  • 需分页处理-数据库分页或内存分页(这里需要注意内存大小问题)

二、处理思路

2.1、同步处理

*单线程处理-前端轮训等待时间过长,不可取
*多线程处理-前端依然轮训等待时间过长,后端还会因为大量线程消耗内存,导致内存不够用,不可取

2.2、异步处理

*前端点击导出按钮后,提示用户,导出任务已建立,待导出成功后,发消息通知用户下载

*引入MQ,自发自收,前端每次点击导出,任务需到MQ中去排队,这样能保证每个Pod中只有一个导出任务在执行

*消费者消费到下载消息时,做分页查询(数据库或内存)处理,并使用EasyExcel做多sheet也写入

*写入完毕,上传文件到OSS,并获取下载链接

*钉消息通知用户下载完成,附带下载地址

三、伪代码

/**
 * @author chong.du
 * @description
 * @date 2023/3/20 16:41
 */
@Slf4j
@Component
public class ExcelUtil {

  /**
   * 每页的条数
   */
  private static final int PER_SHEET_ROW_COUNT = 1000000;

  @Autowired
  private OssClientUtils ossClientUtils;
  @Autowired
  private GroupService groupService;
  @Autowired
  private DetailService detailService;

   public  void export(String empId){

    ExcelWriter writer =null;
    try {

        StopWatch stopWatch = new StopWatch();
        stopWatch.start("归集数据查询耗时");
        List<Group> sheetOneList = groupService.queryGroup();
        //分组归集sheet页查询
        log.info("分组-导出-共计:{}条", sheetOneList.size());
        stopWatch.stop();
        log.info(stopWatch.prettyPrint());
        stopWatch.start("明细数据查询耗时");
        List<Detail> sheetTwoList = Lists.newArrayList();
        //按分组
        Map<String, List<Group>> groupList = sheetOneList.stream()
            .collect(Collectors.groupingBy(Group::getGroupId));
        groupList.entrySet().stream().forEach(entry->{
          List<Detail> exportDtoList = detailService.getDetailDtoList(entry.getKey());
          if (!CollectionUtils.isEmpty(exportDtoList)) {
            sheetTwoList.addAll(exportDtoList);
          }
        });
        sheetTwoList.sort(Comparator.comparing(Detail::getHeadId));
        stopWatch.stop();
        log.info(stopWatch.prettyPrint());
        log.info("导出明细-导出-共计:{}条", sheetTwoList.size());

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        writer = EasyExcel.write(out).autoCloseStream(Boolean.FALSE)
            .registerWriteHandler(new AutoColumnWidthWriteHandler())
            .build();
        //1、分组sheet写入
        WriteSheet sheet1 = EasyExcel.writerSheet(0, "分组列表").head(Group.class).build();
        writer.write(sheetOneList, sheet1);
        //2、明细sheet表分页,每1000000数据为一个sheet
        int totalPage = sheetTwoList.size() / PER_SHEET_ROW_COUNT + (sheetTwoList.size() % PER_SHEET_ROW_COUNT == 0 ? 0 : 1);
        List<Detail> list = null;
        for (int i = 1; i <= totalPage; i++) {
          int skipNum = PER_SHEET_ROW_COUNT * (i - 1);
          list = sheetTwoList.stream()
              .skip(skipNum)
              .limit(PER_SHEET_ROW_COUNT)
              .collect(Collectors.toList());
          WriteSheet sheet2 = EasyExcel.writerSheet(i, "明细列表" + i).head(Detail.class).build();
          writer.write(list, sheet2);
        }
        //3、关闭流
        writer.finish();

        //4、上传文件到OSS
        log.info("=====>{}-文件下载消费者接收到消息后,写入Excel完成,开始上传OSS",empId);
        String yyyyMMdd_hHmmss = DateUtil.format(new Date(), "yyyyMMdd_HHmmss");
        String fileName = StringUtils.join("明细列表导出_", yyyyMMdd_hHmmss, ".xlsx");
        String downloadUrl = ossClientUtils.putFileReturnUrl(new ByteArrayInputStream(out.toByteArray()), fileName);
        log.info("=====>{}-文件下载消费者接收到消息后,写入Excel完成,上传OSS完成",empId);

        //4、异步通知-发送钉钉模板消息通知
        Map<String, Object> params = Maps.newHashMap();
        params.put("msgTitle", "文件下载完成通知");
        params.put("content",fileName);
        params.put("linkWords","查看详情");
        params.put("link",downloadUrl);
        dingMsgSendUtil.sendTemplateMsgToEmp(empId,params);
        log.info("=====>{}-文件下载消费者接收到消息后,写入Excel完成,发送钉钉消息完成",empId);

    } catch (Exception e) {
      log.error("=====>文件下载消费消息异常, msg:{}", e);
      e.printStackTrace();
    }
    finally{
      if (Objects.nonNull(writer)) {
        writer.finish();
      }
    }
  }
}