背景
终于遇见了百万级数据的项目,很简单的一个单表查询,索引加上去之后还是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