一、前言

  今天我们来谈谈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建立二级缓存的支持。

  注意:据多位园友经验,此方案不适用于EF6,请使用EF6的朋友另辟蹊径。

二、缓存设计

(一) 引用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 }
49 }

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

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

  <appSettings>
...
<add key="EntityFrameworkCachingEnabled" value="true" />
...
</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 { get; set; }
23
24 [Import("EFCaching", typeof(DbContext))]
25 private Lazy<EFCachingDbContext> EFCachingDbContext { get; set; }
26 }
27 }

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

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

  第1页不计。

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

  

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

  

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

  

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

  

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

  最后要提示的一点:

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

三、源码获取

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

  https://gmframework.codeplex.com/

  可以通过下列途径获取到最新代码:

  • 如果你是本项目的参与者,可以通过VS自带的团队TFS直接连接到 https://tfs.codeplex.com:443/tfs/TFS17 获取最新代码
  • 如果你安装有SVN客户端(亲测TortoiseSVN 1.6.7可用),可以连接到 https://gmframework.svn.codeplex.com/svn 获取最新代码
  • 如果以上条件都不满足,你可以进入页面 https://gmframework.codeplex.com/SourceControl/latest 查看最新代码,也可以点击页面上的 Download 链接进行压缩包的下载,你还可以点击页面上的 History 链接获取到历史版本的源代码
  • 如果你想和大家一起学习MVC,学习EF,欢迎加入群:5008599(群发言仅限技术讨论,拒绝闲聊,拒绝酱油,拒绝广告)
  • 如果你想与我共同来完成这个开源项目,可以随时联系我。

MVC 实用架构设计(三)——EF-Code First(5):二级缓存的更多相关文章

  1. MVC实用架构设计(三)——EF-Code First(5):二级缓存

    前言 今天我们来谈谈EF的缓存问题. 缓存对于一个系统来说至关重要,但是是EF到版本6了仍然没有见到有支持查询结果缓存机制的迹象.EF4开始会把查询语句编译成存储过程缓存在Sql Server中,据说 ...

  2. MVC实用架构设计(三)——EF-Code First(1):Repository,UnitOfWork,DbContext

    前言 终于到EF了,实在不好意思,最近有点忙,本篇离上一篇发布已经一个多星期了,工作中的小迭代告一段落,终于有点时间来继续我们的架构设计了,在这里先对大家表示歉意. 其实这段时间我并不是把这个系列给忘 ...

  3. MVC实用架构设计(三)——EF-Code First(4):数据查询

    前言 首先对大家表示抱歉,这个系列已经将近一个月没有更新了,相信大家等本篇更新都等得快失望了.实在没办法,由于本人水平有限,写篇博客基本上要大半天的时间,最近实在是抽不出这么长段的空闲时间来写.另外也 ...

  4. MVC实用架构设计(三)——EF-Code First(3):使用T4模板生成相似代码

    前言 经过前面EF的<第一篇>与<第二篇>,我们的数据层功能已经较为完善了,但有不少代码相似度较高,比如负责实体映射的 EntityConfiguration,负责仓储操作的I ...

  5. [转]MVC实用架构设计(三)——EF-Code First(3):使用T4模板生成相似代码

    本文转自:http://www.cnblogs.com/guomingfeng/p/mvc-ef-t4.html 〇.目录 一.前言 二.工具准备 三.T4代码生成预热 (一) 单文件生成:Hello ...

  6. MVC 实用架构设计(〇)——总体设计

    〇.目录 一.前言 二.结构图 三.结构说明 一.前言 一直以来都想写这个系列,但基于各种理由(主要是懒惰),迟迟没有动手.今天,趁着周末的空档,终于把系列的目录公布出来了,算是开个头,也给自己一个坚 ...

  7. MVC实用架构设计:总体设计

    http://developer.51cto.com/art/201309/410166.htm

  8. MVC实用构架设计(三)——EF-Code First(6):数据更新最佳实践

    前言 最近在整理EntityFramework数据更新的代码,颇有体会,觉得有分享的价值,于是记录下来,让需要的人少走些弯路也是好的. 为方便起见,先创建一个控制台工程,使用using(var db ...

  9. EF和MVC系列文章导航:EF Code First、DbContext、MVC

    对于之前一直使用webForm服务器控件.手写ado.net操作数据库的同学,突然来了EF和MVC,好多新概念泉涌而出,的确犹如当头一棒不知所措.本系列文章可以帮助新手入门并熟练使用EF和MVC,有了 ...

随机推荐

  1. String的split方法支持正则表达式

    String的split方法支持正则表达式: 1. 正则表达式\s表示匹配任何空白字符 2. +表示匹配一次或多次

  2. java中使用队列:java.util.Queue(转)

    队列是一种特殊的线性表,是运算受到限制的一种线性表,只允许在表的一端进行插入,而在另一端进行删除元素的线性表.队尾(rear)是允许插入的一端.队头(front)是允许删除的一端.空队列是不含元素的空 ...

  3. OpenGL教程一

    引自:https://blog.csdn.net/u013654125/article/details/73613644 GLEW, GLFW和GLM介绍 现在你有了工程,就让我们开始介绍下工程所用到 ...

  4. ubuntu12.04安装Docker

    由于公司的虚拟机上的ubuntu都是12.04的,所以要在ubuntu12.04上安装Docker.Docker目前只能运行在64位的机器上面. 先要升级内核 sudo apt-get update ...

  5. An SPI class of type org.apache.lucene.codecs.PostingsFormat with name 'Lucene50' does not exist. You need to add the corresponding JAR file supporting this SPI to your classpath. The current classp

    背景介绍: 当ES中guava库与hive等组件的库冲突时,对Elasticsearch库进行shade,relocate解决库冲突问题. 当使用"org.apache.maven.plug ...

  6. [原创]找不到mswinsck.ocx的解决办法

    mswinsck.ocx,是在运行程序或者游戏时,系统弹出错误提示“ 找不到mswinsck.ocx”,或者“ 没有找到 mswinsck.ocx”时,说明您系统中缺失这个OCX文件或者该OCX文件没 ...

  7. Failed to read schema document 'http://code.alibabatech.com/schema/dubbo/dubbo.xsd'问题解决方法

    Failed to read schema document 'http://code.alibabatech.com/schema/dubbo/dubbo.xsd'问题解决方法 关于dubbo服务的 ...

  8. 基于Java的数据采集(一)

    之前写过2篇关于PHP数据采集入库的文章: 基于PHP数据采集入库(一):http://www.cnblogs.com/lichenwei/p/3872307.html 基于PHP数据采集入库(二): ...

  9. ruby的第一次使用

    今天看购买的小册,看到推荐使用的工具是ruby写的,提供了源码地址,但是不知道怎么使用 因此尝试使用了下ruby,并记录下来 1.安装 去ruby的官网,下载windows安装包 启动 Ruby 安装 ...

  10. Python 函数(参数组合)

    在Python中定义函数,可以用必选参数.默认参数.可变参数和关键字参数,这4种参数都可以一起使用,或者只用其中某些,但是请注意,参数定义的顺序必须是:必选参数.默认参数.可变参数和关键字参数 定义一 ...