最近业务方有一个需求,需要一次导入超过100万数据到系统数据库。可能大家首先会想,这么大的数据,干嘛通过程序去实现导入,为什么不直接通过SQL导入到数据库。
大数据量报表导出请参考:Java实现大批量数据导入导出(100W以上) -(二)导出
一、为什么一定要在代码实现
说说为什么不能通过SQL直接导入到数据库,而是通过程序实现:
1. 首先,这个导入功能开始提供页面导入,只是开始业务方保证的一次只有<3W的数据导入;
2. 其次,业务方导入的内容需要做校验,比如门店号,商品号等是否系统存在,需要程序校验;
3. 最后,业务方导入的都是编码,数据库中还要存入对应名称,方便后期查询,SQL导入也是无法实现的。
基于以上上三点,就无法直接通过SQL语句导入数据库。那就只能老老实实的想办法通过程序实现。
二、程序实现有以下技术难点
1. 一次读取这么大的数据量,肯定会导致服务器内存溢出;
2. 调用接口保存一次传输数据量太大,网络传输压力会很大;
3. 最终通过SQL一次批量插入,对数据库压力也比较大,如果业务同时操作这个表数据,很容易造成死锁。
三、解决思路
根据列举的技术难点我的解决思路是:
1. 既然一次读取整个导入文件,那就先将文件流上传到服务器磁盘,然后分批从磁盘读取(支持多线程读取),这样就防止内存溢出;
2. 调用插入数据库接口也是根据分批读取的内容进行调用;
3. 分批插入数据到数据库。
四、具体实现代码
1. 流式上传文件到服务器磁盘
略,一般Java上传就可以实现,这里就不贴出。
2. 多线程分批从磁盘读取
批量读取文件:
1 import org.slf4j.Logger; 2 import org.slf4j.LoggerFactory; 3 4 import java.io.File; 5 import java.io.FileNotFoundException; 6 import java.io.RandomAccessFile; 7 import java.nio.ByteBuffer; 8 import java.nio.channels.FileChannel; 9 10 /** 11 * 类功能描述:批量读取文件 12 * 13 * @author WangXueXing create at 19-3-14 下午6:47 14 * @version 1.0.0 15 */ 16 public class BatchReadFile { 17 private final Logger LOGGER = LoggerFactory.getLogger(BatchReadFile.class); 18 /** 19 * 字符集UTF-8 20 */ 21 public static final String CHARSET_UTF8 = "UTF-8"; 22 /** 23 * 字符集GBK 24 */ 25 public static final String CHARSET_GBK = "GBK"; 26 /** 27 * 字符集gb2312 28 */ 29 public static final String CHARSET_GB2312 = "gb2312"; 30 /** 31 * 文件内容分割符-逗号 32 */ 33 public static final String SEPARATOR_COMMA = ","; 34 35 private int bufSize = 1024; 36 // 换行符 37 private byte key = "\n".getBytes()[0]; 38 // 当前行数 39 private long lineNum = 0; 40 // 文件编码,默认为gb2312 41 private String encode = CHARSET_GB2312; 42 // 具体业务逻辑监听器 43 private ReaderFileListener readerListener; 44 45 public void setEncode(String encode) { 46 this.encode = encode; 47 } 48 49 public void setReaderListener(ReaderFileListener readerListener) { 50 this.readerListener = readerListener; 51 } 52 53 /** 54 * 获取准确开始位置 55 * @param file 56 * @param position 57 * @return 58 * @throws Exception 59 */ 60 public long getStartNum(File file, long position) throws Exception { 61 long startNum = position; 62 FileChannel fcin = new RandomAccessFile(file, "r").getChannel(); 63 fcin.position(position); 64 try { 65 int cache = 1024; 66 ByteBuffer rBuffer = ByteBuffer.allocate(cache); 67 // 每次读取的内容 68 byte[] bs = new byte[cache]; 69 // 缓存 70 byte[] tempBs = new byte[0]; 71 while (fcin.read(rBuffer) != -1) { 72 int rSize = rBuffer.position(); 73 rBuffer.rewind(); 74 rBuffer.get(bs); 75 rBuffer.clear(); 76 byte[] newStrByte = bs; 77 // 如果发现有上次未读完的缓存,则将它加到当前读取的内容前面 78 if (null != tempBs) { 79 int tL = tempBs.length; 80 newStrByte = new byte[rSize + tL]; 81 System.arraycopy(tempBs, 0, newStrByte, 0, tL); 82 System.arraycopy(bs, 0, newStrByte, tL, rSize); 83 } 84 // 获取开始位置之后的第一个换行符 85 int endIndex = indexOf(newStrByte, 0); 86 if (endIndex != -1) { 87 return startNum + endIndex; 88 } 89 tempBs = substring(newStrByte, 0, newStrByte.length); 90 startNum += 1024; 91 } 92 } finally { 93 fcin.close(); 94 } 95 return position; 96 } 97 98 /**