緩存在一個(gè)大型一點(diǎn)的系統(tǒng)里面是必然會(huì)涉及到的,合理的使用緩存能夠給我們的系統(tǒng)帶來更高的響應(yīng)速度。由于數(shù)據(jù)提供服務(wù)涉及到數(shù)據(jù)庫(kù)的相關(guān)操作,如果客戶端的并發(fā)數(shù)量超過一定的數(shù)量,那么數(shù)據(jù)庫(kù)的請(qǐng)求處理則以爆發(fā)式增長(zhǎng),如果數(shù)據(jù)庫(kù)服務(wù)器無法快速處理這些并發(fā)請(qǐng)求,那么將會(huì)增加客戶端的請(qǐng)求時(shí)間,嚴(yán)重者可能導(dǎo)致數(shù)據(jù)庫(kù)服務(wù)或者應(yīng)用服務(wù)直接癱瘓。緩存方案就是為這個(gè)而誕生,隨著緩存的引入,可以把數(shù)據(jù)庫(kù)的IO耗時(shí)操作,轉(zhuǎn)換為內(nèi)存數(shù)據(jù)的快速響應(yīng)操作,或者把整個(gè)頁面緩存到緩存系統(tǒng)里面。本篇隨筆主要介紹利用ABP框架的支持實(shí)現(xiàn)的服務(wù)端緩存處理和Winform客戶端緩存的處理。
1、緩存文章回顧
緩存的重要性不言而喻,我在博客園里面也寫了很多緩存相關(guān)的文章,都是基于實(shí)際系統(tǒng)的總結(jié)處理。
《使用ConcurrentDictionary替代Hashtable對(duì)多線程的對(duì)象緩存處理》
《在.NET項(xiàng)目中使用PostSharp,使用MemoryCache實(shí)現(xiàn)緩存的處理》
《.NET緩存框架CacheManager在混合式開發(fā)框架中的應(yīng)用(1)-CacheManager的介紹和使用》
《在.NET項(xiàng)目中使用PostSharp,使用CacheManager實(shí)現(xiàn)多種緩存框架的處理》
《在Winform開發(fā)框架中下拉列表綁定字典以及使用緩存提高界面顯示速度》
《C#開發(fā)微信門戶及應(yīng)用(48) - 在微信框架中整合CacheManager 緩存框架》
上面這些都是和緩存相關(guān)的內(nèi)容,一般來說,緩存有很多方式的實(shí)現(xiàn),如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等,為了方便我們一般使用.net的內(nèi)存緩存處理,如果我們需要序列化緩存內(nèi)容,那么可以采用MemoryCache或者Redis緩存等。后來我們通過綜合考慮,基于配置方式選擇不同緩存方式,在后端一般可以使用CacheManager 的緩存處理。
如下面是基于常規(guī)架構(gòu)的緩存處理分層,如果是基于Web API的服務(wù)端,那么緩存一般可以在Web API層或者它的下面一層。

如果是基于可序列化的緩存處理,它在IIS或者其他Web 容器重新啟動(dòng)后,緩存不會(huì)丟失,如在Redis里面,有相關(guān)的緩存記錄如下所示。

2、ABP服務(wù)端緩存處理
ABP提供了緩存的抽象,它內(nèi)部使用了這個(gè)緩存抽象。雖然默認(rèn)的實(shí)現(xiàn)使用了MemoryCache,通過配置也可以使用Redis等緩存,緩存的主要接口ICacheManager。
我們可以在應(yīng)用服務(wù)層的構(gòu)造函數(shù)里面,注入該接口,然后使用該接口獲得一個(gè)緩存對(duì)象。
官方簡(jiǎn)單的應(yīng)用服務(wù)層代碼如下所示。
public class TestAppService : ApplicationService
{
private readonly ICacheManager _cacheManager;
public TestAppService(ICacheManager cacheManager)
{
_cacheManager = cacheManager;
}
實(shí)際上,我們應(yīng)用服務(wù)層應(yīng)該會(huì)更加復(fù)雜一些,如下是我們ABP快速開發(fā)框架的應(yīng)用服務(wù)層的代碼
[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;//依賴注入緩存
}
對(duì)于字典模塊,我們一般獲取接口如下所示。
/// <summary>
/// 根據(jù)字典類型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;
}
如果我們需要把它構(gòu)建一個(gè)緩存接口,那么處理方式就是對(duì)它進(jìn)行一個(gè)簡(jiǎn)單包裝即可,如下代碼所示。
/// <summary>
/// 根據(jù)字典類型ID獲取所有該類型的字典列表集合(使用緩存)
/// </summary>
/// <param name="dictTypeId">字典類型ID</param>
/// <returns></returns>
public async Task<Dictionary<string, string>> GetDictByTypeIDCached(string dictTypeId)
{
//系統(tǒng)緩存默認(rèn)為60分鐘,可以在模塊中配置具體的時(shí)間,配置后則是具體配置時(shí)間
return await _cacheManager.GetCache("DictDataAppService")
.GetAsync(dictTypeId, () => GetDictByTypeID(dictTypeId));
}
默認(rèn)緩存超時(shí)是60分鐘,它可以改。如果你超過60分鐘沒有使用緩存中的項(xiàng),會(huì)從緩存中自動(dòng)移除。你可以配置指定的緩存或是全部的緩存。
我們可以在應(yīng)用服務(wù)層模塊類ApplicationModule類里面進(jìn)行修改,實(shí)現(xiàn)對(duì)緩存的過期設(shè)置。
//系統(tǒng)緩存默認(rèn)為60分鐘,可以在模塊中配置具體的時(shí)間,配置后則是具體配置時(shí)間
//所有緩存設(shè)置為2小時(shí)
Configuration.Caching.ConfigureAll(cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);
});
//特殊指定為5分鐘
Configuration.Caching.Configure("DictDataAppService", cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromMinutes(5);
});
Redis 緩存集成
默認(rèn)緩存管理使用的是內(nèi)存緩存。所以,如果你有多個(gè)并發(fā)的Web服務(wù)器使用同個(gè)應(yīng)用,可能會(huì)成為一個(gè)問題,在這種情況下,你需要一個(gè)分布/集中緩存服務(wù),你就可以簡(jiǎn)單的使用Redis做為你的緩存服務(wù)器。
Redis是一個(gè)開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫(kù),和Memcached類似,它支持存儲(chǔ)的value類型相對(duì)更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set --有序集合)和hash(哈希類型)。在此基礎(chǔ)上,redis支持各種不同方式的排序。與memcached一樣,為了保證效率,數(shù)據(jù)都是緩存在內(nèi)存中。區(qū)別的是redis會(huì)周期性的把更新的數(shù)據(jù)寫入磁盤或者把修改操作寫入追加的記錄文件,并且在此基礎(chǔ)上實(shí)現(xiàn)了master-slave(主從)同步。
Redis的代碼遵循ANSI-C編寫,可以在所有POSIX系統(tǒng)(如Linux, <tt>*</tt>BSD, Mac OS X, Solaris等)上安裝運(yùn)行。而且Redis并不依賴任何非標(biāo)準(zhǔn)庫(kù),也沒有編譯參數(shù)必需添加。
下載地址:https://github.com/MSOpenTech/redis/releases下載,安裝為Windows服務(wù)即可。
安裝后作為Windows服務(wù)運(yùn)行,安裝后可以在系統(tǒng)的服務(wù)里面看到Redis的服務(wù)在運(yùn)行了,如下圖所示。

安裝好Redis后,還有一個(gè)Redis伴侶Redis Desktop Manager需要安裝,這樣可以實(shí)時(shí)查看Redis緩存里面有哪些數(shù)據(jù),具體地址如下:http://redisdesktop.com/download
下載屬于自己平臺(tái)的版本即可

下載安裝后,打開運(yùn)行界面,如果我們往里面添加鍵值的數(shù)據(jù),那么可以看到里面的數(shù)據(jù)了。

我們來看看如何在ABP框架中使用Redis緩存
我們現(xiàn)在應(yīng)用服務(wù)層模塊里面配置好使用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;
});
//系統(tǒng)緩存默認(rèn)為60分鐘,可以在模塊中配置具體的時(shí)間,配置后則是具體配置時(shí)間
//所有緩存設(shè)置為2小時(shí)
//Configuration.Caching.ConfigureAll(cache =>
//{
// cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);
//});
//特殊指定為5分鐘
Configuration.Caching.Configure("DictDataAppService", cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromMinutes(5);
});
}
Host項(xiàng)目配置文件,Appsetting.json配置文件如下所示,增加RedisCache的配置節(jié)點(diǎn)。

使用緩存處理的應(yīng)用服務(wù)層接口實(shí)現(xiàn)如下所示
/// <summary>
/// 根據(jù)字典類型ID獲取所有該類型的字典列表集合(使用緩存)
/// </summary>
/// <param name="dictTypeId">字典類型ID</param>
/// <returns></returns>
public async Task<Dictionary<string, string>> GetDictByTypeIDCached(string dictTypeId)
{
//系統(tǒng)緩存默認(rèn)為60分鐘,可以在模塊中配置具體的時(shí)間,配置后則是具體配置時(shí)間
return await _cacheManager.GetCache("DictDataAppService").GetAsync(dictTypeId, () => GetDictByTypeID(dictTypeId));
}
在測(cè)試接口頁面中進(jìn)行測(cè)試

查看緩存管理里面的內(nèi)容,可以發(fā)現(xiàn)已經(jīng)具有值了,如下所示。

這樣我們就可以很容易的從內(nèi)存緩存切換到Redis的緩存了。
實(shí)體緩存
雖然ABP緩存系統(tǒng)出于普通的目的,但有一個(gè)EntityCache基類,可幫你緩存實(shí)體。如果我們通過它們的Id獲取的實(shí)體,我們可以用這個(gè)基類緩存它們,就不用再頻繁地從數(shù)據(jù)庫(kù)查詢。
不過這里不對(duì)這個(gè)進(jìn)行細(xì)講了。
3、Winform客戶端的緩存處理
除了在服務(wù)端進(jìn)行緩存測(cè)試外,為了提高客戶端的響應(yīng)速度,我們還可以在Winform客戶端中使用內(nèi)存緩存進(jìn)行緩存一些不常變化的內(nèi)容的,這樣可以避免頻繁的請(qǐng)求網(wǎng)絡(luò)接口,獲取接口數(shù)據(jù)。
ABP基礎(chǔ)模塊里面也提供了一個(gè)簡(jiǎn)單的緩存類,我們可以使用它進(jìn)行緩存處理。
我曾經(jīng)在之前一篇隨筆《在Winform開發(fā)框架中下拉列表綁定字典以及使用緩存提高界面顯示速度》對(duì)字典模塊中使用緩存進(jìn)行了說明,這個(gè)我們也可以調(diào)整為ABP快速開發(fā)框架中Winform客戶端的字典處理方式。
ABP中有兩種cache的實(shí)現(xiàn)方式:MemroyCache 和 RedisCache. 如下圖,兩者都繼承至ICache接口。ABP核心模塊封裝了MemroyCache 來實(shí)現(xiàn)ABP中的默認(rèn)緩存功能。 Abp.RedisCache這個(gè)模塊封裝RedisCache來實(shí)現(xiàn)緩存。

我們可以在Winform客戶端中使用AbpMemoryCache是實(shí)現(xiàn)內(nèi)存緩存的處理。
例如我們?cè)诮缑婺K中使用一個(gè)字典輔助類來封裝對(duì)字典模塊的調(diào)用,同時(shí)可以使用緩存方式進(jìn)行獲取。

使用緩存處理的邏輯,如下所示

主要就是判斷鍵值是否存在,否則就設(shè)置內(nèi)存緩存即可。
然后在編寫一個(gè)字典控件的擴(kuò)展函數(shù),如下所示。
/// <summary>
/// 綁定下拉列表控件為指定的數(shù)據(jù)字典列表
/// </summary>
/// <param name="control">下拉列表控件</param>
/// <param name="dictTypeName">數(shù)據(jù)字典類型名稱</param>
/// <param name="defaultValue">控件默認(rèn)值</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);
}
綁定字典控件使用的時(shí)候,就非常簡(jiǎn)單了,如下代碼是實(shí)際項(xiàng)目中對(duì)字典列表綁定的操作,字典數(shù)據(jù)在字典模塊里面統(tǒng)一定義的。
/// <summary>
/// 初始化數(shù)據(jù)字典
/// </summary>
private void InitDictItem()
{
txtInDiagnosis.BindDictItems("入院診斷");
txtLeaveDiagnosis.BindDictItems("最后診斷");
//初始化代碼
this.txtFollowType.BindDictItems("隨訪方式");
this.txtFollowStatus.BindDictItems("隨訪狀態(tài)");
}
這樣就非常簡(jiǎn)化了我們對(duì)字典數(shù)據(jù)源的綁定操作了,非常方便易讀,下面是其中一個(gè)功能界面的下拉列表展示。

使用緩存接口,對(duì)于大量字典數(shù)據(jù)顯示的界面,界面顯示速度有了不錯(cuò)的提升。