这次,我来谈一点高深的话题:结构化存储(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 }