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


但是BufferedReader JavaIO方式读取可以天然支持按行截断,况且性能还不错,10G文件,大致只需要读取30s,由于导入的整体瓶颈在写入部分,即便30s读取完,也不会影响整体性能 。所以文件读取使用BufferedReader 逐行读取,即方案3 。
七、如何协调读文件任务和写数据库任务
这块比较混乱,请耐心看完 。
100个读取任务,每个任务读取一批数据,立即写入数据库是否可以呢?前面提到了由于数据库并发写入的瓶颈 , 无法满足1个库同时并发大批量写入10个表,所以100个任务同时写入数据库,势必导致每个库同时有10个表同时在顺序写,这加剧了磁盘的并发写压力 。
为尽可能提高速度 , 减少磁盘并发写入带来的性能下降,需要一部分写入任务被暂停的 。那么读取任务需要限制并发度吗?不需要 。
假设写入任务和读取任务合并,会影响读取任务并发度 。初步计划读取任务和写入任务各自处理,谁也不耽误谁 。但实际设计时发现这个方案较为困难 。
最初的设想是引入Kafka,即100个读取任务把数据投递到Kafka,由写入任务消费kafka写入DB 。100个读取任务把消息投递到Kafka,此时顺序就被打乱了,如何保证有序写入数据库呢?我想到可以使用Kafka partition路由,即读取任务id把同一任务的消息都路由到同一个partition,保证每个partition内有序消费 。
要准备多少个分片呢?100个很明显太多,如果partition小于100个,例如10个 。那么势必存在多个任务的消息混合在一起 。如果同一个库的多个表在一个Kafka partition , 且这个数据库只支持单表批量写入 , 不支持并发写多个表 。这个库多个表的消息混在一个分片中,由于并发度的限制,不支持写入的表对应的消息只能被丢弃 。所以这个方案既复杂,又难以实现 。
所以最终放弃了Kafka方案 , 也暂时放弃了将读取和写入任务分离的方案 。
最终方案简化为 读取任务读一批数据 , 写入一批 。即任务既负责读文件、又负责插入数据库 。
八、如何保证任务的可靠性
如果读取任务进行到一半,宕机或者服务发布如何处理呢?或者数据库故障,一直写入失败,任务被暂时终止,如何保证任务再次拉起时 , 在断点处继续处理,不会存在重复写入呢?
刚才我们提到可以为每一个记录设置一个主键Id,即 文件后缀index+文件所在行号 。可以通过主键id的方式保证写入的幂等 。
文件所在的行号,最大值 大致为 10G/1k = 10M,即10000000 。拼接最大的后缀99 。最大的id为990000000 。
所以也无需数据库自增主键ID,可以在批量插入时指定主键ID 。
如果另一个任务也需要导入数据库呢?如何实现主键ID隔离,所以主键ID还是需要拼接taskId 。例如{taskId}{fileIndex}{fileRowNumber} 转化为Long类型 。如果taskId较大,拼接后的数值过大,转化为Long类型可能出错 。
最重要的是,如果有的任务写入1kw , 有的其他任务写入100W,使用Long类型无法获知每个占位符的长度,存在冲突的可能性 。而如果拼接字符串{taskId}_{fileIndex}_{fileRowNumber} ,新增唯一索引 , 会导致插入性能更差,无法满足最快导入数据的诉求 。所以需要想另一个方案 。
可以考虑使用redis记录当前任务的进度 。例如Redis记录task的进度,批量写入数据库成功后,更新 task进度 。
INCRBY KEY_NAME INCR_AMOUNT
指定当前进度增加100 , 例如 incrby task_offset_{taskId} 100 。如果出现批量插入失败的,则重试插入 。多次失败,则单个插入,单个更新redis 。要确保Redis更新成功,可以在Redis更新时 也加上重试 。
如果还不放心Redis进度和数据库更新的一致性,可以考虑 消费 数据库binlog,每一条记录新增则redis +1。
如果任务出现中断,则首先查询任务的offset 。然后读取文件到指定的offset继续 处理 。
九、如何协调读取任务的并发度
前面提到了为了避免单个库插入表的并发度过高 , 影响数据库性能 。可以考虑限制并发度 。如何做到呢?
既然读取任务和写入任务合并一起 。那么就需要同时限制读取任务 。即每次只挑选一批读取写入任务执行 。
在此之前需要设计一下任务表的存储模型 。

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

文章插图