書說上文《從壹開始先後端分離【 .NET Core2.0 Api + Vue 2.0 + AOP + 分佈式】框架之十 || AOP面向切面編程淺解析:簡單日誌記錄 + 服務切面緩存》,昨天我們說到了AOP面向切面編程,簡單的舉出了兩個栗子,不知道你們有什麼想法呢,不知道是否與傳統的緩存的使用有作對比了麼?html
傳統的緩存是在Controller中,將獲取到的數據手動處理,而後當另外一個controller中又使用的時候,仍是Get,Set相關操做,固然若是小項目,有兩三個緩存還好,若是是特別多的接口調用,面向Service服務層仍是頗有必要的,不須要額外寫多餘代碼,只須要正常調取Service層的接口就行,AOP結合Autofac注入,會自動的查找,而後返回數據,不繼續往下走Repository倉儲了。git
昨天我發佈文章後,有一個網友提出了一個問題,他想的很好,就是若是面向到了Service層,那BaseService中的CURD等基本方法都被注入了,這樣會形成太多的代理類,不只沒有必要,甚至還有問題,好比把Update也緩存了,這個就不是很好了,嗯,我也發現了這個問題,因此須要給AOP增長驗證特性,只針對Service服務層中特定的常使用的方法數據進行緩存等。這樣既能保證切面緩存的高效性,又能手動控制,不知道你們有沒有其餘的好辦法,若是有的話,歡迎留言,或者加羣我們一塊兒討論,一塊兒解決平時的問題。github
在解決方案中添加新項目Blog.Core.Common,而後在該Common類庫中添加 特性文件夾 和 特性實體類,之後特性就在這裏web
//CachingAttributeredis
/// <summary>
/// 這個Attribute就是使用時候的驗證,把它添加到要緩存數據的方法中,便可完成緩存的操做。注意是對Method驗證有效
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class CachingAttribute : Attribute { //緩存絕對過時時間 public int AbsoluteExpiration { get; set; } = 30; }
添加Common程序集引用,而後修改緩存AOP類方法 BlogCacheAOP=》Intercept,簡單對方法的方法進行判斷sql
//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參數中,包含了幾乎全部的方法,你們能夠深刻研究下,獲取到本身須要的數據編程
在指定的Service層中的某些類的某些方法上增長特性(必定是方法,不懂的能夠看定義特性的時候AttributeTargets.Method)json
/// <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; }
運行項目,打斷點,就能夠看到,普通的Query或者CURD等都不繼續緩存了,只有我們特定的 getBlogs()方法,帶有緩存特性的才能夠windows
固然,這裏還有一個小問題,就是全部的方法仍是走的切面,只是增長了過濾驗證,你們也能夠直接把那些須要的注入,不須要的乾脆不注入Autofac容器,我之因此須要都通過的目的,就是想把它和日誌結合,用來記錄Service層的每個請求,包括CURD的調用狀況。
我我的有一個理解,關於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的使用,看到網上一個流程圖:
一、保存數據不常常變化
二、若是數據常常變化,就須要取操做Redis和持久化數據層的動做了,保證全部的都是最新的,實時更新Redis 的key到數據庫,data到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服務(能夠設置爲開機自動啓動)
還有要看Redis服務是否開啓
更新:這裏有個小插曲,若是你第一次使用,能夠修改下 Redis 的默認端口 6079 ,以前有報導說可能存在被攻擊的可能性,不過我的開發,我感受無可厚非。知道有這個事兒便可。
若是你對.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" } }
固然,我受到他的啓發,簡單作了下處理,你們看看是否可行
在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 ""; } } }
如何使用呢,直接引用類庫,傳遞想要的參數就行(這裏對參數是有順序要求的,這個順序就是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(); }
若是直接運行,會報錯,提示沒有權限,
操做:右鍵appsettings.json =》 屬性 =》 Advanced =》 複製到輸出文件夾 =》 永遠複製 =》應用,保存
在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); } }
在Blog.Core.Common類庫中,新建Redis文件夾,新建IRedisCacheManager接口和RedisCacheManager類,並引用Nuget包StackExchange.Redis
namespace Blog.Core.Common
{
/// <summary>
/// Redis緩存接口
/// </summary>
public interface IRedisCacheManager { //獲取 Reids 緩存值 string GetValue(string key); //獲取值,並序列化 TEntity Get<TEntity>(string key); //保存 void Set(string key, object value, TimeSpan cacheTime); //判斷是否存在 bool Get(string key); //移除某一個緩存值 void Remove(string key); //所有清除 void Clear(); } }
由於在開發的過程當中,經過ConnectionMultiplexer頻繁的鏈接關閉服務,是很佔內存資源的,因此咱們使用單例模式來實現:
這裏要引用 Redis 依賴,如今的在線項目已經把這個類遷移到了Common 層,你們知道怎麼用就行。
添加nuget包後,而後引用
using StackExchange.Redis;
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(); } try { this.redisConnection = ConnectionMultiplexer.Connect(redisConnenctionString); } catch (Exception) { throw new Exception("Redis服務未啓用,請開啓該服務"); } } return this.redisConnection; } 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); } } } public bool Get(string key) { return redisConnection.GetDatabase().KeyExists(key); } public string GetValue(string key) { return redisConnection.GetDatabase().StringGet(key); } 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); } } public void Remove(string key) { redisConnection.GetDatabase().KeyDelete(key); }
public void Set(string key, object value, TimeSpan cacheTime) { if (value != null) { //序列化,將object值生成RedisValue redisConnection.GetDatabase().StringSet(key, SerializeHelper.Serialize(value), cacheTime); } } public bool SetValue(string key, byte[] value) { return redisConnection.GetDatabase().StringSet(key, value, TimeSpan.FromSeconds(120)); } }
代碼仍是很簡單的,網上都有不少資源,就是普通的CURD
將redis接口和類 在ConfigureServices中 進行注入,
services.AddScoped<IRedisCacheManager, RedisCacheManager>();//這裏說下,若是是本身的項目,我的更建議使用單例模式 services.AddSingleton
關於爲啥我使用了 Scoped 的,多是想多了,想到了分佈式裏邊了,這裏有個博問:Redis多實例建立鏈接開銷的一些疑問?你們本身看看就好,用單例就能夠。
注意是構造函數注入,而後在controller中添加代碼測試
/// <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個月,終於被羣裏的小夥伴@JoyLing 給解決了,我我的感受仍是很不錯的,這裏記錄一下:
在上篇文章中,咱們已經定義過了一個攔截器,只不過是基於內存Memory緩存的,並不適應於Redis,上邊我們也說到了Redis必需要存入指定的值,好比字符串,而不能將異步對象 Task<T> 保存到硬盤上,因此咱們就修改下攔截器方法,一個專門應用於 Redis 的切面攔截器:
//經過注入的方式,把Redis緩存操做接口經過構造函數注入 private IRedisCacheManager _cache; public BlogRedisCacheAOP(IRedisCacheManager cache) { _cache = cache; } //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) { //獲取自定義緩存鍵,這個和Memory內存緩存是同樣的,不細說 var cacheKey = CustomCacheKey(invocation); //核心1:注意這裏和以前不一樣,是獲取的string值,以前是object var cacheValue = _cache.GetValue(cacheKey); if (cacheValue != null) { //將當前獲取到的緩存值,賦值給當前執行方法 var type = invocation.Method.ReturnType; var resultTypes = type.GenericTypeArguments; if (type.FullName == "System.Void") { return; } object response; if (type != null && typeof(Task).IsAssignableFrom(type)) { //核心2:返回異步對象Task<T> if (resultTypes.Count() > 0) { var resultType = resultTypes.FirstOrDefault(); // 核心3,直接序列化成 dynamic 類型,以前我一直糾結特定的實體 dynamic temp = Newtonsoft.Json.JsonConvert.DeserializeObject(cacheValue, resultType); response = Task.FromResult(temp); } else { //Task 無返回方法 指定時間內不容許從新運行 response = Task.Yield(); } } else { // 核心4,要進行 ChangeType response = System.Convert.ChangeType(_cache.Get<object>(cacheKey), type); } invocation.ReturnValue = response; return; } //去執行當前的方法 invocation.Proceed(); //存入緩存 if (!string.IsNullOrWhiteSpace(cacheKey)) { object response; //Type type = invocation.ReturnValue?.GetType(); var type = invocation.Method.ReturnType; if (type != null && typeof(Task).IsAssignableFrom(type)) { var resultProperty = type.GetProperty("Result"); response = resultProperty.GetValue(invocation.ReturnValue); } else { response = invocation.ReturnValue; } if (response == null) response = string.Empty; // 核心5:將獲取到指定的response 和特性的緩存時間,進行set操做 _cache.Set(cacheKey, response, TimeSpan.FromMinutes(qCachingAttribute.AbsoluteExpiration)); } } else { invocation.Proceed();//直接執行被攔截方法 } }
上邊紅色標註的,是和以前不同的,總體結構仍是差很少的,相信都能看的懂的,最後咱們就能夠很任性的在Autofac容器中,進行任意緩存切換了,是否是很棒!
再次感受小夥伴JoyLing,不知道他博客園地址。