ABP入門系列目錄——學習Abp框架之實操演練
源碼路徑:Github-LearningMpaAbp
1. 引言
創建任務時我們需要指定分配給誰,Demo中我們使用一個下拉列表用來顯示當前系統的所有用戶,以供用戶選擇。我們每創建一個任務時都要去數據庫取一次用戶列表,然后綁定到用戶下拉列表顯示。如果就單單對一個demo來說,這樣實現也無可厚非,但是在正式項目中,顯然是不合理的,浪費程序性能,有待優化。
說到優化,你肯定立馬就想到了使用緩存。是的,緩存是提高程序性能的高效方式之一。
這一節我們就針對這一案例來看一看Abp中如何使用緩存來提高程序性能。
2. Abp的緩存機制
在直接使用緩存之前,我們還是來簡單梳理下Abp的緩存機制。
Abp之所以能成為一個優秀的DDD框架,我想跟作者詳細的文檔有很大關系,
作者已經在ABP官方文檔介紹了如何使用Caching,英文水平好的就直接看官方的吧。
Abp對緩存進行抽象定義了ICache
接口,位於Abp.Runtime.Caching
命名空間。
並對ICache
提供了默認的實現AbpMemoryCache
,AbpMemoryCache
是基於MemoryCache的一種實現方式。MemoryCache
是微軟的一套緩存機制,定義在System.Runtime.Caching
命名空間,顧名思義 ,在內存中進行高速緩存。我們通過類型依賴圖來看下Abp對Cache的實現:
從圖中可以看出主要包括四個部分:
- ICache->CacheBase->AbpMemoryCache:對緩存的抽象以及實現;
- ITypedCache:緩存的泛型實現;
- ICacheManager->CacheManagerBase->AbpMemoryCacheManager:緩存管理類的抽象和實現,代碼中可以通過注入ICacheManager來獲取緩存;
- ICachingConfiguration->CachingConfiguration:用來配置使用哪種緩存。
3. Abp緩存實操演練
3.1. 定位優化點
定位到我們的TasksController
,其中有兩種創建Task的Action,代碼如下:
public PartialViewResult RemoteCreate() {
var userList = _userAppService.GetUsers();
ViewBag.AssignedPersonId = new SelectList(userList.Items, "Id", "Name");
return PartialView("_CreateTaskPartial");
}
[ChildActionOnly]
public PartialViewResult Create() {
var userList = _userAppService.GetUsers();
ViewBag.AssignedPersonId = new SelectList(userList.Items, "Id", "Name");
return PartialView("_CreateTask");
}
可以看到兩個方法都需要調用_userAppService.GetUsers();
來獲取用戶列表。
現在我們來使用緩存技術對其優化。首先我們應該想到了Asp.net mvc自帶的一套緩存機制,OutputCache。
3.2. 使用[OutputCache]進行緩存
如果對OutputCache不了解,可以參考我的這篇文章Asp.net mvc 知多少(九)。
我們可以簡單在Action上添加[OutputCache]特性即可。
[OutputCache(Duration = 1200, VaryByParam = "none")]
[ChildActionOnly]
public PartialViewResult Create() {
var userList = _userAppService.GetUsers();
ViewBag.AssignedPersonId = new SelectList(userList.Items, "Id", "Name");
return PartialView("_CreateTask");
}
[OutputCache(Duration = 1200, VaryByParam = "none")]
這句代碼的意思是該action只緩存1200s。1200s后,ASP.NET MVC會重新執行action並再次緩存。因為是在[ChildActionOnly]中使用[OutputCache],所以該緩存屬於Donut Hole caching。
在該方法內部打個斷點,測試只有第一次調用會進入方法內部,之后1200s內都不會再進入該方法,1200s后會再次進入,說明緩存成功!
3.3. 使用ICacheManager進行緩存
按照上面對Abp緩存機制的梳理,我們可以在需要使用緩存的地方注入ICacheManager
來進行緩存管理。
現在我們就在TasksController中注入ICacheManager
。
申明私有變量,並在構造函數中注入,代碼如下:
private readonly ITaskAppService _taskAppService;
private readonly IUserAppService _userAppService;
private readonly ICacheManager _cacheManager;
public TasksController(ITaskAppService taskAppService, IUserAppService userAppService, ICacheManager _cacheManager) {
_taskAppService = taskAppService;
_userAppService = userAppService;
_cacheManager = cacheManager;
}
下面修改RemoteCreate
action如下:
public PartialViewResult RemoteCreate()
{
var userList = _cacheManager.GetCache("ControllerCache").Get("AllUsers",
() => _userAppService.GetUsers()) as ListResultDto<UserListDto>;
ViewBag.AssignedPersonId = new SelectList(userList.Items, "Id", "Name");
return PartialView("_CreateTaskPartial");
}
分析代碼發現我們在通過上面代碼中獲取的緩存是需要進行類型轉換的。原來_cacheManager.GetCache
返回的是ICache
類型,而ICache
定義key-value
對應的是string-object
類型,所以自然從緩存獲取完數據后要進行類型轉換了(注:最新Abp版本為ICache
提供了擴展方法,不再需要顯示進行類型轉換)。那有沒有泛型版本?聰明如你,作者對ICache
進行包裝封裝了個ITypedCache
以實現類型安全。代碼種進行了5種實現,可以一探究竟:
public PartialViewResult RemoteCreate()
{
//1.1 注釋該段代碼,使用下面緩存的方式
//var userList = _userAppService.GetUsers();
//1.2 同步調用異步解決方案(最新Abp創建的模板項目已經去掉該同步方法,所以可以通過下面這種方式獲取用戶列表)
//var userList = AsyncHelper.RunSync(() => _userAppService.GetUsersAsync());
//1.3 緩存版本
var userList = _cacheManager.GetCache("ControllerCache").Get("AllUsers", () => _userAppService.GetUsers());
//1.4 轉換為泛型版本
//var userList = _cacheManager.GetCache("ControllerCache").AsTyped<string, ListResultDto<UserListDto>>().Get("AllUsers", () => _userAppService.GetUsers());
//1.5 泛型緩存版本
//var userList = _cacheManager.GetCache<string, ListResultDto<UserListDto>>("ControllerCache").Get("AllUsers", () => _userAppService.GetUsers());
ViewBag.AssignedPersonId = new SelectList(userList.Items, "Id", "Name");
return PartialView("_CreateTaskPartial");
}
經測試,用戶列表正確緩存。
與[OutputCache]相比,我們很自然就會問Abp提供的緩存怎么沒有配置緩存過期時間,你想到的框架肯定也想到了,Abp的默認緩存過期時間是60mins,我們可以通過在使用緩存項目的Module(模塊)中自定義緩存時間。
因為我們是在Web項目中使用的Cache,所以定位到XxxWebModule.cs
,在PreInitialize
方法中進行緩存配置。
//配置所有Cache的默認過期時間為2小時
Configuration.Caching.ConfigureAll(cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);
});
//配置指定的Cache過期時間為10分鍾
Configuration.Caching.Configure("ControllerCache", cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromMinutes(10);
});
3.4. 使用IEntityCache對實體進行緩存
3.4.1. 緩存方式的思考
上面的兩種緩存方式,我們一般用於存儲自定義緩存,但有一個局限性,受到具體緩存過期時間的限制。
思考一下,我們緩存的用戶列表,它是一個實時會變化的集合,而這個實時是不定時的,可能1mins之內就有新用戶注冊,也有可能幾天沒有用戶注冊(比如我們這個Demo),這個時候就不好設置緩存過期(刷新)時間。
但由於我們是Demo性質只是為了演示用法,所以我們設定緩存過期時間為10mins也無可厚非。
那有沒有一種緩存機制,不需要設置緩存過期時間,當數據變化的時候就能自動重新緩存呢?
答案是肯定的,Abp為我們提供了IEntityCache
,實體緩存機制。
當我們需要通過ID獲取實體數據而又不想經常去數據庫查詢時,我們就可以使用IEntityCache
。
換句話說,IEntityCache
支持按實體Id進行動態緩存。
3.4.2. IEntityCache緩存原理
在演示具體操作之前,我們先來講解下IEntityCache
的緩存原理:
- 首先它第一次從數據庫中獲取實體,然后后續調用將會從緩存獲取。
- 當實體更新或刪除時它自動將緩存的實體置為無效狀態,因此它將會再下一次請求中從數據庫中重新獲取。
- 它使用緩存的類的完整類名作為緩存名稱,可以通過為構造函數傳參來修改緩存名稱。
- 它是線程安全的。
- 它使用IObjectMapper將實體映射到緩存項。 IObjectMapper由AutoMapper模塊實現。所以,如果你使用它,你需要AutoMapper模塊。您可以覆蓋MapToCacheItem方法以手動將實體映射到緩存項。
3.4.3. IEntityCache上手實戰
既然是緩存實體,基於我們這個demo,我們就拿Task實體玩一下吧。
在這里我們先要復習下什么是DTO,重申下DDD為什么引入DTO。
Data Transfer Objects(DTO)用來在應用層和展現層之間傳輸數據。
DTO的必要性:
- 領域層的抽象
- 數據隱藏
- 序列化和延遲加載問題
那這個DTO跟要講的實體緩存有什么關系呢?
不繞彎子了,就是說實體緩存不應直接對Entity進行緩存,以避免緩存時序列化了不該序列化的對象和實體。
那具體怎么操作呢?我們就直接上Demo吧。
我們定義一個TaskCacheItem
,用來緩存Title、Description、State。並定義映射規則[AutoMapFrom(typeof(Task))]
。
namespace LearningMpaAbp.Tasks.Dtos
{
[AutoMapFrom(typeof(Task))]
public class TaskCacheItem
{
public string Title { get; set; }
public string Description { get; set; }
public TaskState State { get; set; }
}
}
下面我們定義一個針對TaskCacheItem
的緩存接口。
namespace LearningMpaAbp.Tasks
{
public interface ITaskCache:IEntityCache<TaskCacheItem>
{
}
}
實現ITaskCache
緩存接口:
namespace LearningMpaAbp.Tasks
{
public class TaskCache : EntityCache<Task, TaskCacheItem>, ITaskCache, ISingletonDependency
{
public TaskCache(ICacheManager cacheManager, IRepository<Task, int> repository, string cacheName = null)
: base(cacheManager, repository, cacheName)
{
}
}
}
現在,當我們需要根據TaskId獲取Title、Description、State,我們就可以通過在需要的類中注入注入ITaskCache
,來從緩存中獲取。
下面我們在ITaskAppService
中添加一個接口TaskCacheItem GetTaskFromCacheById(int taskId);
。
然后在TaskAppService
中實現它,申明變量並在構造函數注入ITaskCache
,實現定義的接口:
private readonly ITaskCache _taskCache;
/// <summary>
/// In constructor, we can get needed classes/interfaces.
/// They are sent here by dependency injection system automatically.
/// </summary>
public TaskAppService(IRepository<Task> taskRepository, IRepository<User, long> userRepository,
ISmtpEmailSenderConfiguration smtpEmialSenderConfigtion, INotificationPublisher notificationPublisher, ITaskCache taskCache)
{
_taskRepository = taskRepository;
_userRepository = userRepository;
_smtpEmialSenderConfig = smtpEmialSenderConfigtion;
_notificationPublisher = notificationPublisher;
_taskCache = taskCache;
}
public TaskCacheItem GetTaskFromCacheById(int taskId)
{
return _taskCache[taskId];
}
測試如下,直接在即時窗口調用方法,發現只有一條Sql查詢生成,說明實體緩存成功。
可能讀到這里,你可能會問,說好的『Redis緩存用起來』,你講了半天,跟Redis沒有半毛錢關系啊。
Redis這么厲害的技能,當然要壓軸出場啊,下面Redis開講。
4. Redis是什么玩意
Redis 是一個開源(BSD許可)的,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。它支持多種類型的數據結構,如字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)與范圍查詢、bitmaps、hyperloglogs和地理空間(geospatial)索引半徑查詢。
官方的解釋就是這么拗口,對於初識Redis,我們可以簡單把它理解為基於內存的速度非常快性能非常棒的Key-Value數據庫。
有一點需要說明,Redis官方僅支持Linux系統不支持Windows系統。
但是呢,微軟大法好啊,微軟開源技術團隊(Microsoft Open Tech group)開發和維護了一個Win64 的版本,我們可以在https://github.com/MSOpenTech/redis上下載Win64版本來玩一玩。
5. 動手試玩Redis
5.1. 安裝Redis
打開微軟開源技術團隊維護的Redis Github鏈接,找到Releases目錄,下載最新版本的msi安裝即可。
下載后,一直下一步安裝即可。
5.2. 簡單試玩
找到安裝目錄,打開cmd並進入到安裝目錄,輸入redis-server redis.windows.conf
,即可啟動Redis 服務。Redis服務默認啟動在6379
端口。
再啟動一個cmd窗口,執行redis-cli.exe
即可開一個Redis客戶端。
執行set
命令進行緩存設置;
執行get
命令進行緩存讀取;
執行subscribe
命令進行頻道監聽;
執行publish
命令向指定頻道發布消息;
具體步驟詳參下圖:
6. ABP上試玩Redis緩存
跟着我的步伐,對Redis也算有了基本的認識,咱們下面就進入今天的壓軸主題,介紹Abp下如何使用redis進行緩存。
首先我們要知道為什么要用Redis進行緩存。
默認的緩存管理是在內存中(in-memory)進行緩存。當你有不止一個並發web服務器需要運行同一個應用程序,默認的緩存管理就不滿足你的需求。你可能需要一個分布式/中央緩存服務器來進行緩存管理,這時Redis就可以粉墨登場了。
6.1. Abp集成Redis
首先打開Web層,下載Abp.RedisCache Nuget包安裝。
修改XxxWebModule.cs
,在DependsOn特性上添加對AbpRedisCacheModule
的依賴,並在模塊的PreInitialize
方法中調用UseRedis
擴展方法,代碼如下:
[DependsOn(
typeof(LearningMpaAbpDataModule),
typeof(LearningMpaAbpApplicationModule),
typeof(LearningMpaAbpWebApiModule),
typeof(AbpWebSignalRModule),
//typeof(AbpHangfireModule), - ENABLE TO USE HANGFIRE INSTEAD OF DEFAULT JOB MANAGER
typeof(AbpWebMvcModule),
typeof(AbpRedisCacheModule))]
public class LearningMpaAbpWebModule : AbpModule
{
public override void PreInitialize()
{
//省略其他配置代碼
//配置使用Redis緩存
Configuration.Caching.UseRedis();
//配置所有Cache的默認過期時間為2小時
Configuration.Caching.ConfigureAll(cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);
});
//配置指定的Cache過期時間為10分鍾
Configuration.Caching.Configure("ControllerCache", cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromMinutes(10);
});
}
....
}
最后一步在Web.Config文件的【connectionStrings】節點為Abp.Redis.Cache
添加連接字符串,如下:
<connectionStrings>
<add name="Default" connectionString="Server=.\sqlexpress; Database=LearningMpaAbp; Trusted_Connection=True;" providerName="System.Data.SqlClient" />
<add name="Abp.Redis.Cache" connectionString="localhost"/>
</connectionStrings>
啟動Redis Server后,F5運行web項目,斷點調試,發現已經成功應用Redis緩存。
若未啟動Redis Server,會報Error:It was not possible to connect to the redis server(s); to create a disconnected multiplexer, disable AbortOnConnectFail. SocketFailure on PING
這樣我們就用Redis
代替了默認的MemoryCache
緩存方案,而不需要改動其它代碼,Abp就是這么簡單、靈活、松藕合!
7. 總結
這篇文章中主要梳理了Abp中如何進行緩存管理,並簡要介紹了Abp中的緩存機制,並與Asp.net mvc自帶的[Outputcache]緩存進行簡要對比,並進行了緩存管理實戰演練。最后對Redis進行了簡要介紹,並介紹了如何切換Redis緩存。