批量导入大数据以及数据去重,CSV多线程导入100w数据

mysql层面去重:https://www.cnblogs.com/duanxiaojun/p/6855680.html
数据库层面具体使用哪个sql语句去重,根据业务情况来定。

数据库连接池默认开启连接50,最大100

由于mybatis有一次sql的大小限制或者数据库也有大小限制,因此可以将其分为多个list集合,使用ExcutorService、callable、futuretask、countdownlatch(用于计算分段list集合个数),多线程并发插入数据。程序方面对Excel中的数据去重:将数据封装为对象,对象重写equals方法和hashCode方法,这里equals就只对那2个字段进行比较即可,并将对象放入set中去重。数据库方面去重:利用数据库设置联合唯一索引。然后通过insert ignore into语句去执行。insert ignore into:重复或语句错误报错都会被忽略(根据主键和唯一索引判断重复)

程序方面的去重与上一点大致相同。区别是equals和hashCode需要判断全部的属性字段。

以上也可以考虑用redis的zset去重,但是会增加网络延时问题,以及每次都要以网络形式分批去读取redis中的数据,并且反序列化,会增加一定的网络不及时响应等问题。如果程序没有考虑从缓存中读取数据,使用redis去重存储数据,是得不偿失的。如果本身系统查数据都是从redis中获取,那么使用redis的zset存储数据库的数据是可以的。

引入一个问题,如何知道redis上的哪些数据是没有被持久化到数据库中的呢?

经济允许,可以创建两个zset集合,一个zset集合(A)是缓存全部数据,一个zset集合(B)存需要插入到数据库的全部数据。那么多个用户并行上传数据后,将这些数据都存入A和B中,由于前端是从缓存A中获取数据,所以很快就能响应,然后后台异步操作对B中的数据多线程的存入数据库中,将持久化到数据库中的数据再从B中删除即可。这里就需要数据库层面sql再次对insert的数据进行重复控制,将插入到数据库与已存在数据库中的数据进行重复控制。(zset 是根据score参数来判定排序顺序,且存入的数据是不重复的,因此可以根据业务来确定score值,如果是根据创建时间排序,socre就可以存入创建时间字段的时间戳,zrange 命令从小到大排序,zrevrange 命令从大到小排序)

如果批量导入100w+的数据,存在的技术难点:
1)一次读取加载到内存会OOM;
2)调用接口保存一次传输数据量大,网络传输压力大;
3)一句SQL批量插入,对数据库压力大,如果同时操作该表,会造成死锁情况;
4)使用excel会造成大量的繁琐操作,由于数据不可磁盘分区操作,一次性读入会导致1)问题。

解决:
1)将文件以流的形式存入磁盘中,然后通过多线程去分区读取(使用FileChannel、RandomAccessFile实现);
2)根据分区读取的内容数量进行数据库连接的调用,因此不会造成网络传输压力大的问题;
3)分批插入数据到数据库,就不会造成数据库压力大;
4)提示用户将excel保存为csv格式,使用csv的优点在于,一条数据就是一行有利于多线程分区读取文件,不会造成一条数据被分成两部分。

用到文件内存映射。
需要注意的是,如果数据量小的话,不适宜这样做,应为初始化MappedByteBuffer会比较耗时间,因此可以根据文件大小,来判断使用哪种导入方式。

<?xml version="1.0" encoding="UTF-8"?>


4.0.0

<groupId>com.turbo</groupId>
<artifactId>BatchBigFile</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
    </dependency>

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>

</dependencies>

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Person {
private String name;
private String age;
private String gender;
}

/**

  • 业务处理接口
    /
    public interface IHandle {
    /
    *

    • 用于处理业务
    • @param line 每行的数据
      */
      void handle(String line);

    /**

    • 用于业务处理
    • @param personSet 集合
      */
      void handleSet(Set personSet);
      }

package com.turbo;

import lombok.*;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.StringUtils;

import java.io.;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.
;
import java.util.concurrent.atomic.AtomicLong;

public class BigFileReader {
private final byte newLineByte = ‘\n’;
private final byte enterByte = ‘\r’;

private final int threadSize;
private final long fileLength;
private final int bufferSize;
private final String charSet;
private final IHandle ihandle;
private RandomAccessFile rAccessFile;
private final ExecutorService executorService;
private final Set<StartEndPair> startEndPairs;

/** 性能测试 */
private CyclicBarrier cyclicBarrier;
private final AtomicLong countLine = new AtomicLong(0L);

public BigFileReader(File file, Integer bufferSize, String charSet, Integer threadSize, IHandle handle) {
    this.fileLength = file.length();
    this.bufferSize = bufferSize;
    this.charSet = charSet;
    this.threadSize = threadSize;
    this.ihandle = handle;
    try {
        this.rAccessFile = new RandomAccessFile(file, "r");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    this.executorService = new ThreadPoolExecutor(threadSize,
                                                  threadSize,
                                                  0,
                                                  TimeUnit.SECONDS,
                                                  new LinkedBlockingQueue<>(threadSize),
                                                  Executors.defaultThreadFactory(),
                                                  new ThreadPoolExecutor.AbortPolicy());
    this.startEndPairs = new HashSet<>();
}

public void start(){
    long perSize = this.fileLength / threadSize;
    try {
        calculateStartEnd(0, perSize);
    } catch (IOException e) {
        e.printStackTrace();
    }

    // 性能测试开始 */
    final long startTime = System.currentTimeMillis();
    cyclicBarrier = new CyclicBarrier(startEndPairs.size(), () -> {
        System.out.println("use time: " + (System.currentTimeMillis() - startTime));
        System.out.println("all line: " + countLine.get());
    });
    // 性能测试结束 */

    for (StartEndPair pair : startEndPairs) {
        executorService.execute(new SliceReaderTask(pair));
    }
}

/**
 * 计算分区读取的起始位置与结束位置
 * 保证每个分区的结束位置都是换行或回车的位置,以避免一条数据分成两个部分到不同的线程中
 * @author zwx
 * @param start 起始位置
 * @param perSize 平分处理数据的大小
 * @throws IOException io异常
 */
private void calculateStartEnd(long start, long perSize) throws IOException {
    if (start > fileLength-1){
        return;
    }
    StartEndPair pair = new StartEndPair();
    pair.start = start;
    long endPosition = start + perSize - 1;
    if (endPosition >= fileLength){
        pair.end = fileLength - 1;
        startEndPairs.add(pair);
        return;
    }
    rAccessFile.seek(endPosition);
    byte tmp = (byte)rAccessFile.read();
    while (tmp != newLineByte && tmp != enterByte){
        endPosition++;
        if (endPosition >= fileLength - 1){
            endPosition = fileLength - 1;
            break;
        }
        rAccessFile.seek(endPosition);
        tmp = (byte)rAccessFile.read();
    }
    pair.end = endPosition;
    startEndPairs.add(pair);

    calculateStartEnd(endPosition + 1, perSize);
}

/**
 * 线程处理过程
 * @author zwx
 */
private class SliceReaderTask implements Runnable {
    private final long start;
    private final long sliceSize;
    private final byte[] readBuff;
    // private Set<Person> personSet;

    public SliceReaderTask(StartEndPair pair){
        this.start = pair.start;
        this.sliceSize = pair.end - pair.start + 1;
        this.readBuff = new byte[bufferSize];
    }

    @Override
    public void run() {
        try {
            MappedByteBuffer mapBuffer = rAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, start, sliceSize);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            for (int offset = 0; offset < sliceSize; offset += bufferSize) {
                int readLength;
                if (offset + bufferSize <= sliceSize){
                    readLength = bufferSize;
                }else {
                    readLength = (int)sliceSize - offset;
                }
                // 将内存映射中的数据读到readBuff中,从readBuff索引0开始存储到索引readLength
                mapBuffer.get(readBuff, 0, readLength);
                // 遍历readBuff 将每行的数据写入到bos中,然后传给IHandle处理
                for (int i = 0; i < readLength; i++) {
                    byte tmp = readBuff[i];
                    if (tmp == newLineByte || tmp == enterByte){
                        handle(bos.toByteArray());
                        bos.reset();
                    }else {
                        bos.write(tmp);
                    }
                }
            }
            if (bos.size() > 0){
                handle(bos.toByteArray());
            }
            // 性能测试用,记录完成线程数 */
            cyclicBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 如果要批量导入到数据库,起始这里的方法可以改成将每一行的数据都封装到对应的集合中
     * 如:person实体,这里可以将每行数据封装到person实体中,然后add到personSet集合中
     * 封装完成之后再调用IHandle,函数式接口去insert数据
     * @author zwx
     * @param bytes 每行数据 字节
     * @throws UnsupportedEncodingException 编码异常
     */
    private void handle(byte[] bytes) throws UnsupportedEncodingException {
        String line;
        if (charSet == null){
            line = new String(bytes);
        }else {
            line = new String(bytes, charSet);
        }
        if (StringUtils.isNotBlank(line)){
            ihandle.handle(line);
            // 记录行数 */
            countLine.incrementAndGet();
        }
    }
}

/**
 * 该类用于存储每个分区的开始位置与结束位置
 * @author zwx
 */
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
private static class StartEndPair{
    private Long start;
    private Long end;

    @Override
    public int hashCode() {
        final int prime = 11;
        int result = 30;
        result = prime * result + ((start == null) ? 0 : start.hashCode());
        result = prime * result + ((end == null) ? 0 : end.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (obj instanceof StartEndPair){
            StartEndPair objSe = (StartEndPair) obj;
            return objSe.start.equals(this.start) && objSe.end.equals(this.end);
        }
        return false;
    }
}

/**
 * 构建一个bigFileReader
 * @author zwx
 */
@Data
@NoArgsConstructor
@Accessors(chain = true)
public static class Builder{
    private Integer bufferSize = 1024*1024;
    private String charSet;
    private Integer threadSize;
    private IHandle handle;
    private File file;

    public Builder(String filePath, IHandle handle){
        this.file = new File(filePath);
        if (!this.file.exists()){
            throw new IllegalArgumentException("文件不存在");
        }
        this.handle = handle;
    }

    public BigFileReader build(){
        return new BigFileReader(this.file, this.bufferSize, this.charSet, this.threadSize, this.handle);
    }
}

}

import java.util.Set;

/**

  • 实现处理大文件

  • 通过内存映射,多个线程分区读取文件内容,且每个线程

  • 读取的分区结束都以换行符或者回车符结束。

  • 特适合处理csv文件
    */
    public class Main {

    public static void main(String[] args) {
    BigFileReader.Builder builder = new BigFileReader.Builder(“D:/test.csv”, new IHandle() {

         @Override
         public void handle(String line) {
             String enComma = ",";
             String[] split = line.split(enComma);
             System.out.println(new Person().setName(split[0]).setAge(split[1]).setGender(split[2]));
         }
    
         @Override
         public void handleSet(Set<Person> personSet) {
             // 对其导入数据库操作 或者 其他操作
         }
     });
     // 线程数、缓存大小 根据实际业务情况来定
     BigFileReader bigFileReader = builder.setThreadSize(6).setBufferSize(100).setCharSet("utf-8").build();
     bigFileReader.start();
    

    }

}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值