從壹開始前後端分離【 .NET Core2.0 +Vue2.0 】框架之十一 || AOP自定義篩選,Redis入門 11.1
大神留步
先說下一個窩心的問題,求大神幫忙,如何在AOP中去使用泛型,有償幫助,這裡謝謝,文末有詳細問題說明,可以留言或者私信都可以。
當然我也會一直思考,大家持續關注本帖,如果我想到好辦法,會及時更新,並通知大家。
程式碼已上傳Github+Gitee,文末有地址
傳統的快取是在Controller中,將獲取到的資料手動處理,然後當另一個controller中又使用的時候,還是Get,Set相關操作,當然如果小專案,有兩三個快取還好,如果是特別多的介面呼叫,面向Service服務層還是很有必要的,不需要額外寫多餘程式碼,只需要正常調取Service層的介面就行,AOP結合Autofac注入,會自動的查詢,然後返回資料,不繼續往下走Repository倉儲了。
昨天我釋出文章後,有一個網友提除了一個問題,他想的很好,就是如果面向到了Service層,那BaseService中的CURD等基本方法都被注入了,這樣會造成太多的代理類,不僅沒有必要,甚至還有問題,比如把Update也快取了,這個就不是很好了,嗯,我也發現了這個問題,所以需要給AOP增加驗證特性,只針對Service服務層中特定的常使用的方法資料進行快取等。這樣既能保證切面快取的高效性,又能手動控制,不知道大家有沒有其他的好辦法,如果有的話,歡迎留言,或者加群咱們一起討論,一起解決平時的問題。
零、今天完成的大紅色部分
一、給快取增加驗證篩選特性
1、在解決方案中新增新專案Blog.Core.Common,然後在該Common類庫中新增 特性資料夾 和 特性實體類,以後特性就在這裡
//CachingAttribute
/// <summary> /// 這個Attribute就是使用時候的驗證,把它新增到要快取資料的方法中,即可完成快取的操作。注意是對Method驗證有效 /// </summary> [AttributeUsage(AttributeTargets.Method, Inherited = true)] public class CachingAttribute : Attribute { //快取絕對過期時間 public int AbsoluteExpiration { get; set; } = 30; }
2、新增Common程式集引用,然後修改快取AOP類方法 BlogCacheAOP=》Intercept,簡單對方法的方法進行判斷
//qCachingAttribute 程式碼
//Intercept方法是攔截的關鍵所在,也是IInterceptor介面中的唯一定義 public void Intercept(IInvocation invocation) { var method = invocation.MethodInvocationTarget ?? invocation.Method; //對當前方法的特性驗證 var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; //如果需要驗證 if (qCachingAttribute != null) { //獲取自定義快取鍵 var cacheKey = CustomCacheKey(invocation); //根據key獲取相應的快取值 var cacheValue = _cache.Get(cacheKey); if (cacheValue != null) { //將當前獲取到的快取值,賦值給當前執行方法 invocation.ReturnValue = cacheValue; return; } //去執行當前的方法 invocation.Proceed(); //存入快取 if (!string.IsNullOrWhiteSpace(cacheKey)) { _cache.Set(cacheKey, invocation.ReturnValue); } } else { invocation.Proceed();//直接執行被攔截方法 } }
可見在invocation引數中,包含了幾乎所有的方法,大家可以深入研究下,獲取到自己需要的資料
3、在制定的Service層中的某些類的某些方法上增加特性(一定是方法,不懂的可以看定義特性的時候AttributeTargets.Method)
/// <summary> /// 獲取部落格列表 /// </summary> /// <param name="id"></param> /// <returns></returns> [Caching(AbsoluteExpiration = 10)]//增加特性 public async Task<List<BlogArticle>> getBlogs() { var bloglist = await dal.Query(a => a.bID > 0, a => a.bID); return bloglist; }
4、執行專案,打斷點,就可以看到,普通的Query或者CURD等都不繼續快取了,只有咱們特定的 getBlogs()方法,帶有快取特性的才可以
5、當然,這裡還有一個小問題,就是所有的方法還是走的切面,只是增加了過濾驗證,大家也可以直接把那些需要的注入,不需要的乾脆不注入容器,我之所以需要都經過的目的,就是想把它和日誌結合,用來記錄Service層的每一個請求,包括CURD的呼叫情況。
二、什麼是Redis,為什麼使用它
我個人有一個理解,關於Session或Cache等,在普通單伺服器的專案中,很簡單,有自己的生命週期等,想獲取Session就獲取,想拿啥就拿傻,但是在大型的分散式叢集中,有可能這一秒的點選的頁面和下一秒的都不在一個伺服器上,對不對!想想如果普通的辦法,怎麼保證session的一致性,怎麼獲取相同的快取資料,怎麼有效的進行訊息佇列傳遞?
這個時候就用到了Redis,這些內容,網上已經到處都是,但是還是做下記錄吧
Redis是一個key-value儲存系統。和Memcached類似,它支援儲存的value型別相對更多,包括string(字串)、list(連結串列)、set(集合)、zset(sorted set --有序集合)和hash(雜湊型別)。這些資料型別都支援push/pop、add/remove及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。它內建複製、Lua指令碼、LRU收回、事務以及不同級別磁碟持久化功能,同時通過Redis Sentinel提供高可用,通過Redis Cluster提供自動分割槽。在此基礎上,Redis支援各種不同方式的排序。為了保證效率,資料都是快取在記憶體中。區別的是redis會週期性的把更新的資料寫入磁碟或者把修改操作寫入追加的記錄檔案,並且在此基礎上實現了master-slave(主從)同步。
也就是說,快取伺服器如果意外重啟了,資料還都在,嗯!這就是它的強大之處,不僅在記憶體高吞吐,還能持久化。
Redis支援主從同步。資料可以從主伺服器向任意數量的從伺服器上同步,從伺服器可以是關聯其他從伺服器的主伺服器。這使得Redis可執行單層樹複製。存檔可以有意無意的對資料進行寫操作。由於完全實現了釋出/訂閱機制,使得從資料庫在任何地方同步樹時,可訂閱一個頻道並接收主伺服器完整的訊息釋出記錄。同步對讀取操作的可擴充套件性和資料冗餘很有幫助。
Redis也是可以做為訊息佇列的,與之相同功能比較優秀的就是Kafka
Redis還是有自身的缺點:
Redis只能儲存key/value型別,雖然value的型別可以有多種,但是對於關聯性的記錄查詢,沒有Sqlserver、Oracle、Mysql等關係資料庫方便。
Redis記憶體資料寫入硬碟有一定的時間間隔,在這個間隔內資料可能會丟失,雖然後續會介紹各種模式來保證資料丟失的可能性,但是依然會有可能,所以對資料有嚴格要求的不建議使用Redis做為資料庫。
關於Redis的時候,看到網上一個流程圖:
1、儲存資料不經常變化
2、如果資料經常變化,就需要取操作Redis和持久化資料層的動作了,保證所有的都是最新的,實時更新Redis 的key到資料庫,data到Redis中,但是要注意高併發
三、Redis的安裝和除錯使用
1.下載最新版redis,選擇.msi安裝版本,或者.zip免安裝 (我這裡是.msi安裝)
2.雙擊執行.msi檔案,一路next,中間有一個需要註冊服務,因為如果不註冊的話,把啟動的Dos視窗關閉的話,Redis就中斷連線了。
3.如果你是免安裝的,需要執行以下語句
啟動命令:redis-server.exe redis.windows.conf
註冊服務命令:redis-server.exe --service-install redis.windows.conf
去服務列表查詢服務,可以看到redis服務預設沒有開啟,開啟redis服務(可以設定為開機自動啟動)
四、建立appsettings.json資料獲取類
如果你對.net 獲取app.config或者web.config得心應手的話,在.net core中就稍顯吃力,因為不支援直接對Configuration的操作
前幾篇文章中有一個網友說了這樣的方法,在Starup.cs中的ConfigureServices方法中,新增
Blog.Core.Repository.BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value;
當然這是可行的,只不過,如果配置的資料很多,比如這樣的,那就不好寫了。
{ "Logging": { "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Warning" } }, "Console": { "LogLevel": { "Default": "Warning" } } }, //使用者配置資訊 "AppSettings": { //Redis快取 "RedisCaching": { "Enabled": true, "ConnectionString": "127.0.0.1:6379" }, //資料庫配置 "SqlServer": { "SqlServerConnection": "Server=.;Database=WMBlogDB;User ID=sa;Password=123;", "ProviderName": "System.Data.SqlClient" }, "Date": "2018-08-28", "Author": "Blog.Core" } }
當然,我受到他的啟發,簡單做了下處理,大家看看是否可行
0、將上面程式碼新增到appsettings.json檔案中
1、在Blog.Core.Common類庫中,新建Helper資料夾,新建Appsettings.cs操作類,然後引用 Microsoft.Extensions.Configuration.Json 的Nuget包
/// <summary> /// appsettings.json操作類 /// </summary> public class Appsettings { static IConfiguration Configuration { get; set; } static Appsettings() { //ReloadOnChange = true 當appsettings.json被修改時重新載入 Configuration = new ConfigurationBuilder() .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true }) .Build(); } /// <summary> /// 封裝要操作的字元 /// </summary> /// <param name="sections"></param> /// <returns></returns> public static string app(params string[] sections) { try { var val = string.Empty; for (int i = 0; i < sections.Length; i++) { val += sections[i] + ":"; } return Configuration[val.TrimEnd(':')]; } catch (Exception) { return ""; } } }
2、如何使用呢,直接引用類庫,傳遞想要的引數就行(這裡對引數是有順序要求的,這個順序就是json檔案中的層級)
/// <summary> /// 獲取部落格列表 /// </summary> /// <returns></returns> [HttpGet] [Route("GetBlogs")] public async Task<List<BlogArticle>> GetBlogs() { var connect=Appsettings.app(new string[] { "AppSettings", "RedisCaching" , "ConnectionString" });//按照層級的順序,依次寫出來 return await blogArticleServices.getBlogs(); }
3、注意:!!把appsettings.json檔案新增到bin生成檔案中!!
如果直接執行,會報錯,提示沒有許可權,
操作:右鍵appsettings.json =》 屬性 =》 Advanced =》 複製到輸出資料夾 =》 永遠複製 =》應用,儲存
4、這個時候執行專案,就可以看到結果了
五、建立Redis快取介面以及類,並在Controller中測試
1、在Blog.Core.Common的Helper資料夾中,新增SerializeHelper.cs 物件序列化操作,以後再擴充套件
public class SerializeHelper { /// <summary> /// 序列化 /// </summary> /// <param name="item"></param> /// <returns></returns> public static byte[] Serialize(object item) { var jsonString = JsonConvert.SerializeObject(item); return Encoding.UTF8.GetBytes(jsonString); } /// <summary> /// 反序列化 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="value"></param> /// <returns></returns> public static TEntity Deserialize<TEntity>(byte[] value) { if (value == null) { return default(TEntity); } var jsonString = Encoding.UTF8.GetString(value); return JsonConvert.DeserializeObject<TEntity>(jsonString); } }
2、在Blog.Core.Common類庫中,新建Redis資料夾,並新建IRedisCacheManager介面和RedisCacheManager類,並引用Nuget包StackExchange.Redis
public interface IRedisCacheManager { /// <summary> /// 獲取 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="key"></param> /// <returns></returns> TEntity Get<TEntity>(string key); //設定 void Set(string key, object value, TimeSpan cacheTime); //判斷是否存在 bool Get(string key); //移除 void Remove(string key); //清除 void Clear(); }
因為在開發的過程中,通過ConnectionMultiplexer頻繁的連線關閉服務,是很佔記憶體資源的,所以我們使用單例模式來實現
public class RedisCacheManager : IRedisCacheManager { private readonly string redisConnenctionString; public volatile ConnectionMultiplexer redisConnection; private readonly object redisConnectionLock = new object(); public RedisCacheManager() { string redisConfiguration = Appsettings.app(new string[] { "AppSettings", "RedisCaching", "ConnectionString" });//獲取連線字串 if (string.IsNullOrWhiteSpace(redisConfiguration)) { throw new ArgumentException("redis config is empty", nameof(redisConfiguration)); } this.redisConnenctionString = redisConfiguration; this.redisConnection = GetRedisConnection(); } /// <summary> /// 核心程式碼,獲取連線例項 /// 通過雙if 夾lock的方式,實現單例模式 /// </summary> /// <returns></returns> private ConnectionMultiplexer GetRedisConnection() { //如果已經連線例項,直接返回 if (this.redisConnection != null && this.redisConnection.IsConnected) { return this.redisConnection; } //加鎖,防止非同步程式設計中,出現單例無效的問題 lock (redisConnectionLock) { if (this.redisConnection != null) { //釋放redis連線 this.redisConnection.Dispose(); } this.redisConnection = ConnectionMultiplexer.Connect(redisConnenctionString); } return this.redisConnection; } /// <summary> /// 清除 /// </summary> public void Clear() { foreach (var endPoint in this.GetRedisConnection().GetEndPoints()) { var server = this.GetRedisConnection().GetServer(endPoint); foreach (var key in server.Keys()) { redisConnection.GetDatabase().KeyDelete(key); } } } /// <summary> /// 判斷是否存在 /// </summary> /// <param name="key"></param> /// <returns></returns> public bool Get(string key) { return redisConnection.GetDatabase().KeyExists(key); } /// <summary> /// 獲取 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="key"></param> /// <returns></returns> public TEntity Get<TEntity>(string key) { var value = redisConnection.GetDatabase().StringGet(key); if (value.HasValue) { //需要用的反序列化,將Redis儲存的Byte[],進行反序列化 return SerializeHelper.Deserialize<TEntity>(value); } else { return default(TEntity); } } /// <summary> /// 移除 /// </summary> /// <param name="key"></param> public void Remove(string key) { redisConnection.GetDatabase().KeyDelete(key); } /// <summary> /// 設定 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="cacheTime"></param> public void Set(string key, object value, TimeSpan cacheTime) { if (value != null) { //序列化,將object值生成RedisValue redisConnection.GetDatabase().StringSet(key, SerializeHelper.Serialize(value), cacheTime); } } }
程式碼還是很簡單的,網上都有很多資源,就是普通的新增,獲取
3、將redis介面和類 在ConfigureServices中 進行注入,(注意是建構函式注入)然後在controller中新增程式碼測試
services.AddScoped<IRedisCacheManager, RedisCacheManager>();
IAdvertisementServices advertisementServices; IBlogArticleServices blogArticleServices; IRedisCacheManager redisCacheManager;//Reids快取 /// <summary> /// 建構函式 /// </summary> /// <param name="advertisementServices"></param> /// <param name="blogArticleServices"></param> /// <param name="redisCacheManager"></param> public BlogController(IAdvertisementServices advertisementServices, IBlogArticleServices blogArticleServices, IRedisCacheManager redisCacheManager) { this.advertisementServices = advertisementServices; this.blogArticleServices = blogArticleServices; this.redisCacheManager = redisCacheManager; }
/// <summary> /// 獲取部落格列表 /// </summary> /// <returns></returns> [HttpGet] [Route("GetBlogs")] public async Task<List<BlogArticle>> GetBlogs() { var connect=Appsettings.app(new string[] { "AppSettings", "RedisCaching" , "ConnectionString" });//按照層級的順序,依次寫出來 List<BlogArticle> blogArticleList = new List<BlogArticle>(); if (redisCacheManager.Get<object>("Redis.Blog") != null) { blogArticleList = redisCacheManager.Get<List<BlogArticle>>("Redis.Blog"); } else { blogArticleList = await blogArticleServices.Query(d => d.bID > 5); redisCacheManager.Set("Redis.Blog", blogArticleList, TimeSpan.FromHours(2));//快取2小時 } return blogArticleList; }
4、執行,執行Redis快取,看到結果
六、心結
今天的講解就到裡了,是不是有一種草草收場的感覺,是的!本來後來應該最後一節。細心的你應該發現了,我們是在controller進行測試,Redis快取的是List泛型,但是呢,AOP切面快取還是基於記憶體快取,昨天我本想合併下,奈何AOP切面中如何使用泛型,如何把字串給反序列化成泛型list,鄙人表示不是很懂,希望看到的大神幫忙解決下,
如何把在AOP的攔截器中,使用泛型,將字串給反序列化,有償服務。感謝!
- 更新,關於上問題 2018-09-09
今天呢,趁著這個週末研究了研究,簡單梳理了這個需求——在AOP切面的攔截器中,第一次把List<T>序列化成字串,或者陣列存到Redis中,然後在有效期內,下一次訪問的時候,把存下來的字串或者陣列通過反序列化成相應的泛型list 或者 實體類,然後返回給方法。但是呢,現在我就卡在了,如何在AOP使用泛型。我想到了一個不是辦法的辦法,不好,所以只是記錄用,給看到的大家有一個啟發。
由上圖大家也能看得出來,我使用的是基類object,然後Task,來進行反序列化的,存的時候還是正常的存,這樣雖然能存進去,也能取出來,但是取出來的是一個object型別,然後只能在service中,將返回型別改成object,這樣的
public interface IBlogArticleServices :IBaseServices<BlogArticle> { //無法轉換 Task<List<BlogArticle>> getBlogs(); //可以轉換,但是是object型別 Task<object> getBlogDetails(int id); object getBlogs2(); }
雖然這樣也是實現了基於AOP的Redis快取,但是是犧牲返回型別的代價,感覺不是很值得,程式碼暫時不上傳到Git了,大家可以到QQ群了一起討論。