EF Code First:二级缓存

开发 架构
缓存对于一个系统来说至关重要,但是是EF到版本6了仍然没有见到有支持查询结果缓存机制的迹象。EF4开始会把查询语句编译成存储过程缓存在Sql Server中,据说EF6中对此做了改进,会把Linq To Entities 的查询条件直接编译缓存在EF中。但是这些都是只是对查询条件做了缓存,而不是缓存查询的结果集(DbSet.Find(object key)那个虽然走了DbSet.Local数据集,但也仅支持通过主键查找单个实体的情况,很有局限性),没有达到我们想要的效果。

一、前言

今天我们来谈谈EF的缓存问题。

缓存对于一个系统来说至关重要,但是是EF到版本6了仍然没有见到有支持查询结果缓存机制的迹象。EF4开始会把查询语句编译成存储过程缓存在Sql Server中,据说EF6中对此做了改进,会把Linq To Entities 的查询条件直接编译缓存在EF中。但是这些都是只是对查询条件做了缓存,而不是缓存查询的结果集(DbSet.Find(object key)那个虽然走了DbSet.Local数据集,但也仅支持通过主键查找单个实体的情况,很有局限性),没有达到我们想要的效果。

EF不加缓存功能,可能也有另外的考虑吧,这里不去猜测。虽然EF团队没有在EF中加入缓存功能,但已经给出的缓存功能的扩展,这就是Community Entity Framework Provider Wrappers,这个扩展的工作原理由下图可以清晰的了解:

 该扩展提供了跟踪SQL运行日志与SQJ结果集缓存的功能,这里,我们只用到它的缓存功能来为EF建立二级缓存的支持。

二、缓存设计

(一) 引用EFProviderWrappers

如下图,在NuGet中只提供了Entity Framework Provider Wrapper Toolkit(基础类库)与Entity Framework Tracing Provider(日志跟踪)的下载,很遗憾的并没有提供 Entity Framework Caching Provider(缓存)。

我们只能自己动手来引用了,这里提供几种思路:

  • 到 http://code.msdn.microsoft.com/EFProviderWrappers 下载代码,自行编译,然后在项目GMF.Component.Data项目中手动引用EFProviderWrapperToolkit.dll与EFCachingProvider.dll文件。
  • EFProviderWrapperToolkit由NuGet下载,EFCachingProvider手动引用。

我是觉得两种思路都挺麻烦的,这个扩展的代码貌似已经不更新了(3/18/2011),而且在GMF.Component.Data中额外的引用两个程序集也是个麻烦事,于是我用下面的方法来引用:

在GMF.Component.Data项目中新建两个文件夹,把以上源代码中的两个工程以文件夹的形式包含到项目中。

 

这样,似乎更干净利落,如图:

(二) 缓存代码分析及整合

1. 关键代码简介

在EFCachingProvider中,我们要用到的核心类有三个:

  • ICache:缓存缓存基类,系统中实现了一个内存缓存类(InMemoryCache),适用于单台服务器的缓存实现,如果要实现分布式缓存,可以从这个基类进行扩展。
    • InMemoryCache:内存缓存实现类,内部使用了一个Dictionary<string, CacheEntry>作为缓存容器,以查询的SQL语句及参数的连接字符串(或其MD5值)为键(EFCachingCommands.cs类中定义)。还包含了缓存命中、缓存项数量等数据的统计及缓存清理功能。
  • CachingPolicy:缓存策略基类,定义了当前实体是否可缓存(CanBeCached)、定义缓存缓存数(GetCacheableRows)、缓存项滑动过期与绝对过期时间(GetExpirationTimeout)等功能,并默认了绝对过期时间为永不过期(DateTime.MaxValue)
    • NoCachingPolicy:不缓存策略,禁用缓存功能。
    • CacheAllPolicy:缓存所有数据策略,缓存项最大数量为int.MaxValue
    • CustomCachingPolicy:自定义缓存策略,使用了CacheableTables与NonCacheableTables两个集合来表示数据类型是否可缓存的白名单与黑名单,这两个名单将在重写的CanBeCached方法中作为类型是否可缓存的验证依据。
  • EFCachingConnection:此类定义了类型为ICache,CachingPolicy的两个属性,分别用于接收上面据说的两个扩展点。

2. 应用缓存扩展

EF的DbContext上下文类有一个重载

public DbContext(DbConnection existingConnection, bool contextOwnsConnection) { }

 

需要的是DbConnection参数,而EFCachingConnection正好是派生自DbConnection的,我们只需要构建一个EFCachingConnection对象作为参数去构造DbContext派生类的对象,即可完成缓存功能的注入(如本篇第一张图所示)。这里,缓存专用的DbContext派生类只需要派生自原项目中定义的EFDbContext类。

  1. namespace GMF.Component.Data  
  2. {  
  3.     /// <summary>  
  4.     ///     启用缓存的自定义EntityFramework数据访问上下文  
  5.     /// </summary>  
  6.     [Export("EFCaching"typeof (DbContext))]  
  7.     public class EFCachingDbContext : EFDbContext  
  8.     {  
  9.         private static readonly InMemoryCache InMemoryCache = new InMemoryCache();  
  10.  
  11.         public EFCachingDbContext()  
  12.             : base(CreateConnectionWrapper("default")) { }  
  13.  
  14.         public EFCachingDbContext(string connectionStringName)  
  15.             : base(CreateConnectionWrapper(connectionStringName)) { }  
  16.  
  17.         /// <summary>  
  18.         ///     由数据库连接串名称创建连接对象  
  19.         /// </summary>  
  20.         /// <param name="connectionStringName">数据库连接串名称</param>  
  21.         /// <returns></returns>  
  22.         private static DbConnection CreateConnectionWrapper(string connectionStringName)  
  23.         {  
  24.             PublicHelper.CheckArgument(connectionStringName, "connectionStringName");  
  25.  
  26.             string providerInvariantName = "System.Data.SqlClient";  
  27.             string connectionString = null;  
  28.             ConnectionStringSettings connectionStringSetting = ConfigurationManager.ConnectionStrings[connectionStringName];  
  29.             if (connectionStringSetting != null)  
  30.             {  
  31.                 providerInvariantName = connectionStringSetting.ProviderName;  
  32.                 connectionString = connectionStringSetting.ConnectionString;  
  33.             }  
  34.             if (connectionString == null)  
  35.             {  
  36.                 throw PublicHelper.ThrowComponentException("名称为“" + connectionStringName + "”数据库连接串的ConnectionString值为空。");  
  37.             }  
  38.             string wrappedConnectionString = "wrappedProvider=" + providerInvariantName + ";" + connectionString;  
  39.             EFCachingConnection connection = new EFCachingConnection  
  40.             {  
  41.                 ConnectionString = wrappedConnectionString,  
  42.                 CachingPolicy = CachingPolicy.CacheAll,  
  43.                 Cache = InMemoryCache  
  44.             };  
  45.  
  46.             return connection;  
  47.         }  
  48.     }  

这里缓存策略使用了缓存所有数据(CacheAllPolicy)的策略,在实际项目中,最好自定义缓存策略,而不要使用这个策略,以免服务器内存被撑爆。

#p#

我们在应用程序配置(Web.Config或App.Config)中,添加一个名为“EntityFrameworkCachingEnabled”的AppSettings节点,用来进行启用/禁用缓存的开关配置。

  1. <appSettings> 
  2.    ...  
  3.     <add key="EntityFrameworkCachingEnabled" value="true" /> 
  4.    ...  
  5.   </appSettings> 

另外,缓存扩展还需要我们在配置文件中添加如下节点的配置:

  1. <system.data> 
  2.     <DbProviderFactories> 
  3.       <add name="EF Caching Data Provider" invariant="EFCachingProvider" description="Caching Provider Wrapper" type="EFCachingProvider.EFCachingProviderFactory, GMF.Component.Data" /> 
  4.       <add name="EF Generic Provider Wrapper" invariant="EFProviderWrapper" description="Generic Provider Wrapper" type="EFProviderWrapperToolkit.EFProviderWrapperFactory, GMF.Component.Data" /> 
  5.     </DbProviderFactories> 
  6.   </system.data> 

再来看看,怎样使用“EntityFrameworkCachingEnabled”配置来控制缓存功能的开关。我们的设计中,DbContext对象的注入点为如下所示的Context属性:

所以,我们只需要在UnitOfWorkContextBase的派生类中读取 EntityFrameworkCachingEnabled 进行切换即可。

  1. namespace GMF.Component.Data  
  2. {  
  3.     /// <summary>  
  4.     ///     数据单元操作类  
  5.     /// </summary>  
  6.     [Export(typeof (IUnitOfWork))]  
  7.     public class EFRepositoryContext : UnitOfWorkContextBase  
  8.     {  
  9.         /// <summary>  
  10.         ///     获取 当前使用的数据访问上下文对象  
  11.         /// </summary>  
  12.         protected override DbContext Context  
  13.         {  
  14.             get 
  15.             {  
  16.                 bool secondCachingEnabled = ConfigurationManager.AppSettings["EntityFrameworkCachingEnabled"].CastTo(false);  
  17.                 return secondCachingEnabled ? EFCachingDbContext.Value : EFDbContext.Value;  
  18.             }  
  19.         }  
  20.  
  21.         [Import("EF"typeof (DbContext))]  
  22.         private Lazy<EFDbContext> EFDbContext { getset; }  
  23.  
  24.         [Import("EFCaching"typeof(DbContext))]  
  25.         private Lazy<EFCachingDbContext> EFCachingDbContext { getset; }  
  26.     }  

注意,因为EFDbContext与EFCachingDbContext两个属性只能同时用到其中之一,导入需要使用Lazy<>类型来包装,这样没用到的属性就不会实例化了。

下面,我们来测试一下缓存功能是否生效,就用上篇的那个翻页列表吧。判断标准为SQL Server Profiler是否有SQL语句执行。为方便演示,这里在列表的下方显示当前的时间,以便与SQL Server Profiler中的时间进行匹配。

 第1页不计。

点击第2页,执行了查询:

点击第3页,执行了查询:

再回到第2页,没有执行查询:

点击第4页,执行了查询:

结论:重复第2页的时候,数据已经缓存了,没有读数据库查询数据,说明缓存已经生效了。

最后要提示的一点:

带缓存的上下文不能担当生成数据库的职责,因此在第一次运行生成数据库的时候,必须关闭缓存。

 

三、源码获取

为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 http://www.codeplex.com,地址为:

https://gmframework.codeplex.com/

原文链接:http://www.cnblogs.com/guomingfeng/p/mvc-ef-caching.html

责任编辑:林师授 来源: 博客园
相关推荐

2009-06-18 15:24:35

Hibernate二级

2009-09-21 14:59:31

Hibernate二级

2009-09-24 11:04:56

Hibernate二级

2009-09-21 13:31:10

Hibernate 3

2009-09-21 14:39:40

Hibernate二级

2009-09-23 09:37:07

Hibernate缓存

2009-06-10 15:00:58

Hibernate二级配置

2013-09-08 21:41:10

RepositoryUnitOfWorkDbContext

2013-09-08 22:40:38

EF Code Fir数据查询架构设计

2009-08-13 18:12:12

Hibernate 3

2024-12-03 14:38:07

CaffeineRedis二级缓存

2022-12-02 12:01:30

Spring缓存生命周期

2022-03-01 18:03:06

Spring缓存循环依赖

2022-01-12 07:48:19

缓存Spring 循环

2013-09-08 23:37:30

EF Code Fir架构设计MVC架构设计

2015-06-11 10:12:26

Android图片加载缓存

2023-04-27 08:18:10

MyBatis缓存存储

2019-08-21 14:34:41

2013-09-08 22:12:02

EF Code Fir数据迁移MVC架构设计

2023-08-01 08:10:46

内存缓存
点赞
收藏

51CTO技术栈公众号