【背景】:
随机和顺序读写,是存储器的两种输入输出方式。存储的数据在磁盘中占据空间,对于一个新磁盘,操作系统会将数据文件依次写入磁盘,当有些数据被删除时,就会空出该数据原来占有的存储空间,时间长了,不断的写入、删除数据,就会产生很多零零散散的存储空间,就会造成一个较大的数据文件放在许多不连续的存贮空间上,读写些这部分数据时,就是随机读写,磁头要不断的调整磁道的位置,以在不同位置上的读写数据,相对于连续空间上的顺序读写,要耗时很多。在开机时、启动大型程序时,电脑要读取大量小文件,而这些文件也不是连续存放的,也属于随机读取的范围。
改善方法:做磁盘碎片整理,合并碎片文件,但随后还会再产生碎片造成磁盘读写性能下降,而且也解决不了小文件的随机存取的问题,这只是治标。更好的解决办法:更换电子硬盘(SSD),电子盘由于免除了机械硬盘的磁头运动,对于随机数据的读写极大的提高。
举个例子,SSD的随机读取延迟只有零点几毫秒,而7200RPM的随机读取延迟有7毫秒左右,5400RPM硬盘更是高达9毫秒之多,体现在性能上就是开关机速度。
我们常用中间件kafka就是采用的顺序读写,避免了磁盘寻址的过程。
顺序IO和随机IO
- 连续 / 随机 I/O
连续 I/O :指的是本次 I/O 给出的初始扇区地址和上一次 I/O 的结束扇区地址是完全连续或者相隔不多的。反之,如果相差很大,则算作一次随机 I/O。
而发生随机I/O可能是因为磁盘碎片导致磁盘空间不连续,或者当前block空间小于文件大小导致的。
连续 I/O 比随机 I/O 效率高的原因是:在做连续 I/O 的时候,磁头几乎不用换道,或者换道的时间很短;而对于随机 I/O,如果这个 I/O 很多的话,会导致磁头不停地换道,造成效率的极大降低。
对于磁盘的读写分为两种模式,顺序IO和随机IO。 随机IO存在一个寻址的过程,所以效率比较低。而顺序IO,相当于有一个物理索引,在读取的时候不需要寻找地址,效率很高。
- Java中的随机读写
在Java中读写文件的方式有很多种,先总结以下3种方法:
比如:
public static void fileWrite(String filePath, String content) {
FileOutputStream outputStream = null;
try {
File file = new File(filePath);
boolean isCreate = file.createNewFile();//创建文件
if (isCreate) {
outputStream = new FileOutputStream(file);//形参里面可追加true参数,表示在原有文件末尾追加信息
outputStream.write(content.getBytes());
}else {
outputStream = new FileOutputStream(file,true);//表示在原有文件末尾追加信息
outputStream.write(content.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void fileRead(String filePath) {
File file = new File(filePath);
if (file.exists()) {
try {
//创建FileInputStream对象,读取文件内容
FileInputStream fis = new FileInputStream(file);
byte[] bys = new byte[1024];
while (fis.read(bys, 0, bys.length) != -1) {
//将字节数组转换为字符串
System.out.print(new String(bys, StandardCharsets.UTF_8));
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
Java中的顺序读写
上面的对文件的读写都是随机读写,如果用来写比较小的日志文件还能满足要求,如果用来操作一个文件的读写,那可能带来很大的性能消耗。
顺序IO的读写在中间件使用的很频繁,尤其是在队列中。几乎所有的队列(kafka,qmq等使用文件存储消息)都采用了顺序IO读写。
与随机读写不同的是,顺序读写是优先分配一块文件空间,然后后续内容追加到对应空间内。
在使用顺序IO进行文件读写时候,需要知道上次写入的地方,所以需要维护一个索引或者轮询获得一个没有写入位置。
/**
*
* @param filePath
* @param content
* @param index 从指定位置开始写入
* @return 返回当前文件末尾的位置,便于下次继续写入
*
* MappedByteBuffer,可以让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次。而如何同步到文件由NIO来完成
*
* * 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式
* * 参数2: 0 : 可以直接修改的起始位置
* * 参数3: 5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存
* * 可以直接修改的范围就是 0-5
* * 实际类型 DirectByteBuffer
* * MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
*/
public static long fileWrite(String filePath, String content, int index) {
File file = new File(filePath);
RandomAccessFile randomAccessTargetFile;
MappedByteBuffer map;
try {
randomAccessTargetFile = new RandomAccessFile(file, "rw");
FileChannel targetFileChannel = randomAccessTargetFile.getChannel();
map = targetFileChannel.map(FileChannel.MapMode.READ_WRITE, 0, (long) 1024 * 1024); // 预先分配空间
map.position(index);
map.put(content.getBytes());
return map.position();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
return 0L;
}
public static String fileRead(String filePath, long index) {
File file = new File(filePath);
RandomAccessFile randomAccessTargetFile;
MappedByteBuffer map;
try {
randomAccessTargetFile = new RandomAccessFile(file, "rw");
FileChannel targetFileChannel = randomAccessTargetFile.getChannel();
map = targetFileChannel.map(FileChannel.MapMode.READ_WRITE, 0, index);
byte[] byteArr = new byte[10 * 1024];
map.get(byteArr, 0, (int) index);
return new String(byteArr);
} catch (IOException e) {
e.printStackTrace();
} finally {
}
return "";
}
public static void main(String[] args) {
System.out.println(ShunXuFile.fileWrite("D:/FileShunxuTest2","1111111111111111111",0));
}