之前,不怕“重复发明轮子”的我,搞了一个“PDF.NET框架”,即“PWMIS数据开发框架”(目前已经开源),自己用特殊的方式设计了一个实体类基类,然后又设计了操作实体类的语法--“OQL表达式”,一套类似SQL的对象化的操作实体类的语法,接着又实现了实体类的“二进制序列化”,最近突发奇想,何不将这个系列化后的实体类,搞成一个数据库?重新走DBMS的老路显然没有竞争力,目前NoSql正流行,那我就搞个内存数据库吧!
其实,说到做“内存数据库”,概念大了些,我个人能力有限,要做也只能做个“概念整合”,初步想法是,数据全部以“对象”的形式存在内存中,用Linq To Object的方式,来操作这些“数据”,将数据保存到一个持久化媒体中,比如磁盘文件中,开一个后台线程慢慢去写,而前台的数据使用是可以经受主大量并发操作的。想法有了,立刻开工!
1,数据的持久化
首先,封装一下实体类的持久化过程,将实体类序列化后保存在磁盘文件,或者从一个磁盘文件加载实体类,直接上代码:
2 /// 从数据文件载入实体数据(不会影响内存数据),建议使用Get的泛型方法
3 /// </summary>
4 /// <typeparam name="T"></typeparam>
5 /// <returns></returns>
6 public T[] LoadEntity < T > () where T : EntityBase, new ()
7 {
8 Type t = typeof (T);
9 string fileName = this .FilePath + " \\ " + t.FullName + " .pmdb " ;
10 if (File.Exists(fileName))
11 {
12 byte [] buffer = null ;
13 using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
14 {
15 long length = fs.Length;
16 buffer = new byte [length];
17 fs.Read(buffer, 0 , ( int )length);
18 fs.Close();
19 }
20 T[] result = PdfNetSerialize < T > .BinaryDeserializeArray(buffer);
21
22 this .WriteLog( " 加载数据 " + fileName + " 成功! " );
23 return result;
24 }
25 return null ;
26 }
27
28 /// <summary>
29 /// 直接保存实体数据,如果文件已经存在则覆盖(不会影响内存数据)
30 /// </summary>
31 /// <typeparam name="T"></typeparam>
32 /// <param name="entitys"></param>
33 /// <returns></returns>
34 public bool SaveEntity < T > (T[] entitys) where T : EntityBase, new ()
35 {
36 if (entitys != null && entitys.Count() > 0 )
37 {
38 Type t = typeof (T);
39 string fileName = this .FilePath + " \\ " + t.FullName + " .pmdb " ;
40 byte [] buffer = PdfNetSerialize < T > .BinarySerialize(entitys);
41 using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write))
42 {
43 fs.Write(buffer, 0 , buffer.Length);
44 fs.Flush();
45 fs.Close();
46 }
47 this .WriteLog( " 保存数据 " + fileName + " 成功! " );
48 return true ;
49 }
50 return false ;
51 }
这里,实体类的序列化都依赖于PDF.NET框架已有的
PdfNetSerialize < T > .BinaryDeserializeArray( byte [] buffer); // 二进制反序列化
这两个方法,根据具体的类型T 获取文件名,其它就没有什么好说的。
2,构造“数据仓库”
既然是“数据库”,肯定要有一个地方来集中存放,那内存数据库自然是把所有数据放到内存中,于是定义一个“数据容器”对象:
由于容器中要存放各种具体的实体类对象,所以我使用实体类的基类 EntityBase 来定义,数据容器 dataContainer中存放的是具体实体类对象的数组,于是统一保存数据就是下面类似的代码:
2 {
3 foreach (EntityBase[] item in dataContainer)
4 {
5 this .SaveEntity < EntityBase > (item);
6 }
7 }
非常不幸,我调用的 SaveEntity 方法无法编译通过,VS给出的错误提示
“必须是具有公共的无参数构造函数的非抽象类型,才能用作泛型类型或方法”SaveEntity>(T[] entitys)中的参数“T”,
于是改一下保存数据的方法,去掉new() 泛型约束:
但序列化实体类的方法无法编译通过:
byte[] buffer = PdfNetSerialize<T>.BinarySerialize(entitys);
BinarySerialize 方法也要求泛型类类型<T>不能是抽象类或接口类型!
接着去修改序列化方法?不太可能,因为PDF.NET的类库已经很成熟了,难以评估此修改会对原有的项目产生什么影响。
本着“对修改关闭,对扩展开放”的原则,只有另辟蹊径,不走寻常路了。
3,移花接木
我们再来看看 SaveAllEntitys 方法,如果我们能够在调用 SaveEntity 之前,拿到EntityBase类的具体实现类型,那该多好啊!这样就解决了泛型类不能使用抽象类类型的问题,但这里怎么可能拿得到呢?虽然我们在运行时,我们能够确切的看到 item 变量对应的对象的具体类型,但我们的代码在这里却没法给泛型方法的类型<T>一个交代,这可怎么办呢?
这个问题不突破,后面的工作都没法进行,足足让我思考了好几个小时。
“运行时才知道具体类型...”
“运行时...运行时...”
突然,灵光一现,何不在“运行时记录方法实际调用的具体类型”?也就是“捕获调用的方法”,而不是获取“方法的执行结果”。举个简单例子:
' 找金山的具体过程
End Function
Function 我要金山2()
' XXX想要金山!记录下来他怎么找到金山的
End Function
“我要金山2”跟“我要金山1”的区别就是,前者是要找金山的方法,而后者目的只是要金山!正所谓“授人与鱼不如授人与渔”!
在.NET中,如何才能捕获“方法的调用”而不是获取“方法的执行结果”?或者说,如何才能先将方法的调用记录下来,以后在某个时候再来执行?就像上面的例子“我要金山2”,外人看起来他好像是要了一座金山,其实他背后的“野心大大的”,要拥有更多的金山,这对外人而言他简直就是在“移花接木”!
闲话少说,还是请我们今天的主角出场:
“隆重欢迎《委托》先生出场!”
看看我们的“《委托》先生”是怎么表演的:
2
3 /// <summary>
4 /// (延迟)保存数据,该方法会触发数据真正保存到磁盘,请添加、修改数据后调用该方法
5 /// </summary>
6 /// <typeparam name="T"></typeparam>
7 public void Save < T > () where T : EntityBase, new ()
8 {
9 AddSaveMethod(() =>
10 {
11 Type t = typeof (T);
12 string key = t.FullName;
13 if (mem_data.ContainsKey(key))
14 {
15 T[] entitys = (T[])mem_data[key];
16 // 此处将触发key 对应的数据的保存动作
17 lock (lock_obj)
18 {
19 return SaveEntity < T > (entitys);
20 }
21 }
22 return false ;
23 }
24 );
25
26 }
上面的代码定义了一个Func<bool> “委托方法”的列表对象methodList,以保存所有“需要调用的方法”,使得Save<T>() 方法的实际操作不是去保存数据,而是保存了“保存数据的方法”,将该方法作为 AddSaveMethod 方法的参数,以达到“移花接木”的效果:
2 {
3 if ( ! methodList.Contains(toDo))
4 methodList.Add(toDo);
5 }
最后,我们只需要在某个时候,开个后台线程,来真正执行这些“数据保持的方法”即可,下面是保存数据到磁盘的代码:
2 /// 将数据真正保持到磁盘
3 /// </summary>
4 protected internal void Flush()
5 {
6 foreach (var item in methodList.ToArray())
7 {
8 item();
9 methodList.Remove(item);
10 }
11 }
注意每次我们执行保存数据的方法后,都要从methodList 清除它,等待下一次某个工作线程再次触发保存数据的动作。
到此,我们保存各种类型的“实体数据”工作圆满完成了,但怎么用好它,还得看“婆家”的脸色。
4,打造“数据集市”
前面的工作完成了如何加载数据,如何保存数据的问题,但这些工作要做好,还得先找一个“容器”来存储所有的数据,直接放到内存是最简单的想法,但我们不能让这个内存数据库闲得没事也占据大量的内存,就像我们要开好自己的“个体服装店”,必须找个合适的“服装市场”,否则生意清淡门面冷清,所以我们必须为我们的内存数据库找个“数据集市”。
什么地方的内存能够按需使用,闲置后可以回收?这不就是“缓存”吗?!
.NET 4.0提供了 System.Runtime.Caching 命名空间,下面有一些缓存管理的类,它们不依赖于System.Web.dll 程序集,可以在各种类型的应用程序中使用,就选它了:
2 /// 内存数据库引擎,bluedoctor 2011.9.5 详细请看 http://www.pwmis.com/sqlmap
3 /// </summary>
4 public class MemDBEngin
5 {
6 /// <summary>
7 /// 获取引擎实例,实例保存在系统缓存工厂中
8 /// </summary>
9 /// <param name="source"> 要持久化的对象数据保存的路径 </param>
10 /// <returns></returns>
11 public static MemDB GetDB( string source)
12 {
13 MemDB result = CacheProviderFactory.GetCacheProvider().Get < MemDB > (source, () =>
14 {
15 MemDB db = new MemDB(source);
16 db.AutoSaveData();
17 return db;
18 },
19 new System.Runtime.Caching.CacheItemPolicy()
20 {
21 SlidingExpiration = new TimeSpan( 0 , 10 , 0 ), // 距离上次调用10分钟后过期
22 RemovedCallback = args => {
23 MemDB db = (MemDB)args.CacheItem.Value;
24 db.Flush();
25 db.Close();
26 }
27 }
28 );
29
30 return result;
31
32 }
33
34 private static string defaultDbSource = "" ;
35
36 /// <summary>
37 /// 获取默认的内存数据库引擎
38 /// </summary>
39 /// <returns></returns>
40 public static MemDB GetDB()
41 {
42 if (defaultDbSource.Length == 0 )
43 {
44 string source = " ~\\MemoryDB " ;
45 PWMIS.Core.CommonUtil.ReplaceWebRootPath( ref source);
46 defaultDbSource = source;
47 }
48 return GetDB(defaultDbSource);
49 }
50 }
上面就是我们的“内存数据库引擎”的全部代码,才50行代码,它已经具有按需开启数据库、闲置10分钟自动关闭数据库的功能,我们的内存数据库在缓存里面生活很安逸啊!
5,实例使用“内存数据库”
上面的“理论介绍”已经初步完成了,你可能会有以下问题:
问:这个数据库使用是否方便?
答:非常方便,从数据库取出数据后,就像普通的方法一样操作对象,比如使用Linq To Object,使用完了随时调用下保存方法即可;
问:是否很占用内存?
答:数据只是在缓存中,且有自动过期策略,随需随用,不额外占用内存。
问:大并发是否会有冲突?
答:内存数据库就是给“大并发”访问情况的数据使用的,内存数据库采用一个独立后台线程来写入数据,不会有并发冲突,当然,前台数据的使用应该注意下。
问:支持什么格式的数据?
答:只要是PDF.NET的实体类即可,可以将数据从DBMS查询到实体类中,然后保存到内存数据库。
问:是否支持分布式缓存?
答:内存数据库采用.net 4.0的缓存接口,理论上支持各种缓存实现技术,比如内存、文件或者分布式的MemoryCache。
问:与NoSql有什么区别?
答:内存数据库使用的方法跟普通程序对象没有区别,可以使用Linq To Sql或者直接操作操作数据,而NoSql要采用“键-值”对存储数据,程序中要使用专门的格式存取数据,有一定学习成本。
下面,我们以一个实例,来看如何使用内存数据库:
/// 保存问题的回答结果
/// </summary>
/// <param name="uid"> 用户标识 </param>
/// <param name="answerValue"> 每道题的得分 </param>
public void SaveAnswerResult( string uid, int [] answerValue)
{
MemDB db = MemDBEngin.GetDB();// 获取内存数据库实例
QuestionResult[] resultList = db.Get < QuestionResult > (); // 取数据
QuestionResult oldResult = resultList.Where(p => p.UID == uid).FirstOrDefault();
if (oldResult != null )
{
oldResult.AnswerValue = answerValue;
oldResult.AnswerDate = DateTime.Now;
}
else
{
QuestionResult qr = new QuestionResult();
qr.UID = uid;
qr.AnswerValue = answerValue;
qr.AnswerDate = DateTime.Now;
db.Add(qr);
}
db.Save < QuestionResult > ();// 保存数据
}
/// <summary>
/// 载入某用户的答案数据
/// </summary>
/// <param name="uid"></param>
/// <returns></returns>
public int [] LoadAnswerResult( string uid)
{
MemDB db = MemDBEngin.GetDB();
QuestionResult[] resultList = db.Get < QuestionResult > ();
QuestionResult oldResult = resultList.Where(p => p.UID == uid).FirstOrDefault();
if (oldResult != null )
return oldResult.AnswerValue;
else
return null ;
}
上面的实例中,MemDBEngin是内存数据库引擎,QuestionResult 是PDF.NET的实体类。
怎么样?是不是很简单?我发现只要跟DBMS没关的数据处理,都是很简单!估计你现在也可以搞出一个内存数据库了。
后记
“内存数据库”将在PDF.NET框架的下一个版本中正式集成,目前已经在360基金卫士项目中使用,下面是部分日志:
9/9/2011 AM 12:01:45 后台数据监视线程已开启!
9/9/2011 AM 12:01:45 加载数据 QuestionResult.pmdb 成功!
9/9/2011 AM 12:05:00 保存数据 QuestionResult.pmdb 成功!
9/9/2011 AM 12:15:00 数据库已关闭!
9/9/2011 AM 10:19:19 初始化数据库成功,基础目录: \MemoryDB
9/9/2011 AM 10:19:19 后台数据监视线程已开启!
9/9/2011 AM 10:19:19 加载数据 QuestionResult.pmdb 成功!
9/9/2011 AM 10:22:07 保存数据 QuestionResult.pmdb 成功!
9/9/2011 AM 10:32:20 数据库已关闭!
有关内存数据库的其它问题,请回复本文,如需要内存数据库源码,请和我联系,联系方式,请看PDF.NET框架 官网地址 http://www.pwmis.com/sqlmap
“内存数据库”需要PDF.NET框架的支持,当然你也可以扩展支持其它ORM框架,源码规模很小,欢迎大家一起探讨学习!