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会比较耗时间,因此可以根据文件大小,来判断使用哪种导入方式。
4.0.0
com.turbo
BatchBigFile
1.0-SNAPSHOT
8
8
org.projectlombok
lombok
1.18.16
org.apache.commons
commons-lang3
3.12.0
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Person {
private String name;
private String age;
private String gender;
}
/**
业务处理接口
/
public interface IHandle {
/*
/**
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 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 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 personSet) {
// 对其导入数据库操作 或者 其他操作
}
});
// 线程数、缓存大小 根据实际业务情况来定
BigFileReader bigFileReader = builder.setThreadSize(6).setBufferSize(100).setCharSet("utf-8").build();
bigFileReader.start();
}
}