10亿数据如何最快插入MySQL?( 二 )


MySQL 单库的并发写入是有性能瓶颈的,一般情况5K TPS写入就很高了 。
当前数据都采用 SSD 存储,性能应该更好一些 。但如果是HDD的话,虽然顺序读写会有非常高的表现 , 但HDD无法应对并发写入,例如每个库10张表,假设10张表在并发写入,每张表虽然是顺序写入,由于多个表的存储位置不同,HDD只有1个磁头,不支持并发写 , 只能重新寻道,耗时将大大增加 , 失去顺序读写的高性能 。
所以对于HDD而言,单库并发写多个表并不是好的方案 。回到SSD的场景,不同SSD厂商的写入能力不同,对于并发写入的能力也不同 , 有的支持500M/s,有的支持1G/s读写,有的支持8个并发,有的支持4个并发 。在线上实验之前,我们并不知道实际的性能表现如何 。
所以在设计上要更加灵活,需要支持以下能力:

  • 支持配置数据库的数量
  • 支持配置并发写表的数量(如果MySQL是HDD磁盘,只让一张表顺序写入,其他任务等待)
通过以上配置,灵活调整线上数据库的数量,以及写表并发度,无论是HDD还是SSD,我们系统都能支持 。不论是什么厂商型号的SSD,性能表现如何,都可调整配置,不断获得更高的性能 。这也是后面设计的思路 , 不固定某一个阈值数量 , 都要动态可调整 。
接下来聊一下文件读取 , 10亿条数据,每条1K,一共是931G 。近1T大文件 , 一般不会生成如此大的文件 。所以我们默认文件已经被大致切分为100个文件 。每个文件数量大致相同即可 。
为什么切割为100个呢?切分为1000个,增大读取并发,不是可以更快导入数据库吗?刚才提到数据库的读写性能受限于磁盘,但任何磁盘相比写操作 , 读操作都要更快 。尤其是读取时只需要从文件读取,但写入时MySQL要执行建立索引 , 解析SQL、事务等等复杂的流程 。所以写的并发度最大是100,读文件的并发度无需超过100 。
更重要的是读文件并发度等于分表数量 , 有利于简化模型设计 。即100个读取任务,100个写入任务,对应100张表 。
五、如何保证写入数据库有序
既然文件被切分为100个10G的小文件 , 可以按照文件后缀+ 在文件行号 作为记录的唯一键,同时保证同一个文件的内容被写入同一个表 。例如:
  • index_90.txt 被写入 数据库database_9,table_0,
  • index_67.txt 被写入 数据库 database_6,table_7 。
这样每个表都是有序的 。整体有序通过数据库后缀+表名后缀实现 。
六、如何更快地读取文件
10G的文件显然不能一次性读取到内存中,场景的文件读取包括:
  • Files.readAllBytes一次性加载内存
  • FileReader+ BufferedReader 逐行读取
  • File+ BufferedReader
  • Scanner逐行读取
  • JAVA NIO FileChannel缓冲区方式读取
在mac上 , 使用这几种方式读取3.4G大小文件的性能对比:
10亿数据如何最快插入MySQL?

文章插图
详细的评测内容请参考:读取文件性能比较 (zhuanlan.zhihu.com/p/142029812)
由此可见,使用JavaNIO FileChannnel明显更优 , 但是FileChannel的方式是先读取固定大小缓冲区 , 不支持按行读取 。也无法保证缓冲区正好包括整数行数据 。如果缓冲区最后一个字节正好卡在一行数据中间,还需要额外配合读取下一批数据 。如何把缓冲区变为一行行数据 , 比较困难 。
File file = new File("/xxx.zip");
FileInputStream fileInputStream = null;
long now = System.currentTimeMillis();
try {
fileInputStream = new FileInputStream(file);
FileChannel fileChannel = fileInputStream.getChannel();
int capacity = 1 * 1024 * 1024;//1M
ByteBuffer byteBuffer = ByteBuffer.allocate(capacity);
StringBuffer buffer = new StringBuffer();
int size = 0;
while (fileChannel.read(byteBuffer) != -1) {
//读取后,将位置置为0,将limit置为容量, 以备下次读入到字节缓冲中,从0开始存储
byteBuffer.clear();
byte[] bytes = byteBuffer.array();
size += bytes.length;
}
System.out.println("file size:" + size);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//TODO close资源.
}
System.out.println("Time:" + (System.currentTimeMillis() - now));
JavaNIO 是基于缓冲区的,ByteBuffer可转为byte数组,需要转为字符串 , 并且要处理按行截断 。


推荐阅读