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


    之前,不怕“重复发明轮子”的我,搞了一个“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(0100), //距离上次调用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框架,源码规模很小,欢迎大家一起探讨学习!

  • 相关阅读:
    2、函数
    二者取其一(初遇)_网络流
    P1879 [USACO06NOV]玉米田Corn Fields
    P2831 愤怒的小鸟
    P2296 寻找道路
    序(不知道是什么时候的模拟题)
    P2243 电路维修
    P1273 有线电视网
    P2613 【模板】有理数取余
    P1373 小a和uim之大逃离
  • 原文地址:https://www.cnblogs.com/bluedoctor/p/2171734.html
Copyright © 2020-2023  润新知