文件處理:大文件切片+文件讀寫鎖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則是允許隨後進行刪除操作。
文件處理:大文件切片+文件讀寫鎖FileShare