插入百万数据优化

背景

终于遇见了百万级数据的项目,很简单的一个单表查询,索引加上去之后还是20多秒。没办法只得周末的时候研究一下百万级数据查询的优化。首先第一步就是先插入百万级的数据。我刚开始使用的方法是存储过程,想着存储过程会快一些,也更通用一些。没想到500万的数据既然插入了2小时!!!
于是继续百度,看文章,分析,总结,思考。
总算确定了实现的方式:多值表插入(insert values 值集1,值集2)、多sql一起提交。
1、一个sql语句拼10个value(根据mysql最长能支持的sql长度和自己的查询字段长度,综合考虑。我这里可能还拼得少了)。
2、每次提交sql数10个(有文章说这个数效率最高,我也不是很清楚,反正效率达到要求才是最重要的)。
3、这样就是100了,还要50000次循环,这样明显太慢了,需要多线程去完成上面的工作,线程数定为16(本机核心数8的两倍)。
4、每个线程需要处理的任务,平均下来也有3125个。所以考虑用队列来存储任务(队列方式效率比较高)。
总的来说,也可以分为下面的两步:
第一步:java 工程 多线程,队列实现sql语句的组装。
ps:
第二步:阿里的druid连接池,实现sql到数据库的执行。

开始

目标500万数据!

获取数据库连接

第一步肯定是想办法获取数据库的连接,新建一个DataSourceUtil工具类。

import com.alibaba.druid.pool.DruidDataSource;

import java.sql.Connection;

/**
 * @author: aliyu
 * @create: 2021-06-26 23:09
 * @description:
 */
public class DataSourceUtil {
    
    

    private static DruidDataSource druidDataSource;

    //初始化数据源
    static{
    
    
        druidDataSource = new DruidDataSource();
        //设置数据库连接参数
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/db_myframe?serverTimezone=GMT");
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("XXXXX");
        //配置初始化大小、最小、最大
        druidDataSource.setInitialSize(16);
        druidDataSource.setMinIdle(1);
        druidDataSource.setMaxActive(16);
        //连接泄漏监测
        druidDataSource.setRemoveAbandoned(true);
        druidDataSource.setRemoveAbandonedTimeout(30);
        //配置获取连接等待超时的时间
        druidDataSource.setMaxWait(20000);
        //配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        druidDataSource.setTimeBetweenEvictionRunsMillis(20000);
        //防止过期
        druidDataSource.setValidationQuery("SELECT 'x'");
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setTestOnBorrow(true);

    }
    /*一个方法从连接池中获取连接*/
    public static Connection getConnect() {
    
    
        Connection con = null;
        try {
    
    
            con = druidDataSource.getConnection();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return con;
    }


}

ps: 主要就数据源的配置,url,用户名,密码这类
还有就是,连接数我配置为自己电脑核心数的两倍—16(8*2=16),并且初始化也是16(火力全开)。
然后 getConnect方法获取连接。

创建线程类

根据上面的分析,每个sql组10个values,每10个sql提交一个,那么一个线程一次执行一次任务可以插入100条,需要50000个任务。这50000个任务由16个线程共同完成。

public class InsertDataThread implements Runnable {
    
    
    /**
     * 初始化队列长度为50000,16个线程一起完成50000个任务
     */
    private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(50000);
    /**
     * 模拟先插入前天的记录
     */
    private static String create_date = "DATE_SUB(current_time(3), INTERVAL 2 day)";
    private static String create_time = "DATE_SUB(curdate(), INTERVAL 2 day)";
    /**
     * 构造方法中为队列初始化赋值
     */
    public InsertDataThread() {
    
    
        for (int j = 0; j < 50000; j++) {
    
    
            queue.offer(j);
        }
    }

    /**
     * 线程任务执行(每次任务插入记录数为10个values乘以 10个sql一起提交 = 100条记录)
     */
    @Override
    public void run() {
    
    
        while (true) {
    
    
            try {
    
    
                synchronized (this){
    
        //queue作为线程共有的参数,肯定一个时间内只能有一个线程操作
                    queue.poll(); //取出队头(队列是阻塞的,要一个一个的执行)
                    if(queue.size() == 33200){
    
        //模拟昨天的数据
                        create_date = "DATE_SUB(current_time(3), INTERVAL 1 day)";
                        create_time = "DATE_SUB(curdate(), INTERVAL 1 day)";
                    }else if(queue.size() == 16600){
    
      //模拟今天的数据
                        create_date = "current_time(3)";
                        create_time = "curdate()";
                    }else if(queue.size() == 0){
    
    
                        break;
                    }
                    System.out.println("执行到了第"+ queue.size());
                }
                /*一个队列插入了100条*/
                Connection conn = DataSourceUtil.getConnect();
                conn.setAutoCommit(false); //需要关掉自动提交才可以多个sql一起提交
                for (int k = 0; k < 10; k++) {
    
    
                    /*一个sql 语句 10 条*/
                    StringBuffer sb = new StringBuffer();
                    sb.append("insert into db_myframe.t_authority (authority, authority_type, url, authority_parentid , remark, show_status, hide_children, authority_seq, create_time, create_date) values ");
                    for (int j = 0; j < 10; j++) {
    
    
                        sb.append("('学生管理', 10, '/mocknnd', 0, '学生管理', 1, 1, 1, "+create_time+", "+create_date+"), ");
                    }
                    String sql = sb.toString().substring(0, sb.lastIndexOf(","));
                    // 最后在所有命令执行完之后再提交事务conn.commit();
                    PreparedStatement pstmt = conn.prepareStatement(sql);
                    pstmt.execute();
                    pstmt.close();  //线程池管理的可能这个不需要自己写了
                }
                conn.commit(); // 每10个sql提交一次事务
                conn.close(); //线程池管理的可能这个不需要自己写了
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

写一个测试类执行插入

public class InsertMillionDataTest {
    
    

    public static void main(String[] args) {
    
    
        InsertDataThread insertDataThread = new InsertDataThread();
        //核心数的两倍16个线程去执行任务
        for (int i = 0; i < 16; i++) {
    
    
            Thread th1 = new Thread(insertDataThread);
            th1.start();
        }
    }
}

其他

貌似,100提交一次的话执行时间2分59秒。
在这里插入图片描述
主要参考文章地址:https://blog.csdn.net/weixin_39561577/article/details/111257340

猜你喜欢

转载自blog.csdn.net/mofsfely2/article/details/118283519