专栏首页程序员的SOD蜜移花接木:当泛型方法遇上抽象类----我的“内存数据库”诞生记

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • “设计应对变化”--实例讲解一个数据同步系统

     系列文章索引: [WCF邮件通信系统应用 之 数据同步程序 之 设计内幕 之 一] 同步一个数据库要发多少个数据包? [WCF邮件通信系统应用 之 数据同步...

    用户1177503
  • PDF.NET开发框架“内存数据库”架构设计

    前一段时间,我写了篇《移花接木:当泛型方法遇上抽象类----我的“内存数据库”诞生记 》,记录了PDF.NET内存数据库的设计过程,最近做了些小改动,已经投入生...

    用户1177503
  • 打造轻量级的实体类数据容器

        这里有三个关键词:轻量级,实体类,数据容器,还有一个潜在的关键词:通用。这几个名词之间有什么联系呢?     一般来说,操作实体类往往伴随着一个实体类集...

    用户1177503
  • ubuntu蓝牙音响配对成功但在声音设置中无法设置 解决

    zhangrelay
  • LeetCode 11盛水最多的容器&12整数转罗马数字

    给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) ...

    bigsai
  • AtomicInteger 源码分析

    AtomicInteger 扩展了 Number,适用于基于数字的处理,并提供了如原子递增等,适合一些计数场景

    itliusir
  • 编程小知识之 Object.Destroy

    Object.Destroy 应该是 Unity 开发中最常用的函数之一了,对于该函数的一个基本认知是:

    用户2615200
  • Laravel 广播系统工作原理

    今天,让我们深入研究下 Laravel 的广播系统。广播系统的目的是用于实现当服务端完成某种特定功能后向客户端推送消息的功能。本文我们将学习如何使用第三方 Pu...

    柳公子
  • 用Scater包分析文章数据

    Scater需要利用SingleCellExperiment这个对象(需要注意的是,官方友情提示,在导入对象之前,最好是将表达量数据存为矩阵)

    生信技能树jimmy
  • Spring AOP 创建代理的源码解析

    在上篇文章 Spring AOP 注解方式源码解析 中已经获取到了 bean 的对应增强器,之后,就可以创建对应的代理了,Spring AOP 底层使用的是 ...

    Java技术大杂烩

扫码关注云+社区

领取腾讯云代金券