MVC 實用架構設計(三)——EF-Code First(5):二級緩存

1、前言

  今天咱們來談談EF的緩存問題。html

  緩存對於一個系統來講相當重要,可是是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的朋友另闢蹊徑。架構

2、緩存設計

(一) 引用EFProviderWrappers

  以下圖,在NuGet中只提供了Entity Framework Provider Wrapper Toolkit(基礎類庫)與Entity Framework Tracing Provider(日誌跟蹤)的下載,很遺憾的並無提供 Entity Framework Caching Provider(緩存)。app

  

  咱們只能本身動手來引用了,這裏提供幾種思路:分佈式

  • 到 http://code.msdn.microsoft.com/EFProviderWrappers 下載代碼,自行編譯,而後在項目GMF.Component.Data項目中手動引用EFProviderWrapperToolkit.dll與EFCachingProvider.dll文件。
  • EFProviderWrapperToolkit由NuGet下載,EFCachingProvider手動引用。

  我是以爲兩種思路都挺麻煩的,這個擴展的代碼貌似已經不更新了(3/18/2011),並且在GMF.Component.Data中額外的引用兩個程序集也是個麻煩事,因而我用下面的方法來引用:ide

在GMF.Component.Data項目中新建兩個文件夾,把以上源代碼中的兩個工程以文件夾的形式包含到項目中。svn

  這樣,彷佛更乾淨利落,如圖:學習

  

(二) 緩存代碼分析及整合

 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頁的時候,數據已經緩存了,沒有讀數據庫查詢數據,說明緩存已經生效了。

  最後要提示的一點:

帶緩存的上下文不能擔當生成數據庫的職責,所以在第一次運行生成數據庫的時候,必須關閉緩存。

3、源碼獲取

  爲了讓你們能第一時間獲取到本架構的最新代碼,也爲了方便我對代碼的管理,本系列的源碼已加入微軟的開源項目網站 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(羣發言僅限技術討論,拒絕閒聊,拒絕醬油,拒絕廣告)
  • 若是你想與我共同來完成這個開源項目,能夠隨時聯繫我。
相關文章
相關標籤/搜索