记一次批量导入数据过慢的解决方案

问题

​ 在做统计工作时,由于需要大量的基础数据,所以生成了一份测试数据。但是在实际执行数据脚本时,发现执行速度实在是太慢了。3000 条数据跑了5分钟还没有结束。这样下去感觉下班之前数据都导不完。于是在一通百度下,整理出了如下的解决方式。

当我们在数据库中添加一条记录的过程 各方的耗时是这样的

  • 链接耗时 (30%)
  • 发送 query 到服务器 (20%)
  • 解析 query (20%)
  • 插入操作 (10% * 词条数目)
  • 插入 index (10% * Index的数目)
  • 关闭链接 (10%)

单值插入 最慢

INSERT INTO 【TABLE_NAME】 VALUES() 

多值插入 较快

增加SQL的长度

减少多次IO操作

INSERT INTO 【TABLE_NAME】 VALUES(),(),(),()

LOAD DATA LOCAL INFILE 方式 Mysql 官网文档地址

LOAD DATA LOCAL INFILE 【FILE_PATH】 INTO TABLE 【TABLE_NAME】

如果还不能满足的话 那么还有如下的地方可以优化

多线程插入

在同一个事务内提交大量 插入

数据库引擎使用 myisam 替代 innoDB

个人的解决方式(只适用于当下情况)附代码:

package utils;

import org.springframework.util.StringUtils;

import java.io.*;
import java.util.LinkedList;

/**
 * 适用于 Navicat 导出的脚本 在执行时过于缓慢的问题
 * 解决方案
 * <p>
 *     分割数据 同表的数据建立单个的文件 表名.sql
 *     获取所有的表结构 table.sql
 *     创建加载数据的脚本 load.sql
 *
 *     执行顺序 table.sql -> load.sql
 * </p>
 *
 * @author yang yang
 * @email m17610912950@163.com
 * @time 2019年08月19日 14:21
 **/
public class MysqlDataImportUtils {

    public static LinkedList<String> tableList = new LinkedList<String>();

    public static void main(String args[]) {
        String path = "C:\\Users\\Administrator\\Desktop\\g.sql";
        try {
            splitSqlFile(path);
            writeSqlFile();
            getTableFile(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 将SQL文件切割
     *
     * @param filePath
     * @return void
     * @date 2019-08-19
     * @author yang yang
     **/
    public static void splitSqlFile(String filePath) throws Exception {
        //创建输出流
        BufferedWriter out = null;
        //创建输入流 用于读取SQL文件
        FileReader reader = new FileReader(filePath);
        BufferedReader br = new BufferedReader(reader);
        try {
            String line;
            String tn = null;
            //当有下一行时 读取
            while ((line = br.readLine()) != null) {
                //当读取到的行的信息是 添加的记录时
                if (line.contains("INSERT") || line.contains("INTO") || line.contains("VALUES")) {
                    //获取表的名称
                    String tableName = line.substring(line.indexOf("`") + 1, line.lastIndexOf("`"));
                    //缓存中的表名为空 且获取到的表名称非空时 (第一次读取到添加的记录)
                    if (StringUtils.isEmpty(tn) && !StringUtils.isEmpty(tableName)) {
                        tn = tableName;
                        //创建文件
                        File writeName = new File(tn + ".sql");
                        // 创建新文件,有同名的文件的话直接覆盖
                        boolean newFile = writeName.createNewFile();
                        //创建写入流
                        FileWriter writer = new FileWriter(writeName);
                        out = new BufferedWriter(writer);
                        // 缓存中添加 表的名称 用于后续的处理【用于生成加载 添加数据的 脚本】
                        tableList.add(tn);
                    }
                    if (!StringUtils.isEmpty(tn) && !StringUtils.isEmpty(tableName)) {
                        //表名是否一致 如果一致 向文件中写入数据 【此时读取到 当前表的数据】
                        if (tn.equalsIgnoreCase(tableName)) {
                            String tableInfo = line.substring(line.indexOf("(") + 1, line.indexOf(")"));
                            String all = tableInfo.replaceAll(", ", "|");
                            all = all.replaceAll("'", "");
                            all = all.replaceAll("`", "");
                            out.write(all + ",\r\n");
                        }
                        //表名不一致时 释放输出流 【此时读取到 另外一个表的添加记录】
                        if (!tn.equalsIgnoreCase(tableName)) {
                            out.flush();
                            out.close();
                            out = null;
                            tn = null;
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭读入流
            reader.close();
            // 关闭输出流
            if (null != out) {
                out.flush();
                out.close();
            }
        }
    }

    /**
     * 获取SQL文件中所有 建表语句
     * @param filePath
     * @return void
     * @date 2019-08-19
     * @author yang yang
     **/
    public static void getTableFile(String filePath) throws Exception {
        try {
            //读取文件
            FileReader reader = new FileReader(filePath);
            BufferedReader br = new BufferedReader(reader);
            //用于写出 文件
            File writeName = new File("table.sql");
            writeName.createNewFile(); // 创建新文件,有同名的文件的话直接覆盖
            try (FileWriter writer = new FileWriter(writeName);
                 BufferedWriter out = new BufferedWriter(writer)
            ) {
                String line;
                while ((line = br.readLine()) != null) {
                    if (!(line.contains("INSERT") || line.contains("INTO") || line.contains("VALUES"))) {
                        out.write(line + "\r\n");
                    }
                }
                out.flush(); // 把缓存区内容压入文件
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 用于数据库加载分割后数据的脚本
     * @param
     * @return void
     * @date 2019-08-19
     * @author yang yang
     **/
    public static void writeSqlFile() throws Exception {
        try {
            // 相对路径,如果没有则要建立一个新的output.txt文件 【输出的脚本文件】
            File writeName = new File("load.sql");
            writeName.createNewFile(); // 创建新文件,有同名的文件的话直接覆盖
            try (FileWriter writer = new FileWriter(writeName);
                 BufferedWriter out = new BufferedWriter(writer)
            ) {
                for (String tableName : tableList) {
                    StringBuffer sb = new StringBuffer();
                    sb.append(" LOAD DATA LOCAL INFILE 'F:/workspace/dev/2.0.0/" + tableName + ".sql' INTO TABLE " + tableName + " ");
                    sb.append(" CHARACTER SET utf8 ");
                    sb.append(" FIELDS TERMINATED BY '|' ");
                    sb.append(" OPTIONALLY ENCLOSED BY '' ");
                    sb.append(" ESCAPED BY '\\\\' ");
                    sb.append(" LINES TERMINATED BY '\n'; ");
                    out.write(sb.toString() + "\r\n");
                }
                out.flush(); // 把缓存区内容压入文件
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}