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