摘要:NHibernate Contrib 支持很多第三方的二级缓存,如SysCache,MemCache,Prevalence等等,但是没有MongoDB的,于是自己扩展了一个支持MongoDB的缓存组件(NHibernate.Caches.MongoDBCache.dll)。本篇先把组件的源代码开放出来。
一、背景
在NHibernate的Contrib贡献项目官方网站(NHibernateContrib项目是由NHibernate开发团队或者终端用户根据需要自行编译并贡献的一系列的程序)中,拥有一个NHibernate.Caches的项目,里面包含汗多基于NHibernate二级缓存的组件,其中包括有:
NHibernate.Caches.MemCache:基于memcached分布式存储的缓存组件。这个大家都比较熟悉了就不多说了,详细可查阅相关信息。
NHibernate.Caches.Prevalence:基于Bamboo.Prevalence的缓存组件。它可产生一系列的缓存目录,通过缓存目录可以从文件中获取数据,并且在缓存目录中通过Snapshot,也就是快照,可以进行断点保存。详细介绍请看我的文章:(在Spring.Net中对于NHibernate.Caches.Prevalence的使用)
NHibernate.Caches.SharedCache:基于MergeSystem.Indexus.WinServiceCommon、MergeSystem.Indexus.WinService和MergeSystem.Indexus.Notify的分布式存储的缓存组件。用于在动态WEB或Win应用程序中减少数据库的负责,提高访问速度。
NHibernate.Caches.SysCache:我们通常DotNet上所使用的System.Web.Caching.Cache。
NHibernate.Caches.SysCache2:同上。不同的是增加了对于SQL2005的缓存依赖的支持。
NHibernate.Caches.Velocity:基于微软推出的分布式缓存Velocity组件。跟memcached一样,“Velocity”维护一张大的哈希表,这张表可以跨越多个服务器,你可以通过添加或者减少服务器来平衡系统压力。
二、什么是MongoDB?
MongoDB是一个基于分布式文档存储的数据库。旨在为WEB应用提供可护展的高性能数据存储解决方案。它是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bjson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。 它的特点是高性能、易部署、易使用,存储数据非常方便。
MongoDB官方服务端下载地址:http://www.mongodb.org/downloads
MongoDB官方客户端(.NET)下载地址:https://github.com/samus/mongodb-csharp
三、准备工作
服务器端下载下来后,首先要安装MongoDB,大家可以参考下这篇文章:http://www.cnblogs.com/mamboer/archive/2010/03/05/1679292.html
在你开发之前必须先吧MongoDB的服务或者控制台启动。这里我采用启动控制台。
从图中看出,MongoDB采用的默认端口是27017,并且在我安装的时候,将MongoDB的数据库目录配置在:C:\data\db上。
现在开始,我要增加一个支持MongoDB的缓存组件,那么首先要先了解它们二级缓存流程的一些机制,本篇先不具体谈它的原理(会在下篇具体描述),先谈下它是如何实现的,要研究如何实现其实很简单,依葫芦画瓢,去看人家写的代码。
四、分析与实现
1. 在Spring.NET关于NHibernate的配置中,可以启用二级缓存其中有个配置节点是:
<
entry
key
="cache.provider_class"
value
="NHibernate.Cache.HashtableCacheProvider"
/>
HashtableCacheProvider是NHibernate二级缓存中自带的默认的缓存提供程序。而HashtableCacheProvider继承的是ICacheProvider接口,于是要创建一个支持MongoDB的缓冲提供程序,就必须继承它。
2. 创建一个MongoDBCacheProvider类:
代码
///
<summary>
///
MongoDB缓存提供程序
///
</summary>
public
class
MongoDBCacheProvider : ICacheProvider
{
private
static
readonly
ILog log
=
LogManager.GetLogger(
typeof
(MongoDBCacheProvider));
static
MongoDBCacheProvider()
{
}
public
ICache BuildCache(
string
regionName, IDictionary
<
string
,
string
>
properties)
{
if
(regionName
==
null
)
{
regionName
=
string
.Empty;
}
if
(properties
==
null
)
{
properties
=
new
Dictionary
<
string
,
string
>
();
}
if
(log.IsDebugEnabled)
{
}
return
new
MongoDBCache(regionName, properties);
}
public
long
NextTimestamp()
{
return
Timestamper.Next();
}
public
void
Start(IDictionary
<
string
,
string
>
properties)
{
}
public
void
Stop()
{
}
}
这样就实现了一个初步的MongoDB缓存提供程序的构架。注意到BuildCache方法返回的是一个ICache对象。这里就必须实现一个继承ICache接口的MongoDB缓存对象。
3. 看下ICache都定义了哪些接口方法和属性:
代码
public
interface
ICache
{
void
Clear();
void
Destroy();
object
Get(
object
key);
void
Lock(
object
key);
long
NextTimestamp();
void
Put(
object
key,
object
value);
void
Remove(
object
key);
void
Unlock(
object
key);
string
RegionName {
get
; }
int
Timeout {
get
; }
}
从字面上解释,应该大家都能够明白的:Clear清空缓存,Destroy和Clear类似,但是具体问题具体分析,Get取缓存,Lock锁定缓存,在ReadWrite模式的缓存上需要使用到,NextTimestamp下一时间段的时间戳,Put设置缓存,Remove清除指定的缓存数据,Unlock解除锁定,同样在ReadWrite模式的缓存上需要使用,RegionName区域名称,Timeout缓存过期时间。
4. 创建一个MongoDBCache的缓存类:
在它的构造函数中的代码:
代码
public
MongoDBCache(
string
regionName, IDictionary
<
string
,
string
>
properties)
{
_regionName
=
regionName;
if
(properties
!=
null
)
{
string
dbName
=
string
.Empty;
if
(properties.TryGetValue(
"
mongodb.dasebaseName
"
,
out
dbName))
{
if
(
!
string
.IsNullOrEmpty(dbName))
{
_dbName
=
dbName;
}
}
string
connectionString
=
string
.Empty;
if
(properties.TryGetValue(
"
mongodb.connectionString
"
,
out
connectionString))
{
if
(
!
string
.IsNullOrEmpty(connectionString))
{
_connectionString
=
connectionString;
}
}
string
pattern
=
string
.Empty;
if
(properties.TryGetValue(
"
mongodb.pattern
"
,
out
pattern))
{
if
(
!
string
.IsNullOrEmpty(pattern))
{
_pattern
=
pattern;
}
}
string
regionPrefix
=
string
.Empty;
if
(properties.TryGetValue(
"
regionPrefix
"
,
out
regionPrefix))
{
if
(
!
string
.IsNullOrEmpty(regionPrefix))
{
_regionPrefix
=
regionPrefix;
}
}
}
mongo
=
new
Mongo(_connectionString);
//
连接
mongo.Connect();
//
获取Mongo数据库实体
db
=
mongo[_dbName];
}
其中可以看出这里需要连接mongo的对象,并且指定它的数据库。
而在它的析构函数中:
代码
~
MongoDBCache()
{
Dispose();
}
///
<summary>
///
释放资源
///
</summary>
public
void
Dispose()
{
//
关闭连接
mongo.Disconnect();
//
释放mongo资源
mongo.Dispose();
}
必须关闭mongo的连接,并且释放mongo资源。
对于存储缓存数据(存在Mongo数据库的表中):
设置缓存数据Put
public
void
Put(
object
key,
object
value)
{
if
(key
==
null
)
{
throw
new
ArgumentNullException(
"
key
"
,
"
null key not allowed
"
);
}
if
(value
==
null
)
{
throw
new
ArgumentNullException(
"
value
"
,
"
null value not allowed
"
);
}
if
(log.IsDebugEnabled)
{
log.DebugFormat(
"
setting value for item {0}
"
, key);
}
string
hashKey
=
GetAlternateKeyHash(key);
GenerateTableName(key);
Console.WriteLine(
string
.Format(
"
Put------Key:{0}, Value:{1}
"
, hashKey, value.ToString()));
IMongoCollection
<
Document
>
table
=
db.GetCollection
<
Document
>
(TableName);
IDictionary
<
string
,
object
>
dict
=
new
Dictionary
<
string
,
object
>
();
dict.Add(
"
Key
"
, hashKey);
Document query
=
new
Document(dict);
//
查询
Document document
=
table.FindOne(query);
try
{
if
(document
==
null
)
{
IDictionary
<
string
,
object
>
newDict
=
new
Dictionary
<
string
,
object
>
();
newDict.Add(
"
Value
"
, SerializeHelper.BinarySerialize(value));
newDict.Add(
"
Key
"
, hashKey);
newDict.Add(
"
Type
"
, value.GetType().Name);
newDict.Add(
"
Date
"
, DateTime.Now.ToString());
document
=
new
Document(newDict);
}
else
{
document[
"
Value
"
]
=
SerializeHelper.BinarySerialize(value);
document[
"
Type
"
]
=
value.GetType().Name;
document[
"
Date
"
]
=
DateTime.Now.ToString();
}
//
保存Document
table.Save(document);
}
catch
{
}
finally
{
}
}
这里会将value对象序列化为字节数组,有人会问为什么不直接存储对象呢,还需要序列化,这是由于它的存储的数据结构决定的,它最后在数据库中形成的结果为一个BSON结构;还有人会问可以把它序列化为JSON字符串吗,我也做过尝试,但是后来发现value实际上的类型是CacheItem或者CacheEntity,它们都没有无参的构造函数,所以无法反序列化。因此,这里我采用了字节转换的方式。
从代码中,可以看到document包含Key,Value,Type,Date(非必须的)的字段,其中Type在获取缓存数据(Get)的时候非常有用。
对于获取数据:
获取缓存数据Get
public
object
Get(
object
key)
{
string
hashKey
=
GetAlternateKeyHash(key);
GenerateTableName(key);
Console.WriteLine(
string
.Format(
"
Get------Key:{0}
"
, hashKey));
IMongoCollection
<
Document
>
table
=
db.GetCollection
<
Document
>
(TableName);
IDictionary
<
string
,
object
>
dict
=
new
Dictionary
<
string
,
object
>
();
dict.Add(
"
Key
"
, hashKey);
Document query
=
new
Document(dict);
//
查询
Document document
=
table.FindOne(query);
if
(document
!=
null
)
{
try
{
byte
[] bytes
=
((MongoDB.Binary)document[
"
Value
"
]).Bytes;
#region
反序列化字节数组
if
(
string
.Equals(document[
"
Type
"
].ToString(),
typeof
(CacheEntry).Name, StringComparison.InvariantCultureIgnoreCase))
{
return
SerializeHelper.BinaryDeSerialize
<
CacheEntry
>
(bytes);
}
else
if
(
string
.Equals(document[
"
Type
"
].ToString(),
typeof
(CachedItem).Name, StringComparison.InvariantCultureIgnoreCase))
{
return
SerializeHelper.BinaryDeSerialize
<
CachedItem
>
(bytes);
}
else
if
(
string
.Equals(document[
"
Type
"
].ToString(),
typeof
(List
<
Object
>
).Name, StringComparison.InvariantCultureIgnoreCase))
{
return
SerializeHelper.BinaryDeSerialize
<
List
<
Object
>>
(bytes);
}
else
if
(
string
.Equals(document[
"
Type
"
].ToString(),
typeof
(Int64).Name, StringComparison.InvariantCultureIgnoreCase))
{
return
SerializeHelper.BinaryDeSerialize
<
Int64
>
(bytes);
}
else
if
(
string
.Equals(document[
"
Type
"
].ToString(),
typeof
(CacheLock).Name, StringComparison.InvariantCultureIgnoreCase))
{
return
SerializeHelper.BinaryDeSerialize
<
CacheLock
>
(bytes);
}
else
{
return
null
;
}
#endregion
}
catch
{
return
null
;
}
}
return
null
;
}
其中Document document = table.FindOne(query);是从表中根据指定的Document查询数据。并且对于字节数据Value字段,必须进行字节反序列化。
在Spring.NET对于NH的配置节点中可以这样子写:
代码
<!--
MongoDB缓存机制
-->
<
entry
key
="cache.provider_class"
value
="NHibernate.Caches.MongoDBCache.MongoDBCacheProvider, NHibernate.Caches.MongoDBCache"
/>
<
entry
key
="mongodb.dasebaseName"
value
="xinogxt"
/>
<
entry
key
="mongodb.connectionString"
value
="servers=127.0.0.1:27017"
/>
<
entry
key
="mongodb.pattern"
value
="^TestWebServer\.Model\..+?"
/>
其中mongodb.dasebaseName是给MongoDB配置的数据库名称;mongodb.connectionString是MongoDB服务的连接字符串;mongodb.pattern是为了作为表名称的匹配正则表达式,可以看下这段代码:
代码
///
<summary>
///
生成表格名称
///
</summary>
///
<param name="key"></param>
private
void
GenerateTableName(
object
key)
{
if
(key
is
CacheKey)
{
CacheKey cacheKey
=
(CacheKey)key;
//
判断是否匹配正则表达式
if
(Regex.IsMatch(cacheKey.EntityOrRoleName, _pattern))
{
_tableName
=
cacheKey.EntityOrRoleName.Replace(
"
.
"
,
"
_
"
);
}
}
}
它是通过CacheKey的EntityOrRoleName属性,进行筛选,比如:这里的EntityOrRoleName为”“TestWebServer.Model.TblEnterprise”的字符串(这是一个NH自动生成的实体类),我给它的正则表达式为“^TestWebServer\.Model\..+?”,那么它匹配了,我就取它的这个字符串为表名称,最后的表名为:“TestWebServer_Model_TblEnterprise”。这样我缓存每一个实体,都能够自动创建相应的一个Mongo表。
5. 看下运行的结果:
测试代码如下:
[Test]
public
void
EnterpriseDaoTest6()
{
IEnterpriseDao dao
=
(IEnterpriseDao)applicationContext.GetObject(
"
EnterpriseDao
"
);
ITblEnterprise enterprise
=
dao.GetInfo(
1
);
…
}
第一次执行:
第一次的时候,执行了数据库的SELECT的SQL语句。
我查看本地目录以及用MongoVUE客户端工具查看了下Mongo数据库:
缓存数据已经存在目录(数据库)中。
第二次执行:
发现这里没有执行SQL。
说明MongoDB缓存成功。
6. 通过对对于NHibernate二级缓存机制的理解,我们完全可以扩展属于我们自己的缓存组件。不仅仅是作为MongoDB为载体的缓存实现。
因此,在下一篇文章中,我将重点介绍关于NHibernate二级缓存机制的原理,并且继续深入探讨MongoDB缓存组件的相关原理。
NHibernate.Caches.MongoDBCache.dll项目源代码下载:NHibernate.Caches.MongoDBCache.rar