緩存在一個大型一點的系統里面是必然會涉及到的,合理的使用緩存能夠給我們的系統帶來更高的響應速度。由於數據提供服務涉及到數據庫的相關操作,如果客戶端的並發數量超過一定的數量,那么數據庫的請求處理則以爆發式增長,如果數據庫服務器無法快速處理這些並發請求,那么將會增加客戶端的請求時間,嚴重者可能導致數據庫服務或者應用服務直接癱瘓。緩存方案就是為這個而誕生,隨着緩存的引入,可以把數據庫的IO耗時操作,轉換為內存數據的快速響應操作,或者把整個頁面緩存到緩存系統里面。本篇隨筆主要介紹利用ABP框架的支持實現的服務端緩存處理和Winform客戶端緩存的處理。
1、緩存文章回顧
緩存的重要性不言而喻,我在博客園里面也寫了很多緩存相關的文章,都是基於實際系統的總結處理。
《使用ConcurrentDictionary替代Hashtable對多線程的對象緩存處理》
《在.NET項目中使用PostSharp,使用MemoryCache實現緩存的處理》
《.NET緩存框架CacheManager在混合式開發框架中的應用(1)-CacheManager的介紹和使用》
《在.NET項目中使用PostSharp,使用CacheManager實現多種緩存框架的處理》
《在Winform開發框架中下拉列表綁定字典以及使用緩存提高界面顯示速度》
《C#開發微信門戶及應用(48) - 在微信框架中整合CacheManager 緩存框架》
上面這些都是和緩存相關的內容,一般來說,緩存有很多方式的實現,如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等,為了方便我們一般使用.net的內存緩存處理,如果我們需要序列化緩存內容,那么可以采用MemoryCache或者Redis緩存等。后來我們通過綜合考慮,基於配置方式選擇不同緩存方式,在后端一般可以使用CacheManager 的緩存處理。
如下面是基於常規架構的緩存處理分層,如果是基於Web API的服務端,那么緩存一般可以在Web API層或者它的下面一層。
如果是基於可序列化的緩存處理,它在IIS或者其他Web 容器重新啟動后,緩存不會丟失,如在Redis里面,有相關的緩存記錄如下所示。
2、ABP服務端緩存處理
ABP提供了緩存的抽象,它內部使用了這個緩存抽象。雖然默認的實現使用了MemoryCache,通過配置也可以使用Redis等緩存,緩存的主要接口ICacheManager。
我們可以在應用服務層的構造函數里面,注入該接口,然后使用該接口獲得一個緩存對象。
官方簡單的應用服務層代碼如下所示。
public class TestAppService : ApplicationService { private readonly ICacheManager _cacheManager; public TestAppService(ICacheManager cacheManager) { _cacheManager = cacheManager; }
實際上,我們應用服務層應該會更加復雜一些,如下是我們ABP快速開發框架的應用服務層的代碼
[AbpAuthorize] public class DictDataAppService : MyAsyncServiceBase<DictData, DictDataDto, string, DictDataPagedDto, CreateDictDataDto, DictDataDto>, IDictDataAppService { /// <summary> /// 緩存管理接口 /// </summary> private readonly ICacheManager _cacheManager; private readonly IRepository<DictData, string> _repository; public DictDataAppService(IRepository<DictData, string> repository, ICacheManager cacheManager) : base(repository) { _repository = repository; _cacheManager = cacheManager;//依賴注入緩存 }
對於字典模塊,我們一般獲取接口如下所示。
/// <summary> /// 根據字典類型ID獲取所有該類型的字典列表集合(Key為名稱,Value為值) /// </summary> /// <param name="dictTypeId">字典類型ID</param> /// <returns></returns> public async Task<Dictionary<string, string>> GetDictByTypeID(string dictTypeId) { IList<DictData> list = await Repository.GetAllListAsync(s => s.DictType_ID == dictTypeId); Dictionary<string, string> dict = new Dictionary<string, string>(); foreach (DictData info in list) { if (!dict.ContainsKey(info.Name)) { dict.Add(info.Name, info.Value); } } return dict; }
如果我們需要把它構建一個緩存接口,那么處理方式就是對它進行一個簡單包裝即可,如下代碼所示。
/// <summary> /// 根據字典類型ID獲取所有該類型的字典列表集合(使用緩存) /// </summary> /// <param name="dictTypeId">字典類型ID</param> /// <returns></returns> public async Task<Dictionary<string, string>> GetDictByTypeIDCached(string dictTypeId) { //系統緩存默認為60分鍾,可以在模塊中配置具體的時間,配置后則是具體配置時間 return await _cacheManager.GetCache("DictDataAppService") .GetAsync(dictTypeId, () => GetDictByTypeID(dictTypeId)); }
默認緩存超時是60分鍾,它可以改。如果你超過60分鍾沒有使用緩存中的項,會從緩存中自動移除。你可以配置指定的緩存或是全部的緩存。
我們可以在應用服務層模塊類ApplicationModule類里面進行修改,實現對緩存的過期設置。
//系統緩存默認為60分鍾,可以在模塊中配置具體的時間,配置后則是具體配置時間 //所有緩存設置為2小時 Configuration.Caching.ConfigureAll(cache => { cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2); }); //特殊指定為5分鍾 Configuration.Caching.Configure("DictDataAppService", cache => { cache.DefaultSlidingExpireTime = TimeSpan.FromMinutes(5); });
Redis 緩存集成
默認緩存管理使用的是內存緩存。所以,如果你有多個並發的Web服務器使用同個應用,可能會成為一個問題,在這種情況下,你需要一個分布/集中緩存服務,你就可以簡單的使用Redis做為你的緩存服務器。
Redis是一個開源的使用ANSI C語言編寫、支持網絡、可基於內存亦可持久化的日志型、Key-Value數據庫,和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set --有序集合)和hash(哈希類型)。在此基礎上,redis支持各種不同方式的排序。與memcached一樣,為了保證效率,數據都是緩存在內存中。區別的是redis會周期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件,並且在此基礎上實現了master-slave(主從)同步。
Redis的代碼遵循ANSI-C編寫,可以在所有POSIX系統(如Linux, *BSD, Mac OS X, Solaris等)上安裝運行。而且Redis並不依賴任何非標准庫,也沒有編譯參數必需添加。
下載地址:https://github.com/MSOpenTech/redis/releases下載,安裝為Windows服務即可。
安裝后作為Windows服務運行,安裝后可以在系統的服務里面看到Redis的服務在運行了,如下圖所示。
安裝好Redis后,還有一個Redis伴侶Redis Desktop Manager需要安裝,這樣可以實時查看Redis緩存里面有哪些數據,具體地址如下:http://redisdesktop.com/download
下載屬於自己平台的版本即可
下載安裝后,打開運行界面,如果我們往里面添加鍵值的數據,那么可以看到里面的數據了。
我們來看看如何在ABP框架中使用Redis緩存
我們現在應用服務層模塊里面配置好使用Redis,如下代碼所示
[DependsOn( ................ typeof(AbpRedisCacheModule) //Redis緩存加入 )] public class ApplicationModule : AbpModule { public override void PreInitialize() { ............ //使用Redis緩存 int DatabaseId = -1; int.TryParse(AppSettingConfig.GetAppSetting("RedisCache", "DatabaseId"), out DatabaseId); string connectionString = AppSettingConfig.GetAppSetting("RedisCache", "ConnectionString"); Configuration.Caching.UseRedis(options => { options.ConnectionString = connectionString; options.DatabaseId = DatabaseId; }); //系統緩存默認為60分鍾,可以在模塊中配置具體的時間,配置后則是具體配置時間 //所有緩存設置為2小時 //Configuration.Caching.ConfigureAll(cache => //{ // cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2); //}); //特殊指定為5分鍾 Configuration.Caching.Configure("DictDataAppService", cache => { cache.DefaultSlidingExpireTime = TimeSpan.FromMinutes(5); }); }
Host項目配置文件,Appsetting.json配置文件如下所示,增加RedisCache的配置節點。
使用緩存處理的應用服務層接口實現如下所示
/// <summary> /// 根據字典類型ID獲取所有該類型的字典列表集合(使用緩存) /// </summary> /// <param name="dictTypeId">字典類型ID</param> /// <returns></returns> public async Task<Dictionary<string, string>> GetDictByTypeIDCached(string dictTypeId) { //系統緩存默認為60分鍾,可以在模塊中配置具體的時間,配置后則是具體配置時間 return await _cacheManager.GetCache("DictDataAppService").GetAsync(dictTypeId, () => GetDictByTypeID(dictTypeId)); }
在測試接口頁面中進行測試
查看緩存管理里面的內容,可以發現已經具有值了,如下所示。
這樣我們就可以很容易的從內存緩存切換到Redis的緩存了。
實體緩存
雖然ABP緩存系統出於普通的目的,但有一個EntityCache基類,可幫你緩存實體。如果我們通過它們的Id獲取的實體,我們可以用這個基類緩存它們,就不用再頻繁地從數據庫查詢。
不過這里不對這個進行細講了。
3、Winform客戶端的緩存處理
除了在服務端進行緩存測試外,為了提高客戶端的響應速度,我們還可以在Winform客戶端中使用內存緩存進行緩存一些不常變化的內容的,這樣可以避免頻繁的請求網絡接口,獲取接口數據。
ABP基礎模塊里面也提供了一個簡單的緩存類,我們可以使用它進行緩存處理。
我曾經在之前一篇隨筆《在Winform開發框架中下拉列表綁定字典以及使用緩存提高界面顯示速度》對字典模塊中使用緩存進行了說明,這個我們也可以調整為ABP快速開發框架中Winform客戶端的字典處理方式。
ABP中有兩種cache的實現方式:MemroyCache 和 RedisCache. 如下圖,兩者都繼承至ICache接口。ABP核心模塊封裝了MemroyCache 來實現ABP中的默認緩存功能。 Abp.RedisCache這個模塊封裝RedisCache來實現緩存。
我們可以在Winform客戶端中使用AbpMemoryCache是實現內存緩存的處理。
例如我們在界面模塊中使用一個字典輔助類來封裝對字典模塊的調用,同時可以使用緩存方式進行獲取。
使用緩存處理的邏輯,如下所示
主要就是判斷鍵值是否存在,否則就設置內存緩存即可。
然后在編寫一個字典控件的擴展函數,如下所示。
/// <summary> /// 綁定下拉列表控件為指定的數據字典列表 /// </summary> /// <param name="control">下拉列表控件</param> /// <param name="dictTypeName">數據字典類型名稱</param> /// <param name="defaultValue">控件默認值</param> /// <param name="emptyFlag">是否添加空行</param> public static void BindDictItems(this ComboBoxEdit control, string dictTypeName, string defaultValue, bool isCache = true, bool emptyFlag = true) { var dict = GetDictByDictType(dictTypeName, isCache); List<CListItem> itemList = new List<CListItem>(); foreach (string key in dict.Keys) { itemList.Add(new CListItem(key, dict[key])); } control.BindDictItems(itemList, defaultValue, emptyFlag); }
綁定字典控件使用的時候,就非常簡單了,如下代碼是實際項目中對字典列表綁定的操作,字典數據在字典模塊里面統一定義的。
/// <summary> /// 初始化數據字典 /// </summary> private void InitDictItem() { txtInDiagnosis.BindDictItems("入院診斷"); txtLeaveDiagnosis.BindDictItems("最后診斷"); //初始化代碼 this.txtFollowType.BindDictItems("隨訪方式"); this.txtFollowStatus.BindDictItems("隨訪狀態"); }
這樣就非常簡化了我們對字典數據源的綁定操作了,非常方便易讀,下面是其中一個功能界面的下拉列表展示。
使用緩存接口,對於大量字典數據顯示的界面,界面顯示速度有了不錯的提升。