java如何代码识别excel单元格 删除线_Excel导出长时间占用cpu优化

2782e7a14d4b197fed1cf7d8ce4691fc.png

一、引言

1.1 案例背景

线上监控报警,cpu长时间占用高,后续有客户反映订单导出了很长时间都没导出来。

1.2 本文概述

本文主要讲述案例的分析过程、处理过程和最终成果。

二、分析处理

2.1 问题定位

基本思路,先找出长时间占用cpu的线程,获取线程的堆栈信息,分析相关代码块。

2.1.1 获取服务进程id

# jdk命令 获取所有java进程 jps -l# 或者 ps命令 查看java进程ps -ef | grep 启动的jar名,一般是项目名

2.1.2 找到cpu占用高的线程

# 查找进程中线程占用情况,获取到cpu占用高的线程idtop -Hp pid

呈现如下,按照cpu排序,拿到占用最高的PID.

7427c0953f5e4eff3a40f295ca65fc57.png

2.1.3 查看线程堆栈信息

# 获得线程pid对应16进制的数printf %x 线程pid# 获取堆栈信息jstack 进程id | grep -A 20 16进制线程pid# 堆栈信息如下"export-service-thread-1" #44e prio=5 os_prio=31 tid=0x00007fc37fd7d000 nid=0x1af03 runnable [0x000070001d09d000]   java.lang.Thread.State: RUNNABLE        at org.apache.xmlbeans.impl.store.Locale.count(Locale.java:2055)        at org.apache.xmlbeans.impl.store.Xobj.count_elements(Xobj.java:2050)        at org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTMergeCellsImpl.sizeOfMergeCellArray(Unknown Source)        - locked <0x00000007a71b3080> (a org.apache.xmlbeans.impl.store.Locale)        at org.apache.poi.xssf.usermodel.XSSFSheet.addMergedRegion(XSSFSheet.java:279)        at org.apache.poi.xssf.streaming.SXSSFSheet.addMergedRegion(SXSSFSheet.java:342)        at com.xxServiceImpl.mergeRows(xxServiceImpl.java:747)        at com.xxServiceImpl.writeContent(xxServiceImpl.java:809)

2.1.4 分析堆栈

根据堆栈信息可以看到线程一直处于RUNNABLE状态,而且是一直在执行poi的addMergedRegion方法导致的,这个方法的作用是合并单元格。

源码如下:

public int addMergedRegion(CellRangeAddress region) {    // 校验    region.validate(SpreadsheetVersion.EXCEL2007);    validateArrayFormulas(region);    // 合并  worksheet为CTWorksheet    CTMergeCells ctMergeCells = worksheet.isSetMergeCells() ? worksheet.getMergeCells() : worksheet.addNewMergeCells();    CTMergeCell ctMergeCell = ctMergeCells.addNewMergeCell();    ctMergeCell.setRef(region.formatAsString());    // 计算合并过的单元格总数    return ctMergeCells.sizeOfMergeCellArray();}

代码块2.1

通过堆栈我们可以看到线程一直在执行sizeOfMergeCellArray

源码如下:

public int sizeOfMergeCellArray() {    // 统计前需要先获取锁    synchronized(this.monitor()) {        this.check_orphaned();        // 统计合并单元类型的单元格个数        return this.get_store().count_elements(MERGECELL$0);    }}public int count_elements (QName name){    return _locale.count( this, name, null );}

代码块2.2

通过源码可以看到最终调用到count方法

源码如下:

int count(Xobj parent, QName name, QNameSet set){    int n = 0;    // 遍历所有单元格判断如果是合并单元格类型就+1    for (Xobj x = findNthChildElem(parent, name, set, 0);         x != null; x = x._nextSibling){        if (x.isElem()){            if (set == null){                if (x._name.equals(name))                    n++;            }            else if (set.contains(x._name))                n++;        }    }    return n;}

代码块2.3

分析到这我们得知每次合并单元格都会遍历所有单元格,去统计当前sheet合并的单元格数,如果10万的数据行,每个数据行有10条数据,那么每次需要循环100万次。如果有很多的单元格合并操作,那么这将会是一个庞大的性能损耗。并且代码块2.2可以看到还涉及到锁竞争问题。

我们的excel表因为有大量的单元格合并操作,因此就触发了这个问题。

2.2 合并单元格优化

基本思路:已经得知性能损耗主要是在统计合并单元格的地方,所以我们可以考虑参考源码自己封装一个合并单元格的方法。

通过addMergedRegion的源码我们得知,合并前需要先获取CTMergeCells,然后再进行合并操作,而CTMergeCells是由CTWorksheet生成的,那么就要考虑不同版本的Sheet如何去获取Sheet的CTWorksheet

Poi包含三个版本的Sheet, 分别为 SXSSFSheetXSSFSheetHSSFSheet,简单提一下它们的区别。

HSSF

针对Excel2003版本的excel表进行数据处理的,导出的数据行数有限制,最多导出65535行。文件扩展名为xls。

XSSF

针对Excel2007版本之后的excel表数据,最多导出1048576行。数据量大可能导致内存溢出。文件扩展名为xlsx。

SXSSF

基于XSSFSheet的优化,限制内存中的数据,内存中的数据达到一定程度就写到临时文件,不过涉及到多次io,因此也是一种时间换空间的策略。

在了解三个版本的区别后,我们查看源码如何获取CTWorksheet

XSSFSheet比较简单,本身就提供一个getCTWorksheet()方法,如下:

public CTWorksheet getCTWorksheet() {    return this.worksheet;}

代码块2.4

通过参看源码发现SXSSF是基于XSSFSheet进行封装的,所以我们可以先获取内部的XSSFSheet再去获取CTWorksheet。

获取方式可以通过如下方式:

SXSSFSheet sxssfSheet = (SXSSFSheet) sheet;SXSSFWorkbook sxssfWorkbook = (SXSSFWorkbook) sxssfSheet.getWorkbook();// 根据sheetName获取XSSFSheetXSSFSheet xssfSheet = sxssfWorkbook.getXSSFWorkbook().getSheet(sxssfSheet.getSheetName());ctWorksheet = xssfSheet.getCTWorksheet();

代码块2.5

剩下一个HSSF我从源码里并没有找到相关获取的方法,于是我又去看了HSSF合并单元格的实现,发现HSSF的实现方式不太一样,HSSF每次合并单元格,就会把单元格加入到集合中,这样每次只要获取集合的个数,就可以知道有多少个合并的单元格了。因为HSSF可以直接使用原本的实现。

public int addMergedRegion(int rowFrom, int colFrom, int rowTo, int colTo) {    if (rowTo < rowFrom) {        throw new IllegalArgumentException("The 'to' row (" + rowTo                                           + ") must not be less than the 'from' row (" + rowFrom + ")");    }    if (colTo < colFrom) {        throw new IllegalArgumentException("The 'to' col (" + colTo                                           + ") must not be less than the 'from' col (" + colFrom + ")");    }    // 合并    MergedCellsTable mrt = getMergedRecords();    mrt.addArea(rowFrom, colFrom, rowTo, colTo);    // 获取表中合并过的单元格数    return mrt.getNumberOfMergedRegions()-1;}// 获取表中合并过的单元格数public int getNumberOfMergedRegions() {    return _mergedRegions.size();}

代码块2.6

根据以上分析,我拆分出一个工具类如下:

public class ExportCommonUtils {    /**     * 合并单元格     * */    public static void addMergedRegion(Sheet sheet, CellRangeAddress region){        CTWorksheet ctWorksheet;        // 根据Sheet类型执行不同的单元格合并处理        if (sheet instanceof  SXSSFSheet){            SXSSFSheet sxssfSheet = (SXSSFSheet) sheet;            SXSSFWorkbook sxssfWorkbook = (SXSSFWorkbook) sxssfSheet.getWorkbook();            XSSFSheet xssfSheet = sxssfWorkbook.getXSSFWorkbook().getSheet(sxssfSheet.getSheetName());            ctWorksheet = xssfSheet.getCTWorksheet();        }else if (sheet instanceof XSSFSheet){            XSSFSheet xssfSheet = (XSSFSheet) sheet;            ctWorksheet = xssfSheet.getCTWorksheet();        }else{            sheet.addMergedRegion(region);            return;        }        // 合并单元格        CTMergeCells ctMergeCells = ctWorksheet.isSetMergeCells() ? ctWorksheet.getMergeCells() : ctWorksheet.addNewMergeCells();        CTMergeCell ctMergeCell = ctMergeCells.addNewMergeCell();        ctMergeCell.setRef(region.formatAsString());    }}

代码块2.7

ps:

因为我们业务并不需要统计这个excel表有多少个合并过的单元格,所以我直接把统计这一步去掉。当然如果需要这个返回值,也可以通过一个临时变量来统计,每次都合并都加1。

还有校验的步骤也去掉了,因为校验只是一些越界判断、合并区域是否合法等判断,在合并大量单元格的情况下,也是有部分性能损耗的,所以我把这块也删掉了,由业务方对这块区域合并的可靠性负责。

三、总结

优化效果:

优化前处理1w条需要合并的数据,需要耗时1个多小时,优化后只需要几秒,并且随着数据量的上升,提升的效果越明显。也不会长时间占用cpu了。

ps:

虽然这里把合并数据优化到秒级别,但是如果有大量导出事件同时触发,可能还是会短时间内cpu占用高。因此这块需要交给线程池处理,控制导出的线程数,导出本身不是一种实时性要求很高的业务,因此可以不用追求高实时性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值