Java 导入百万级数据 Excel

自作多情 提交于 2020-03-25 23:37:45

3 月,跳不动了?>>>

在工作中有时候会涉及到一些百万甚至千万级的数据导入到数据,这个时候普通的POI导入方式根本不能满足需求,这个时候我们就需要使用 POI + SAX + DAO 层批处理的方式导入数据

一、下面我们先用普通POI导入数据

添加POI依赖

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.0.1</version>
</dependency>

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.0.1</version>
</dependency>

下面是普通Excel的工具类

public class ExcelUtils {

    public Workbook getFile(String path) {
        InputStream is = null;
        try {
            is = new FileInputStream(path);
            Workbook workbook = null;
            if (path.endsWith(".xlsx")) {
                workbook = new XSSFWorkbook(is);
            } else if (path.endsWith(".xls") || path.endsWith(".et")) {
                workbook = new HSSFWorkbook(is);
            }
            return workbook;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public List getUserList(Workbook workbook){
        //获取Excel表单
        Sheet sheet = workbook.getSheetAt(0);
        Row row = null;
        List<User> users = new ArrayList<User>();
        for(int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
            //获取一行
            row = sheet.getRow(rowNum);
            User user = new User();
            user.setName(getStringValue(row.getCell(0)));
            user.setAge(Integer.valueOf(getStringValue(row.getCell(1))));
            user.setGender(getStringValue(row.getCell(2)));
            user.setIdcard(getStringValue(row.getCell(3)));
            user.setNumber(getStringValue(row.getCell(4)));
            users.add(user);
        }
        return users;
    }

    /**
     * 获取单元格的值的字符串
     * @param cell 单元格对象
     * @return cell单元格的值的字符串
     */
    private static String getStringValue(Cell cell) {
        if (cell == null) {
            return null;
        }
        CellType cellType = cell.getCellType();
        switch (cellType) {
            case STRING:
                return cell.getStringCellValue();
            case NUMERIC:
                double value = cell.getNumericCellValue();
                return String.valueOf(Math.round(value));
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            default:
                return null;
        }
    }
}

User.java,这个是我保存到MySql数据库时候用到的映射对象

@Table("user")
public class User {

    @Column(value = "id", isKey = true)
    private Long id;

    @Column("name")
    private String name;

    @Column("gender")
    private String gender;

    @Column("age")
    private Integer age;

    @Column("idcard")
    private String idcard;

    @Column("number")
    private String number;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getIdcard() {
        return idcard;
    }

    public void setIdcard(String idcard) {
        this.idcard = idcard;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

下面是测试函数,saveBatch()函数是我自己封装了Spring提供的 JdbcTemplate 工具类 这里有介绍

public class Test {

    public static void main(String[] args) {
        Long time = System.currentTimeMillis();
        String path = "C:\\Users\\Administrator\\Desktop\\user.xlsx";
        ExcelUtils utils = new ExcelUtils();
        Workbook file = utils.getFile(path);
        List list = utils.getUserList(file);
        System.out.println("时间" + (System.currentTimeMillis() - time));
        System.out.println(list.size());
        saveBatch(list);
        System.out.println("保存时间" + (System.currentTimeMillis() - time));
    }



    public static void saveBatch(List<User> list) {
        try {
            BaseDao<User> baseDao = new BaseDao<User>();
            JdbcTemplate template = new JdbcTemplate(JdbcTemplateUtils.getDataSource());
            baseDao.setJdbcTemplate(template);
            baseDao.batchSave(list);
            template.getDataSource().getConnection().close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我用上面这段测试了五万数据与十万数据,五万数据还是能保存,但是即使能保存效率也极低。而导入十万甚至百万数据的时候,别说持久化到数据库,在我的程序里面读也没有读取成功,读取速度超级慢,读了几分钟之后抛出了个java.lang.OutOfMemoryError异常,也就是内存溢出(OOM)

二、这个时候我们就需要用到SAX

SAX就是把我们的Excel文件转成XML文件,然后逐行扫描并且解析,并且SAX解析XML要比DOM解析XML更快。SAX与DOM操作不同的是不需要把文件数据保存到我们的内存,不需要构建一棵庞大的数据树,这样能减少内存消耗提升数据读取速度,避免抛出OOM

下面是POI + SAX方式批量插入数据

添加SAX依赖

<dependency>
    <groupId>xerces</groupId>
    <artifactId>xercesImpl</artifactId>
    <version>2.11.0</version>
</dependency>

下面是 POI +SAX 实现导入的代码

public class SaxUtils {

    // 存整列表
    public List<User> dataList = new ArrayList<User>();
    // 存每行    private User user;
    // 记录当前遍到第几列
    private int cellIndex = 0;
    // 取的文件
    private final String filename;


    public SaxUtils(String filename) throws Exception{
        if(filename == null || "".equals(filename)) throw new Exception("文件名不能空");
        this.filename = filename;
        processFirstSheet();
    }
    /**
     * 指定取第一sheet
     * @throws Exception
     */
    private void processFirstSheet() throws Exception {
        OPCPackage pkg = OPCPackage.open(filename);
        XSSFReader r = new XSSFReader( pkg );
        SharedStringsTable sst = r.getSharedStringsTable();

        XMLReader parser = fetchSheetParser(sst);
        // 取第一 sheet
        InputStream sheet1 = r.getSheet("rId1");
        InputSource sheetSource = new InputSource(sheet1);
        // 利用XML解析 sheet
        parser.parse(sheetSource);
        sheet1.close();
        sst.close();
        pkg.close();
    }

    /**
     * XML 解析器
     * @param sst
     * @return
     * @throws SAXException
     */
    private XMLReader fetchSheetParser(SharedStringsTable sst) throws SAXException {
        XMLReader parser =
                XMLReaderFactory.createXMLReader(
                        "org.apache.xerces.parsers.SAXParser"
                );
        ContentHandler handler = new ReadHandler(sst);
        parser.setContentHandler(handler);
        return parser;
    }

    /**
     * 实现DefaultHandler      *  startElement(String uri, String localName, String name, Attributes attributes)
     *  endElement(String uri, String localName, String name)
     *  characters(char[] ch, int start, int length)
     *  endDocument()
     */
    private  class ReadHandler extends DefaultHandler {
        private SharedStringsTable sst;
        private String lastContents;
        private boolean isSstIndex;

        private ReadHandler(SharedStringsTable sst) {
            this.sst = sst;
        }
        /**
         * 个单元格         */
        @Override
        public void startElement(String uri, String localName, String name,
                                 Attributes attributes) throws SAXException {
            // c => cell
            if(name.equals("c")) {
                // 取元素坐                String index = attributes.getValue("r");
                // 是否新行
                if(Pattern.compile("^A[0-9]+$").matcher(index).find()){

                    //存保存上一行                    if(user!=null && !StringUtils.isEmpty(user.getName())){
                        dataList.add(user);
                    }
                    // 新行要先除上一行的                    user = new User();
                    // 重置前列
                    cellIndex = 0;
                }
                // 断该值是否是SST中的一索引
                String cellType = attributes.getValue("t");
                if(cellType != null && cellType.equals("s")) {
                    isSstIndex = true;
                } else {
                    isSstIndex = false;
                }

            }
            // Clear contents cache
            lastContents = "";
        }
        /**
         * 个单元格         */
        @Override
        public void endElement(String uri, String localName, String name)
                throws SAXException {
            // Process the last contents as required.
            // Do now, as characters() may be called more than once
            if(isSstIndex) {
                int idx = Integer.parseInt(lastContents);
                lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
                isSstIndex = false;
            }

            // v 是每个单元格 cell            // 如果 v 保存
            if(name.equals("v")) {
                if(cellIndex == 0) {
                    user.setName(lastContents);
                } else if (cellIndex == 1) {
                    user.setAge(Integer.parseInt(lastContents));
                } else if (cellIndex == 2) {
                    user.setGender(lastContents);
                } else if (cellIndex == 3) {
                    user.setIdcard(lastContents);
                } else if (cellIndex == 4) {
                    user.setNumber(lastContents);
                }
                cellIndex++;
            }

        }

        @Override
        public void characters(char[] ch, int start, int length)
                throws SAXException {
            lastContents += new String(ch, start, length);
        }
        /**
         * 如果文档结束后,发现读取的末尾行正前行中,存         * (存在这样取的末尾行正好是文最后一行,最后一行无法存到集合中,
         * 最后一行有下一行了,所以不为启动starElement()方法,
         * 然我可以通指定最大列来处理,但不想那做,展性不好)
         */
        @Override
        public void endDocument() throws SAXException {
            if(user!=null && !StringUtils.isEmpty(user.getName())){
                dataList.add(user);
            }

        }

    }
}

测试类:

public class Test {

    public static void main(String[] args) throws Exception {

        long time = System.currentTimeMillis();
        SaxUtils reader = new SaxUtils("C:\\Users\\Administrator\\Desktop\\user.xlsx");

        System.out.println("据量:" + reader.dataList.size());
        System.out.println("时间" + ((System.currentTimeMillis() - time) / 1000.0));
        long time2 = System.currentTimeMillis();
        saveBatch(reader.dataList);
        System.out.println("保存时间" + ((System.currentTimeMillis() - time2) / 1000.0));
    }



    public static void saveBatch(List<User> list) {
        try {
            BaseDao<User> baseDao = new BaseDao<User>();
            JdbcTemplate template = new JdbcTemplate(JdbcTemplateUtils.getDataSource());
            baseDao.setJdbcTemplate(template);
            baseDao.batchSave(list);
            template.getDataSource().getConnection().close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果:

如果我们不修改mysql配置直接插入会出现下面的错误提示,并且会提示我们配置 max_allowed_packet 参数,原因是插入的数据太大。除了这个配置,我们再加一个配置 innodb_buffer_pool_sizeinnodb_buffer_pool_size 百万级数据配不配都可以,工作中根据实际情况配置,默认是128M) 配置了这个缓存区我们批量插入操作的时候可以更快,并且把一百万数据拆分成每次保存十万条数据

接下来我们需要修改my.cnf文件,把插入的内存调大点

然后再执行,持久化时间是25秒数据也成功插入了

三、使用原生JDBC做批量持久化操作

上面的效率还是有点慢,因为各种ORM框架封装的细节我们很难把控,这个时候我们换成原生的JDBC批处理再测试下

public class JdbcTest {

    private String url = "jdbc:mysql://localhost:3306/winfongs?characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true";
    private String user = "root";
    private String password = "123456";


    public void batchSave(List<User> users){
        Connection conn = null;
        PreparedStatement pstm =null;
        ResultSet rt = null;
        try {
            // JDBC驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection(url, user, password);
            // 置批sql
            String sql = "INSERT INTO user(name,gender,age,idcard,number) VALUES(?,?,?,?,?)";
            pstm = conn.prepareStatement(sql);
            // 禁止自提交事            conn.setAutoCommit(false);
            Long startTime = System.currentTimeMillis();
            System.out.println("开始执行持久化!!");
            // 历数入的            for (int j = 0,i = 0; j < users.size(); j++) {
                User user = users.get(j);
                pstm.setString(1, user.getName());
                pstm.setString(2, user.getGender());
                pstm.setInt(3, user.getAge());
                pstm.setString(4, user.getIdcard());
                pstm.setString(5, user.getNumber());
                pstm.addBatch();
                if( (i == 100000) ||  (j == users.size() - 1) ) {
                    // 提交事                    pstm.executeBatch();
                    conn.commit();
                    i = 0;
                }
                i++;
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }finally{
            if(pstm!=null){
                try {
                    pstm.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                    throw new RuntimeException(e);
                }
            }
            if(conn!=null){
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
public static void main(String[] args) {
    try {
        long time = System.currentTimeMillis();
        SaxUtils reader = new SaxUtils("C:\\Users\\Administrator\\Desktop\\user.xlsx");
        System.out.println("数据量:" + reader.dataList.size());
        System.out.println("读取数据时间:" + ((System.currentTimeMillis() - time) / 1000.0));
        long time2 = System.currentTimeMillis();
        new JdbcTest().batchSave(reader.dataList);
        System.out.println("保存时间:" + ((System.currentTimeMillis() - time2) / 1000.0));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

使用原生JDBC保存百万数据只需要13S时间,解析SAX加上持久化时间在30S左右,个人认为还是可以接受,不知道有没有办法把整个流程优化到 15S 以内甚至更短

上面的测试,数据库连接都需要加上 rewriteBatchedStatements=true 这个参数,不然速度还是会很慢

、SAX操作流程

最后总结一下

想要导入百万数据甚至千万数据(CSV文件才能保存千万级数据)需要注意的几点

1.导入文件方式使用 POI + SAX 方式导入

2.DAO层使用批处理(使用原生JDBC批处理+事务)

3.配置MySql 的插入内存参数 max_allowed_packet

4.配置MySql 的插入缓存参数  innodb_buffer_pool_size 或者 bulk_insert_buffer_size (innodb使用innodb_buffer_pool_size,MyISAM使用bulk_insert_buffer_size )

5.JDBC连接加上参数rewriteBatchedStatements=true

6.网络资源,同样的代码与数据库,我自己的网络保存的时间与在公司测试的时间相差巨大,因为个人使用的网络和公司使用的不是一个级别的,最后用了本地虚拟机的数据库操作保存,速度是最快的

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!