文件处理:大文件切片+文件读写锁FileShare
大文件切分:
在项目开发中,我们会遇到单个文件大小超过1TB的文件,这样的文件只能进行单文件读取,往往会造成读取完成耗时过长,导致客户在使用体验过程中不满意。
为了解决提升大文件的解析速度,我想到了先分割大文件为小文件,之后进行并行多个文件同时解析入库方案。
那么,怎么才可以把一个大文件分割为多个小文件呢?
如果我按照大小来控制分割出来的小文件,会造成文件的丢失问题,如果按照行数来分割,一行一行进行读取务必会造成分割文件耗时过长。
如果一个1TB的文件,我们按照大小来控制文件个数,假设每个分割出来的文件大小为200M,这样的话1TB分割出来约5200个文件,这样子的话最多造成约10000行信息被破坏,可以忽略不计。
所以我们为了减少分割文件带来的耗时时间长度,采取分割方案采用定长控制分割出来的文件大小。
实现方案1:一次性读取1M,直到读取到200M为止,开始写入下一个分割文件。
using (FileStream readerStream = new FileStream(file, FileMode.Open, FileAccess.Read)) { // 如果大于1GB using (BinaryReader reader = new BinaryReader(readerStream)) { int fileCursor = 0; int readerCursor = 0; char[] buffer = new char[1024 * 1024]; int length = 0; NextFileBegin: string filePath = string.Format(splitFileFormat, fileCursor); Console.WriteLine("开始读取文件【{1}】:{0}", filePath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); using (FileStream writerStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write)) { using (BinaryWriter writer = new BinaryWriter(writerStream)) { while ((length = reader.Read(buffer, 0, buffer.Length)) > 0) { readerCursor++; writer.Write(buffer, 0, length); if (readerCursor >= splitFileSize) { Console.WriteLine("结束读取文件【{1}】:{0}", filePath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); readerCursor = 0; fileCursor++; goto NextFileBegin; } } } } } }
实现方案2:一次性读取200M,立即写入分割文件,开始下一个分割文件操作。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Configuration; namespace BigFileSplitTest { class Program { static void Main(string[] args) { /* * <!--是否开启大文件分隔策略--> <add key="BigFile.Split" value="true"/> <!--当文件大于这个配置项时就执行文件分隔,单位:GB --> <add key="BigFile.SplitMinFileSize" value="10" /> <!--当执行文件分割时,每个分隔出来的文件大小,单位:MB --> <add key="BigFile.SplitFileSize" value="200"/> * <add key="BigFile.FilePath" value="\\172.x1.xx.xx\文件拷贝\xx\FTP\xx\2016-04-07\x_20160407.txt"/> <add key="BigFile.FileSilitPathFormate" value="\\172.x1.xx.xx\文件拷贝\liulong\FTP\xx\2016-04-07\x_20160407{0}.txt"/> */ string file = ConfigurationManager.AppSettings.Get("BigFile.FilePath"); string splitFileFormat = ConfigurationManager.AppSettings.Get("BigFile.FileSilitPathFormate"); int splitMinFileSize = Convert.ToInt32(ConfigurationManager.AppSettings.Get("BigFile.SplitMinFileSize")) * 1024 * 1024 * 1204; int splitFileSize = Convert.ToInt32(ConfigurationManager.AppSettings.Get("BigFile.SplitFileSize")) * 1024 * 1024; FileInfo fileInfo = new FileInfo(file); if (fileInfo.Length > splitMinFileSize) { Console.WriteLine("判定结果:需要分隔文件!"); } else { Console.WriteLine("判定结果:不需要分隔文件!"); Console.ReadKey(); return; } int steps = (int)(fileInfo.Length / splitFileSize); using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read)) { using (BinaryReader br = new BinaryReader(fs)) { int couter = 1; bool isReadingComplete = false; while (!isReadingComplete) { string filePath = string.Format(splitFileFormat, couter); Console.WriteLine("开始读取文件【{1}】:{0}", filePath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); byte[] input = br.ReadBytes(splitFileSize); using (FileStream writeFs = new FileStream(filePath, FileMode.Create)) { using (BinaryWriter bw = new BinaryWriter(writeFs)) { bw.Write(input); } } isReadingComplete = (input.Length != splitFileSize); if (!isReadingComplete) { couter += 1; } Console.WriteLine("完成读取文件【{1}】:{0}", filePath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); } } } Console.WriteLine("分隔完成,请按下任意键结束操作。。。"); Console.ReadKey(); } } }
从实验结果发现:方案一的性能较方案二的性能约耗时10倍。
文件读写锁:
开发过程中,我们往往需要大量与文件交互,但往往会出现很多令人措手不及的意外,所以对普通的C#文件操作做了一次总结,问题大部分如下:
1:写入一些内容到某个文件中,在另一个进程/线程/后续操作中要读取文件内容的时候报异常,提示 System.IO.IOException: 文件“XXX”正由另一进程使用,因此该进程无法访问此文件。
2:在对一个文件进行一些操作后(读/写),随后想追加依然报System.IO.IOException: 文件“XXX”正由另一进程使用,因此该进程无法访问此文件。次问题与1相似。
3:对一个文件进行一些操作后,想删除文件,依然报System.IO.IOException: 文件“XXX”正由另一进程使用,因此该进程无法访问此文件。
看到这些,有经验的同学应该就会说资源没被释放掉,但也存在如下可能性。我们对文件的操作非常频繁,所以写了特定的操作类/组件来维护文件之间的操作,知道特定的时刻才结束,常见的如日志,随着程序的启动便开始写日志,直到程序关闭。但其中也存在我们需要提供一个特殊的操作(读/写/删除)来操作文件,例如我们需要提供一个日志查看器来查看当前日志或所有日志,这时,便无可避免的发生了以上的问题。
FileMode
MSDN上的解释是指定操作系统打开文件的方式,我想这个应该不需要解释了,大家平时用得比较多了。MSDN的表格也很好的阐述了各个枚举值的作用,我就不在解释了。
FileAccess
定义用于文件读取、写入或读取/写入访问权限的常数。这个枚举用得比较多,描述也很通俗易懂。
FileShare
相信这个枚举类型大家会比较陌生,甚至有同学见都没见过(惭愧的是,我也是才认识它没多久),陌生归陌生,但它的作用却是不可低估,只是微软帮我们把它封装得比较好,以至于我们一度认为它不是什么重要角色。好吧,进入主题!
包含用于控制其他 FileStream 对象对同一文件可以具有的访问类型的常数。这句话是什么意思呢?说实话,我现在看句话还是觉得很纠结,相信很多同学看到也是一头雾水,没关系,先跳过!
看它的成员描述,和FileAccess很是相似,那我们就尝试着来揭开它暂时神秘的面纱。
FileShare.Read
从字面上的意思,我们可以理解为首先打开一个文件之后(资源未释放),我们可以再用只读的方式读取文件从而不会抛出文件无法访问的异常。利用刚才实现的方法,可以轻易地验证我们的猜想:
WriteFile(FileMode.Create, FileAccess.Write, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.Read);
这是什么回事?不是都设置成已读了吗?或许只能在读文件的时候才能设置为只读共享。我们再尝试一下:
ReadFile(FileAccess.Read, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.Read);
这次的确是能在第一次没释放资源时再读,那我们再试试能否在设置只读共享后写文件:
ReadFile(FileAccess.Read, FileShare.Read);
WriteFile(FileMode.Create, FileAccess.Write, FileShare.Read);
首先正确的读出了文件的内容,但当尝试写入一些内容时却又报错了。那么,根据以上的实验,就可以得知只读共享只有在连续读取文件才有效!写入文件后再读取或者读取文件后再写入都会抛异常。
FileShare.Write
结合Read的经验,字面上的意思应该可以理解为,只有在写文件时设置共享方式为Write,随后才能继续写入文件,否则会抛出异常。测试的时候发现当设置共享方式为Write之后,万能的Window记事本也打不开文件了。
FileShare.ReadWrite
有了以上的经验,从字面上理解,可以认为这个ReadWrite一定是结合了Read和Write的特性。那到底它有什么用呢?上面我们知道,在读文件设置Read共享能继续读而不能写,在写文件时设置Write共享则能继续写而不能读,但是当我们设置了写共享后并想读取文件时怎么办?只能先释放资源再重新加载了吗?不需要,ReadWrite就是为此而生的。
WriteFile(FileMode.Create, FileAccess.Write, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.ReadWrite);
注意:写文件的时候并不允许把共享设置成Write,否则读文件时用ReadWrite则无效(报异常),但都设置为ReadWrite可以。
FileShare.None/FileShare.Delete
有了上面的经验,这两个就很容易的就理解了,None则为不允许后续有任何操作,而Delete则是允许随后进行删除操作。