本文將講解如何在ABP中使用Redis Cache以及使用過程中遇到的各種問題。下面就直接講解使用步驟,Redis環境的搭建請直接網上搜索。
使用步驟:
一、ABP環境搭建
- 到http://www.aspnetboilerplate.com/Templates下載一個ABP項目的模板,項目 類型選擇Angularjs+EntityFramework,項目名稱為“UsingRedisInAbp”
- 生成數據庫,並初始化基本數據。在包管理器的控制台上運行Updata-Database命令,運行時需要注意,默認項目要選中“UsingRedisInAbp.EntityFramework”,啟動項目要設置為“UsingRedisInAbp.Web”
- 在nuget里面添加對”Abp.RedisCache”的引用,我引用的0.7.8.1版本
二、替換默認的緩存管理器
修改UsingRedisInAbpApplicationModule類的代碼,主要是修改默認緩存管理器和Redis的連接字符串,修改后的完整代碼如下:
[DependsOn(typeof(UsingRedisInAbpCoreModule), typeof(AbpAutoMapperModule))] public class UsingRedisInAbpApplicationModule : AbpModule { public override void PreInitialize() { base.PreInitialize(); IocManager.Register<ICacheManager, AbpRedisCacheManager>(); //如果Redis在本機,並且使用的默認端口,下面的代碼可以不要 //Configuration.Modules.AbpRedisCacheModule().ConnectionStringKey = "KeyName"; } public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } }
三、緩存的使用
現在我們編譯一下項目,編譯通過后我們按F5運行,如果看到如下界面,表示運行成功了

下面我們看看Redis里是否有相關的緩存信息,用Redis Desktop Manager連接Redis,可以看到如下信息,說明信息已經緩存成功了

四、自定義緩存信息的讀取與設置
為了演示方便,我們以讀取文章信息為例來說明,文章信息只包含一個”Title”字段.
- 在.Core項目里添加文章類,然后在UsingRedisInAbpDbContext類里添加一個Articles屬性
- 使用Add-Migration添加信息,然后更新數據庫
- 在.Application項目里添加Caching文件夾,並在里面添加讀取和設置緩存的通用接口和實現類,實現類CacheService.cs的代碼如下:
public class CacheService : ICacheService,ISingletonDependency { public ICacheManager CacheManager { get; set; } public TValue GetCachedEntity<TKey, TValue>(TKey key) where TValue : class,IEntity<TKey> { var cache = CacheManager.GetCache<TKey, TValue>(typeof(TValue).Name); var item = cache.Get(key, () => { var repository = IocManager.Instance.Resolve<IRepository<TValue, TKey>>(); var entity = repository.FirstOrDefault(key); if (entity == null) { throw new UserFriendlyException(string.Format("讀取的信息不存在,Key:{0}",key)); } return entity; }); return item; } public TValue GetCachedEntity<TValue>(int key) where TValue : class, IEntity<int> { return GetCachedEntity<int, TValue>(key); } public void Set<TKey, TValue>(TKey key, TValue value, TimeSpan? slidingExpireTime = null) { var cache = CacheManager.GetCache<TKey, TValue>(typeof(TValue).Name); cache.Set(key, value, slidingExpireTime); } }
從緩存讀取信息的邏輯為:首先從緩存里讀取信息,如果未讀取到,則從數據庫讀取對應信息,並且將信息保存到緩存中
4.修改前端相關代碼,為了方便測試,直接在home.js和home.cshtml里添加訪問緩存信息的代碼,home.js的代碼如下
home.cs
(function() { var controllerId = 'app.views.home'; angular.module('app').controller(controllerId, [ '$scope', 'abp.services.app.cacheTest', function ($scope,service) { var vm = this; //Home logic... vm.article = {}; vm.title = ""; vm.id = 0; vm.createArticle = function() { service.createArticle({ title: vm.title }).success(function(result) { abp.notify.success("文章創建成功"); }); }; vm.getArticle = function() { service.getArticle({ id:vm.id}).success(function (result) { vm.article = result; }); }; } ]); })();
home.cshtml
<div ng-controller="app.views.home as vm"> <h1>@L("WellcomeMessage")</h1> <div class="row"> <div class=" well well-sm"> <div class="row"> <div class="col-md-6"> <input type="text" ng-model="vm.id" class="form-control" placeholder="id" required="" maxlength="32"> </div> <div class="col-md-6"> <button type="button" ng-click="vm.getArticle()" class="btn btn-primary"><i class="fa fa-sign-in"></i> 讀取緩存信息</button> </div> </div> <div> <div>{{vm.article.id}}</div> <div>{{vm.article.title}}</div> </div> </div> <div class=" well well-sm"> <div class="row"> <div class="col-md-6"> <input ng-model="vm.title" type="text" class="form-control" placeholder="文章標題" required="" maxlength="32"> </div> <div class="col-md-6"> <button type="button" ng-click="vm.createArticle()" class="btn btn-primary"><i class="fa fa-sign-in"></i> 新建文章</button> </div> </div> </div> </div> </div>
現在訪問頁面,看看能否正常添加信息與訪問緩存里的信息,我們打開首頁,可以看到能夠正常添加與訪問緩存的信息


5.現在緩存能夠正常的設置與讀取了,但是有一個很嚴重的問題,那就是重啟web服務后,無法正常加載緩存,必須要清空緩存,網站才能正常運行,報錯信息如下:
Unable to find assembly 'EntityFrameworkDynamicProxies-Abp.Zero, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

根據錯誤信息來看,應該是EntityFramework動態生成了實體類的代理類,導致反序列化失敗,通過查看ABP的源代碼我們可以知道,ABP使用的是BinaryFormatter,動態生成的實體類使用BinaryFormatter是有問題的。要解決這個問題有以下兩種方法:
A) 替換ABP的Redis緩存默認實現,不使用BinaryFormatter進行序列化,使用JSON.NET進行序列化
要替換ABP的Redis緩存默認實現修改修改3個地方
- 實現一個ICache,可以參考ABP的實現,修改序列化與反序列化的相關代碼,序列化與反序列化時需要注意,需要將原始對象包裝到RedisCacheItem中,之所以要這樣做,是因為反序列化時需要獲取原始對象的類型,如果直接反序列化為object對象,有時會直接被反序列化為JObject對象,這時就沒法直接轉換為原始對象了
- 實現一個ICacheManager,可以參考ABP的實現
- 將新實現的ICacheManager注冊到IOC中
修改后的完整代碼如下:
RedisCacheItem.cs
public class RedisCacheItem { public Type Type { get; set; } public string Item { get; set; } }
RedisCacheManager.cs
public class RedisCacheManager : CacheManagerBase { public RedisCacheManager(IIocManager iocManager, ICachingConfiguration configuration) : base(iocManager, configuration) { IocManager.RegisterIfNot<RedisCache>(DependencyLifeStyle.Transient); } protected override ICache CreateCacheImplementation(string name) { return IocManager.Resolve<RedisCache>(new { name }); } }
RedisCache.cs
public class RedisCache : CacheBase { private readonly ConnectionMultiplexer _connectionMultiplexer; private readonly AbpRedisCacheConfig _config; public IDatabase Database { get { return _connectionMultiplexer.GetDatabase(); } } public RedisCache(string name, IAbpRedisConnectionProvider redisConnectionProvider, AbpRedisCacheConfig config) : base(name) { _config = config; var connectionString = redisConnectionProvider.GetConnectionString(_config.ConnectionStringKey); _connectionMultiplexer = redisConnectionProvider.GetConnection(connectionString); } public override object GetOrDefault(string key) { var obj = Database.StringGet(GetLocalizedKey(key)); if (obj.HasValue) { var item = JsonConvert.DeserializeObject < RedisCacheItem>(obj); return JsonConvert.DeserializeObject(item.Item, item.Type); } return null; } public override void Set(string key, object value, TimeSpan? slidingExpireTime = null) { if (value == null) { throw new AbpException("Can not insert null values to the cache!"); } var cacheItem = new RedisCacheItem { Type = value.GetType(), Item = JsonConvert.SerializeObject(value) }; Database.StringSet( GetLocalizedKey(key), JsonConvert.SerializeObject(cacheItem), slidingExpireTime ); } public override void Remove(string key) { Database.KeyDelete(GetLocalizedKey(key)); } public override void Clear() { Database.KeyDeleteWithPrefix(GetLocalizedKey("*")); } private string GetLocalizedKey(string key) { return "n:" + Name + ",c:" + key; } }
UsingRedisInAbpApplicationModule.cs
[DependsOn(typeof(UsingRedisInAbpCoreModule), typeof(AbpAutoMapperModule))] public class UsingRedisInAbpApplicationModule : AbpModule { public override void PreInitialize() { base.PreInitialize(); //IocManager.Register<ICache,RedisCache>(); IocManager.Register<ICacheManager, RedisCacheManager>(); //如果Redis在本機,並且使用的默認端口,下面的代碼可以不要 //Configuration.Modules.AbpRedisCacheModule().ConnectionStringKey = "KeyName"; } public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } }
B)禁用EntityFramework的動態代理實體類生成功能
直接在UsingRedisInAbpDbContext類的構造函數中添加如下代碼
Configuration.ProxyCreationEnabled = false;
此時無論是使用第一種方式還是第二種方式,都能夠正常的讀取和設置緩存了。
兩種修改方式比較:
- 使用第一種方式時,會序列化與反序列化兩次,性能會受到一定的影響
- 使用第二種方式時,實體的導航屬性延遲加載功能會受到影響
那么有沒有一種方式可以實現只序列化一次,實體的導航屬性夠延遲加載也不受影響呢?如果一定要實現的話,可以這樣做,ABP的默認緩存實現不進行修改,只將我們自己的自定義緩存實現換成訪問Redis Cache就行。
目前的緩存還無法自動更新,下一篇將實現原始數據增刪改后,同步更新緩存的內容。
本文的完整代碼下載地址:http://files.cnblogs.com/files/loyldg/UsingRedisInAbp.src.rar
