NHibernate初学者指南(10):一级和二级缓存
一级缓存
为了获得更好的性能,NHibernate智能地缓存数据。NHibernate有不同的缓存机制起作用,最重要的就是一级缓存。每个session对象维持一个一级缓存,session对象创建时缓存创建,session对象释放时缓存销毁。
缓存只不过是一个哈希表。哈希表根据唯一键存储值,值可以根据唯一键检索。
一个实体由它的ID唯一标识,如果两个实体类型相同,ID也相等,那么这两个实体是相等的。NHibernate要求两个相同类型的对象不能有相同的ID。原因是,如果允许系统有相同ID的两个实例,那么就会将系统置于不一致的状态中。有了这个条件,NHibernate就可以执行下面的操作了:
NHibernate session对象从数据库中加载指定ID的实体,然后放到一级缓存中,访问该实体的键是它的ID值。当系统再次从数据库中加载同一个实体时,session对象首先检查它的缓存,如果实体已经存在于缓存中,NHibernate就返回缓存的实例。只有实体不在缓存时,NHibernate session对象才从数据库中加载实体。看下面的过程:
- 程序请求session ID为1的product
- session问一级缓存:“有ID为1的product吗?”
- 一级缓存回答说:“没有”
- session就从数据库中加载ID为1的product
- session将product放入一级缓存,键为product的ID值
- session返回给程序product实例
- 程序执行更多的操作
- 程序再次请求session ID为1的product
- session问一级缓存:“有ID为1的product吗”
- 一级缓存回答说:“有”
- session就使用ID作为键从缓存中加载ID为1的product,并返回给程序
我们使用下面的代码从数据库中加载实体并隐式的使NHibernate session将它存储到一级缓存:
var product = sesson.Get<Product>(1);
之后的Get操作不会引起NHibernate查询数据库而是从一级缓存中检索对象。
清除缓存
我们使用下面的语句请求session从一级缓存中移除一个实体:
session.Evict(product);
如果想完全的清除缓存,可以使用下面的代码:
session.Clear();
上述语句应该仅在特殊情况下使用,因为,如果使用不当会导致显著的性能下降。建议只在写测试代码时使用这些操作。
刷新缓存中的实体
如果想刷新一级缓存中的单个实体,那么可以使用下面的语句:
session.Refresh(product);
上面的代码重新从数据库中加载product实体的状态,这在session打开的同时数据库里的实体被其他程序更改的情况下非常有意义。
二级缓存
我们已经看到NHibernate提供了非常有效的方式缓存数据。可惜,一级缓存绑定到session对象,也就是说每次session被释放,所有的缓存数据就会丢失。二级缓存定义在session工厂级别的,只要session工厂没有被释放缓存就一直存在。一旦实体加载,二级缓存就被激活,实体对所有的session(同session工厂的)都可用。这样,只要实体在二级缓存中,NHibernate就不会从数据库加载实体,直到它从缓存中移除。
启动二级缓存,我们就要定义使用哪个缓存提供程序。二级缓存有各种实现。我们的例子中使用基于哈希表的缓存,它包含在核心的NHibernate程序集中。请注意,不应该在生产级的代码中使用这种缓存提供程序,仅仅用在测试中。参看后面的“二级缓存的实现”部分,选择哪个实现最适合你;然而,如果你改变了缓存提供程序,你不用修改你的代码。
使用下面的代码只会引发NHibernate访问一次数据库检索ID为1的product,即使我们使用两个不同的session实例:
using (var session1 = sessionFactory.OpenSession()) { var product = session1.Get<Product>(1); } using (var session2 = sessionFactory.OpenSession()) { var product = session2.Get<Product>(1); }
第二个Get操作会从二级缓存中获取product实体。然而要注意的是,如果没有启动二级缓存,上面的代码会访问两次数据库。
另外,启动二级缓存,必须相应的配置NHibernate。配置的详细内容会在后面介绍。我们还必须映射实体为可缓存的。如果使用fluent映射映射实体,那么添加到映射的必要语句是:
Cache.ReadWrite();
只有显示配置的实体才会缓存在二级缓存中。
缓存区域
如果不使用缓存区域,那么二级缓存只能整体清除。如果需要清除二级缓存的一部分,就得使用缓存区域。缓存区域根据它们的名字区分。我们可以将任何数量的不同查询放在命名的缓存区域中。清除一个缓存区域的命令如下:
sessionFactory.EvictQueries("My Region");
当前使用的sessionFactory是session工厂的实例,My Region是缓存区域的名字。
二级缓存的实现
所有的二级缓存提供程序都是NHibernate contributions项目的一部分。下面列表给出了一些支持的提供程序的简短描述。
- SysCache:使用System.Web.Caching.Cache作为缓存提供程序。也就是说可以依靠ASP.NET的缓存功能来理解它如何工作。
- SysCache2:和NHibernate.Caches.SysCache一样,使用ASP.NET缓存。这个提供程序也支持SQL基于依赖过期,意思是当数据库里的相关数据发生改变不可能自动地配置某些缓存区域。
- Velocity:它是Microsoft Windows Server App Fabric的一部分,是一组构建、扩展和管理基于IIS的web应用程序的综合服务。
- Prevalence:使用Bamboo.Prevalence作为缓存提供程序。Bamboo.Prevalence是由Klaus Wuestefeld在Prevayler中提出的对象流行概念的.NET实现。Bamboo.Prevalence提供针对CLR的确定性系统的透明对象持久化。它为智能客户端应用程序提供持久化的缓存。
- MemCache:Memcached.Memcahed是一个高性能,分布式内存对象缓存系统,但是旨在通过减轻数据库负载加速动态web应用程序。根本上说,就是一个分布式的哈希表。
实例—使用二级缓存
在这个例子中,我们使用NHibernate的二级缓存实现一个非常简单的例子。我们在例子中使用哈希表缓存提供程序,但是在生产级代码中不要使用这个提供程序!
1. 在SSMS中创建一个数据库:SecondLevelCacheSample。
2. 打开Visual Studio,创建一个Console Application:SecondLevelCacheSample。
3. 添加对NHibernate.dll,FluentNHibernate.dll和NHibernate.ByteCode.Castle.dll程序集的引用。
4. 在项目中创建Domain文件夹。
5. 在Domain文件夹中创建一个类文件Product.cs,添加如下代码:
public class Product { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual decimal UnitPrice { get; set; } public virtual int ReorderLevel { get; set; } public virtual bool Discontinued { get; set; } }
6. 在Domain文件夹中添加一个ProductMap.cs类文件用来映射Product实体,代码如下:
public class ProductMap : ClassMap<Product> { public ProductMap() { Cache.ReadWrite(); Id(x => x.Id).GeneratedBy.HiLo("1000"); Map(x => x.Name); Map(x => x.UnitPrice); Map(x => x.ReorderLevel); Map(x => x.Discontinued); } }
注意在映射中添加了Cache.ReadWrite()语句用来告诉NHibernate为Product实体使用二级缓存。
7. 在Program类中添加一个ISessionFactory类型的静态字段,如下:
private static ISessionFactory sessionFactory;
8. 在Program类中添加一个静态方法:ConfigureSystem。这个方法包含配置NHibernate的一般代码和二级缓存的代码。如下面的代码所示:
private static void ConfigureSystem() { const string connString = "server=.;database=SecondLevelCacheSample;" + "user id=sa;password=sasa;"; var configuration = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008 .ConnectionString(connString) .ShowSql) .Mappings(m => m.FluentMappings .AddFromAssemblyOf<Product>()) .BuildConfiguration(); configuration.Properties["cache.provider_class"] = "NHibernate.Cache.HashtableCacheProvider"; configuration.Properties["cache.use_second_level_cache"] = "true"; var exporter = new SchemaExport(configuration); exporter.Execute(true, true, false); sessionFactory = configuration.BuildSessionFactory(); }
配置二级缓存的代码中,第一个是告诉NHibernate使用哪个二级缓存提供程序。它是一个键值对。值是实现二级缓存提供程序类的全路径。在我们的例子中不需要定义程序集,因为该提供程序在NHibernate程序集内。第二个是启用或禁止二级缓存的使用。注意,从技术上来说,第二个配置不需要,因为它的默认值是true。
9. 在Program类中,创建一个TestLoadEntity的静态方法,代码如下:
private static void TestLoadEntity() { int productId; var product = new Product { Name = "Apple", UnitPrice = 1.55m, ReorderLevel = 100, Discontinued = false }; using (var session = sessionFactory.OpenSession()) { using (var tx = session.BeginTransaction()) { productId = (int)session.Save(product); tx.Commit(); } } using (var session1 = sessionFactory.OpenSession()) { var product1 = session1.Get<Product>(productId); } using (var session2 = sessionFactory.OpenSession()) { var product2 = session2.Get<Product>(productId); } }
10. 在Main方法中调用ConfigureSystem和TestLoadEntity方法:
static void Main(string[] args) { ConfigureSystem(); TestLoadEntity(); Console.Write("\r\nHit enter to exit:"); Console.ReadLine(); }
11. 运行程序,验证没有select语句生成。这说明二级缓存起作用了。
12. 现在改变cache.use_second_level_cache设置为false,再次运行程序。这次,有两个select语句发送到数据库,如下图所示:
很明显,除了insert语句外,还有两个select语句:每个session对象一个,用来加载product实体。
在这个例子中,我们学会了如何配置程序以便NHibernate使用二级缓存,也学会了如何配置实体映射以便它们可以被缓存到二级缓存中。