本文3.0版本文章
代碼已上傳Github+Gitee,文末有地址
書說上文《從壹開始前后端分離【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之十 || AOP面向切面編程淺解析:簡單日志記錄 + 服務切面緩存》,昨天咱們說到了AOP面向切面編程,簡單的舉出了兩個栗子,不知道大家有什么想法呢,不知道是否與傳統的緩存的使用有做對比了么?
傳統的緩存是在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、在AOP攔截器中進行過濾
添加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層中增加緩存特性
在指定的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()方法,帶有緩存特性的才可以
當然,這里還有一個小問題,就是所有的方法還是走的切面,只是增加了過濾驗證,大家也可以直接把那些需要的注入,不需要的干脆不注入Autofac容器,我之所以需要都經過的目的,就是想把它和日志結合,用來記錄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服務(可以設置為開機自動啟動)
還有要看Redis服務是否開啟
更新:這里有個小插曲,如果你第一次使用,可以修改下 Redis 的默認端口 6079 ,之前有報導說可能存在被攻擊的可能性,不過個人開發,我感覺無可厚非。知道有這個事兒即可。
四、創建appsettings.json數據獲取類
如果你對.net 獲取app.config或者web.config得心應手的話,在.net core中就稍顯吃力,因為不支持直接對Configuration的操作,
1、appsettings.json文件配置參數
前幾篇文章中有一個網友說了這樣的方法,在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" } }
當然,我受到他的啟發,簡單做了下處理,大家看看是否可行
1、創建 appsettings 幫助類
在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、運行項目,查看效果
五、基於Controller的Redis緩存
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、定義Redis接口和實現類
在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
3、將Redis服務注入到容器中,並在Controller中調用
將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、運行,執行Redis緩存,看到結果
六、基於AOP的Redis緩存
旁白:這一塊終於解決了,時間大概經過了4個月,終於被群里的小伙伴@JoyLing 給解決了,我個人感覺還是很不錯的,這里記錄一下:
1、核心:Redis緩存切面攔截器
在上篇文章中,我們已經定義過了一個攔截器,只不過是基於內存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,不知道他博客園地址。
七、CODE
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core