這次,我來談一點高深的話題:結構化存儲(Structured Storage).
也許有人要奇怪我為什麼研究到那個東西去了,不就是個打雜的麼?還要研究這些東西?我先說說來由。
我的網站上的地名信息系統一共包含70萬個文件,這麼多的文件處理起來是很成問題的,例如采用以下方法:
1.不在服務端任何緩存處理,直接在用戶訪問時返回頁面,這樣的話,缺點是反應速度慢,對Web服務器和數據庫服務器產生的壓力挺大,畢竟,與地圖相關的查詢通常是比較耗性能的(據我所知,一般的地圖公司的查詢引擎很少基於SQL)。
2.在服務端保存緩存文件,這樣的話數據庫的壓力小了,可是在頁面上緩存許多文件維護起來是很麻煩的事情(最麻煩的是通過FTP刪除了),而且,在文件夾下放太多的文件本身就會對系統的性能有挺大的影響。
基於以上的情況,我原來的Step1.cn處理方法是只對級別比較高的地名進行緩存,現在網站升級之後我有5G的空間可以用,因此我打算用第二套方法,可是沒有過多久,萬網居然通知我網站的文件數超出限制,我這才知道,在對文件大小的限制居然只有5萬個,如果我改回到Step1.cn的那種模式,這麼大的空間就完全浪費了,因此,我需要一種方法將這些文件合並起來。
在這裡,需要預先說明的是,這樣做應該是會耗損服務器的性能的,我也是抱著一個技術研究的想法去玩這個,目前還不敢部署上去,因為在網站服務器上使用這個可能會比較奇怪,對服務器的性能消耗有多大,我心裡也沒有底。
下面先說說結構化存儲,所謂的結構化存儲就是實現了一個簡單的文件系統的功能,可以向這個文件系統添加刪除文件或目錄,用起來和物理文件系統差別不大,主要區別是結構化存儲最終是將所有的文件存儲到一個大的文件裡面,這樣就可以實現我的需求。
關於結構化存儲,我雖然走通了其使用,可是我不是專家,如果要了解更多信息,建議參考如下文章並和博主聯系:
1.C# 中基於 COM+ 的結構化存儲(JianHua Zhang)
2.結構化存儲C#類庫(BlueDog)
我是直接使用了BlueDog的庫文件,在調用的時候,發現存在一個嚴重BUG,會造成文件句柄不能正常釋放的問題,該問題和解決方案我會在本文最後附上,目前正准備聯系BlueDog修正這個問題。
有了這個庫(需要說明的是,我以前其實打算直接用ZIP格式的,可是後來發現,ZIP文件一旦生成,不能修改,而且我覺得ZIP會附加一個壓縮和解壓過程,應該對性能的影響更大),下面要解決的問題就是網站上如何使用這個庫,當然不能在業務之中使用這個類,因為這樣架構是不合理的,似乎從.NET架構上來講,應該使用VirtualPathProvider Class (System.Web.Hosting)來實現的(例如寫一個類並繼承這個VirtualPathProvider,然後在Web.Config文件之中注冊,之後網站使用到System.IO.File的地方自動由自定義的類來處理,詳細情況可以參考ASP.NET 2.0 裡的VirtualPathProvider)可是我研究了決定不使用,因為相對比較麻煩,不夠靈活,而且我想把GZIP功能直接附加進去。
我是采用普通的類繼承的方式來實現這個功能的由於這個功能剛剛出來,沒有經過完善,因此不提供源碼下載,僅僅貼出部分核心代碼,以供大家參考:
1.基類代碼FileSystem,這個類是一個最基礎的Web文件系統對象,這個對象實現的功能就是將文件直接通過Response輸出,不進行任何保存操作,它的靜態函數Create用來根據當前訪問的文件路徑和系統配置判斷采用哪一個文件系統來處理。
FileSystem.cs
1using System;
2using System.IO;
3using System.Web;
4namespace Step1.WebFileSystem
5{
6 public class FileSystem
7 {
8 protected string path, physicsPath;
9 protected Handler handle;
10 protected HttpContext httpContext = null;
11 protected Stream stream=null;
12 public FileSystem(string path, Handler handle)
13 {
14 httpContext = HttpContext.Current;
15 this.path = path;
16 this.physicsPath = httpContext.Server.MapPath(path);
17 this.handle = handle;
18 }
19 public virtual bool Exists()
20 {
21 return false;
22 }
23 public virtual DateTime GetLastWriteTime()
24 {
25 return DateTime.Now;
26 }
27 public virtual Stream GetWriter()
28 {
29 stream = httpContext.Response.OutputStream;
30 if (this.handle.UseGzip)
31 {
32 return (Stream)(new GZipOutputStream(stream));
33 }
34 else
35 {
36 return stream;
37 }
38 }
39 public virtual Stream GetReader()
40 {
41 return null;
42 }
43 public virtual void TransmitFile()
44 {
45 return;
46 }
47 public virtual void Close()
48 {
49 if (stream != null)
50 {
51 stream.Close();
52 stream=null;
53 }
54 }
55 public virtual void Dispose()
56 {
57 Close();
58 }
59 public void TransmitStream(bool unZip)
60 {
61 Stream reader = unZip ? new GZipInputStream(this.GetReader()) : this.GetReader();
62 byte[] buffer = new byte[1024];
63 int p;
64 while ((p = reader.Read(buffer, 0, 1024)) > 0)
65 {
66 httpContext.Response.OutputStream.Write(buffer, 0, p);
67 httpContext.Response.Flush();
68 }
69 this.Close();
70 }
71 public void ResponseFile()
72 {
73 HttpRequest request = httpContext.Request;
74 string acceptEncoding = request.Headers["Accept-Encoding"];
75 if (this.handle.UseGzip && acceptEncoding != null && acceptEncoding.IndexOf("gzip") >= 0)
76 {//返回gzip格式
77 httpContext.Response.AddHeader("Content-Encoding", "gzip");
78 this.TransmitFile();
79 }
80 else
81 {//返回明文格式
82 if (this.handle.UseGzip)
83 {
84 this.TransmitStream(true);
85 }
86 else
87 {
88 this.TransmitFile();
89 }
90 }
91 }
92 protected void CreateFolder(string path, bool createCurrent)
93 {
94 string folder = Path.GetDirectoryName(path);
95 if (!Directory.Exists(folder))
96 {
97 CreateFolder(folder, true);
98 }
99 if (createCurrent)
100 {
101 Directory.CreateDirectory(path);
102 }
103 }
104 public static FileSystem Create(string path)
105 {
106 Configuration config = Configuration.Instance();
107 if (config != null)
108 {
109 for(int i = 0; i < config.handlers.Length; i++)
110 {
111 if (config.handlers[i].UrlRegex.IsMatch(path))
112 {
113 switch(config.handlers[i].HandlerType)
114 {
115 case "FFS":
116 return new FileSystemFFS(path, config.handlers[i]);
117 case "OS":
118 return new FileSystemOS(path, config.handlers[i]);
119 default:
120 return new FileSystem(path, config.handlers[i]);
121 }
122 }
123 }
124
125 }
126 return new FileSystem(path,new Handler());
127 }
128 }
129}
130
下面就是結構化存儲的實現FileSystemFFS
FileSystemFFS.cs
1using System;
2using System.IO;
3using System.Web;
4using ExpertLib;
5using ExpertLib.IO;
6using ExpertLib.IO.Storage;
7using System.Text.RegularExpressions;
8
9namespace Step1.WebFileSystem
10{
11 public class FileSystemFFS:FileSystem
12 {
13 private Storage ffsFile=null;
14 private string filePath, ffsPath;
15 private bool writeError = false;
16 public FileSystemFFS(string path, Handler handle)
17 : base(path, handle)
18 {
19 this.ffsPath = httpContext.Server.MapPath(Regex.Replace(path,handle.Pattern,handle.SavePath));
20 this.filePath = Regex.Replace(path, handle.Pattern, handle.SaveName).Replace('/', '_');
21 }
22 private void Open()
23 {
24 ffsFile = StorageFile.OpenStorageFile(this.ffsPath);
25 }
26 public override bool Exists()
27 {
28 if (ffsFile == null)
29 {
30 FileInfo fileInfo = new FileInfo(this.ffsPath);
31 if (!fileInfo.Exists) { return false; }
32 Open();
33 }
34 return ffsFile.IsElementExist(this.filePath);
35 }
36 public override DateTime GetLastWriteTime()
37 {
38 if (ffsFile == null)
39 {
40 FileInfo fileInfo = new FileInfo(this.ffsPath);
41 if (!fileInfo.Exists) { return DateTime.Now; }
42 Open();
43 }
44 List<StgElementInfo> elementsInfo = ffsFile.GetChildElementsInfo();
45 for (int i = elementsInfo.Count - 1; i <= 0; i--)
46 {
47 if (elementsInfo[i].Name == filePath)
48 {
49 return elementsInfo[i].LastModifyTime;
50 }
51 }
52 return DateTime.Now;
53 }
54 public override Stream GetWriter()
55 {
56 Close();
57 if (ffsFile == null)
58 {
59 if (!File.Exists(this.ffsPath))
60 {
61 CreateFolder(this.ffsPath,false);
62 ffsFile = StorageFile.CreateStorageFile(this.ffsPath,StorageCreateMode.Create,StorageReadWriteMode.ReadWrite,StorageShareMode.ShareDenyWrite,StorageTransactedMode.Direct);
63 }
64 else
65 {
66 Open();
67 }
68 }
69 stream = (Stream)ffsFile.CreateStream(filePath);
70 if (stream == null)
71 {
72 writeError = true;
73 return base.GetWriter();
74 }
75 if (this.handle.UseGzip)
76 {
77 return (Stream)(new GZipOutputStream(stream));
78 }
79 else
80 {
81 return stream;
82 }
83 }
84 public override Stream GetReader()
85 {
86 Close();
87 if (ffsFile == null)
88 {
89 FileInfo fileInfo = new FileInfo(this.ffsPath);
90 if (!fileInfo.Exists) { return null; }
91 Open();
92 }
93 return stream = (Stream)ffsFile.OpenStream(filePath);
94 }
95 public override void TransmitFile()
96 {
97 if (writeError)
98 {
99 base.TransmitFile();
100 }
101 else
102 {
103 this.TransmitStream(false);
104 }
105 }
106 public override void Dispose()
107 {
108 Close();
109 if (ffsFile != null)
110 {
111 ffsFile.Dispose();
112 ffsFile = null;
113 }
114 }
115 }
116}
117
還有一個FileSystemOS,明顯是這直接使用系統文件存儲,我就不列出代碼了,下面看看我在頁面上如何調用吧(頁面上並不關心采用哪種文件系統):
Page.cs
1 DateTime lastWriteTime=DateTime.Now;
2 //檢查是否存在
3 bool isExists=wfs.Exists() && DateTime.Now.Subtract(lastWriteTime=wfs.GetLastWriteTime()).TotalDays <= fileKeepDays;
4 lastWriteTime = isExists ? lastWriteTime : DateTime.Now;
5 //設置緩存
6 Response.Cache.SetExpires(lastWriteTime.AddDays(fileKeepDays));
7 Response.Cache.SetLastModified(lastWriteTime.AddMinutes(-1));
8 Response.ContentType = "text/html";
9 if (!isExists)
10 {
11 try
12 {
13 writer = wfs.GetWriter();
14 //在這裡將頁面內容填寫進去,我一般都是用XSLT來輸出頁面
15 }
16 finally
17 {
18 if (writer != null)
19 {
20 writer.Close();
21 }
22 wfs.Dispose();
23 }
24 }
25 wfs.ResponseFile();
26 wfs.Dispose();
有必要的話再耐心看看我在Web.Config裡面是怎樣配置使用WFS的:
Web.Config
1 <Handler pattern="^/place/cn/((?:[^$/]+/){3})([^$]+).aspx" handlerType="FFS" useGzip="true" savePath="/place/cn/$1FFS.resx" saveName="$2"/>
2 <Handler pattern="^/place/cn/([^$]+).aspx" handlerType="OS" useGzip="true" savePath="/place/cn/$1.aspx"/>
以上的配置是為我的地名信息系統設計的,大體意思是:對於前三級行政區劃(省、市、縣),因為訪問次數比較多,考慮到性能,將緩存直接輸出到系統對應的文件,對於後面的所有行政區劃按所在縣的名稱分文件按照結構化存儲。
最後,說說BlueDog的C#類庫的BUG吧,其實說起來很簡單,他可能沒有注意到EnumElements方法產生獲取的ComTypes.STATSTG對象也必須由COM來銷毀,否則,對應的文件操作句柄就沒有被釋放,下次打開文件就會出錯(這個問題折磨了我好久!),受到影響的至少有以下兩個方法(其他方法因為我沒有使用,因此,不知道還有沒有),這兩個方法應該更改為如下:
Strorage.cs Bug Fix
1 public bool IsElementExist(string elementName)
2 {
3 ArgumentValidation.CheckForEmptyString(elementName, "elementName");
4
5 IEnumSTATSTG statstg;
6 ComTypes.STATSTG stat;
7 uint k;
8 this.storage.EnumElements(0, IntPtr.Zero, 0, out statstg);
9 statstg.Reset();
10 bool found = false;
11 while (statstg.Next(1, out stat, out k) == HRESULT.S_OK)
12 {
13 //忽略大小寫比較
14 if (string.Compare(stat.pwcsName, elementName, true) == 0)
15 {
16 found = true;
17 break;
18 }
19 }
20 Marshal.ReleaseComObject(statstg);//釋放statstg
21 return found;
22 }
23 public List<StgElementInfo> GetChildElementsInfo()
24 {
25 IEnumSTATSTG statstg;
26 ComTypes.STATSTG stat;
27 uint k;
28 List<StgElementInfo> list = new List<StgElementInfo>();
29 this.storage.EnumElements(0, IntPtr.Zero, 0,out statstg);
30 statstg.Reset();
31 while (statstg.Next(1, out stat, out k) == HRESULT.S_OK)
32 {
33 list.Add(new StgElementInfo(stat));
34 }
35 Marshal.ReleaseComObject(statstg);//釋放statstg
36 return list;
37 }