前言
今天我們來談談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項目中新建兩個文件夾,把以上源代碼中的兩個工程以文件夾的形式包含到項目中。
這樣,似乎更干凈利落,如圖:
緩存代碼分析及整合
關鍵代碼簡介
在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的兩個屬性,分別用於接收上面據說的兩個擴展點。
應用緩存擴展
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(群發言僅限技術討論,拒絕閑聊,拒絕醬油,拒絕廣告)
- 如果你想與我共同來完成這個開源項目,可以隨時聯系我。
擴展閱讀
- Tracing and Caching Provider Wrappers for Entity Framework
- Entity Framework - Second Level Caching with DbContext
- Entity Framework 緩存處理與日志監控
- 數據層擴展包EFCachingProvider 總結
系列導航
- MVC實用架構設計(〇)——總體設計
- MVC實用架構設計(一)——項目結構搭建
- MVC實用架構設計(二)——使用MEF實用IOC
- MVC實用架構設計(三)——EF-Code First(1):Repository,UnitOfWork,DbContext
- MVC實用架構設計(三)——EF-Code First(2):實體映射、數據遷移,重構
- MVC實用架構設計(三)——EF-Code First(3):使用T4模板生成相似代碼
- MVC實用架構設計(三)——EF-Code First(4):數據查詢
- MVC實用架構設計(三)——EF-Code First(5):二級緩存
- MVC實體架構設計(三)——EF-Code First(6):數據更新
- 未完待續。。。