移花接木:当泛型方法遇上抽象类----我的“内存数据库”诞生记

之前,不怕“重复发明轮子”的我,搞了一个“PDF.NET框架”,即“PWMIS数据开发框架”(目前已经开源),自己用特殊的方式设计了一个实体类基类,然后又设计了操作实体类的语法--“OQL表达式”,一套类似SQL的对象化的操作实体类的语法,接着又实现了实体类的“二进制序列化”,最近突发奇想,何不将这个系列化后的实体类,搞成一个数据库?重新走DBMS的老路显然没有竞争力,目前NoSql正流行,那我就搞个内存数据库吧!

 

其实,说到做“内存数据库”,概念大了些,我个人能力有限,要做也只能做个“概念整合”,初步想法是,数据全部以“对象”的形式存在内存中,用Linq To Object的方式,来操作这些“数据”,将数据保存到一个持久化媒体中,比如磁盘文件中,开一个后台线程慢慢去写,而前台的数据使用是可以经受主大量并发操作的。想法有了,立刻开工!

 

1,数据的持久化

首先,封装一下实体类的持久化过程,将实体类序列化后保存在磁盘文件,或者从一个磁盘文件加载实体类,直接上代码:

 

 1          ///   <summary>
 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 > .BinarySerialize(List < T >  entitys);  // 二进制序列化
PdfNetSerialize < T > .BinaryDeserializeArray( byte [] buffer);  // 二进制反序列化

 

这两个方法,根据具体的类型T 获取文件名,其它就没有什么好说的。

 

2,构造“数据仓库”

既然是“数据库”,肯定要有一个地方来集中存放,那内存数据库自然是把所有数据放到内存中,于是定义一个“数据容器”对象:

 

List < EntityBase[] >  dataContainer  = new  List < EntityBase[] > ();

由于容器中要存放各种具体的实体类对象,所以我使用实体类的基类 EntityBase 来定义,数据容器 dataContainer中存放的是具体实体类对象的数组,于是统一保存数据就是下面类似的代码:

 

1  private   void  SaveAllEntitys()
2  {
3       foreach (EntityBase[] item  in  dataContainer)
4      {
5           this .SaveEntity < EntityBase > (item);
6      }
7  }

 

非常不幸,我调用的 SaveEntity 方法无法编译通过,VS给出的错误提示

“必须是具有公共的无参数构造函数的非抽象类型,才能用作泛型类型或方法”SaveEntity>(T[] entitys)中的参数“T”,

于是改一下保存数据的方法,去掉new() 泛型约束:

 

public   bool  SaveEntity < T > (T[] entitys)  where  T : EntityBase {...}

但序列化实体类的方法无法编译通过:

byte[] buffer = PdfNetSerialize<T>.BinarySerialize(entitys);

BinarySerialize 方法也要求泛型类类型<T>不能是抽象类或接口类型!

 

接着去修改序列化方法?不太可能,因为PDF.NET的类库已经很成熟了,难以评估此修改会对原有的项目产生什么影响。

 

本着“对修改关闭,对扩展开放”的原则,只有另辟蹊径,不走寻常路了。

 

3,移花接木

我们再来看看 SaveAllEntitys 方法,如果我们能够在调用 SaveEntity 之前,拿到EntityBase类的具体实现类型,那该多好啊!这样就解决了泛型类不能使用抽象类类型的问题,但这里怎么可能拿得到呢?虽然我们在运行时,我们能够确切的看到 item 变量对应的对象的具体类型,但我们的代码在这里却没法给泛型方法的类型<T>一个交代,这可怎么办呢?

这个问题不突破,后面的工作都没法进行,足足让我思考了好几个小时。

“运行时才知道具体类型...”

运行时...运行时...”

突然,灵光一现,何不在“运行时记录方法实际调用的具体类型”?也就是“捕获调用的方法”,而不是获取“方法的执行结果”。举个简单例子:

 

Function  我要金山1()

' 找金山的具体过程

End Function

Function  我要金山2()

' XXX想要金山!记录下来他怎么找到金山的

End Function

 

“我要金山2”跟“我要金山1”的区别就是,前者是要找金山的方法,而后者目的只是要金山!正所谓“授人与鱼不如授人与渔”!

在.NET中,如何才能捕获“方法的调用”而不是获取“方法的执行结果”?或者说,如何才能先将方法的调用记录下来,以后在某个时候再来执行?就像上面的例子“我要金山2”,外人看起来他好像是要了一座金山,其实他背后的“野心大大的”,要拥有更多的金山,这对外人而言他简直就是在“移花接木”!

闲话少说,还是请我们今天的主角出场:

 

“隆重欢迎《委托》先生出场!”

 

看看我们的“《委托》先生”是怎么表演的:

 

 1           private  List < Func < bool >>  methodList;
 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 方法的参数,以达到“移花接木”的效果:

 

1          private   void  AddSaveMethod(Func < bool >  toDo)
2          {
3               if ( ! methodList.Contains(toDo))
4                  methodList.Add(toDo);
5          }

 

最后,我们只需要在某个时候,开个后台线程,来真正执行这些“数据保持的方法”即可,下面是保存数据到磁盘的代码:

 

 1          ///   <summary>
 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 程序集,可以在各种类型的应用程序中使用,就选它了:

 

 1     ///   <summary>
 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>
        
///  保存问题的回答结果
        
///   </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 初始化数据库成功,基础目录: \MemoryDB
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框架,源码规模很小,欢迎大家一起探讨学习!

 

你可能感兴趣的:(内存数据库)