大神留步
先說下一個窩心的問題章姓,求大神幫忙佳遣,如何在Task異步編程中炭序,使用Redis存、取Task<List<T>>泛型苍日,有償幫助惭聂,這里謝謝,文末有詳細(xì)問題說明相恃,可以留言或者私信都可以辜纲。
當(dāng)然我也會一直思考,大家持續(xù)關(guān)注本帖拦耐,如果我想到好辦法耕腾,會及時更新,并通知大家杀糯。
代碼已上傳Github+Gitee扫俺,文末有地址
書說上文《從壹開始前后端分離【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之十 || AOP面向切面編程淺解析:簡單日志記錄 + 服務(wù)切面緩存》,昨天咱們說到了AOP面向切面編程固翰,簡單的舉出了兩個栗子狼纬,不知道大家有什么想法呢,不知道是否與傳統(tǒng)的緩存的使用有做對比了么骂际?
傳統(tǒng)的緩存是在Controller中疗琉,將獲取到的數(shù)據(jù)手動處理,然后當(dāng)另一個controller中又使用的時候歉铝,還是Get盈简,Set相關(guān)操作,當(dāng)然如果小項目太示,有兩三個緩存還好柠贤,如果是特別多的接口調(diào)用,面向Service服務(wù)層還是很有必要的类缤,不需要額外寫多余代碼臼勉,只需要正常調(diào)取Service層的接口就行,AOP結(jié)合Autofac注入呀非,會自動的查找坚俗,然后返回數(shù)據(jù),不繼續(xù)往下走Repository倉儲了岸裙。
昨天我發(fā)布文章后猖败,有一個網(wǎng)友提除了一個問題,他想的很好降允,就是如果面向到了Service層恩闻,那BaseService中的CURD等基本方法都被注入了,這樣會造成太多的代理類剧董,不僅沒有必要幢尚,甚至還有問題破停,比如把Update也緩存了,這個就不是很好了尉剩,嗯真慢,我也發(fā)現(xiàn)了這個問題,所以需要給AOP增加驗證特性理茎,只針對Service服務(wù)層中特定的常使用的方法數(shù)據(jù)進(jìn)行緩存等黑界。這樣既能保證切面緩存的高效性,又能手動控制皂林,不知道大家有沒有其他的好辦法朗鸠,如果有的話,歡迎留言础倍,或者加群咱們一起討論烛占,一起解決平時的問題。
零沟启、今天完成的大紅色部分
一忆家、給緩存增加驗證篩選特性
1、在解決方案中添加新項目Blog.Core.Common美浦,然后在該Common類庫中添加 特性文件夾 和 特性實體類弦赖,以后特性就在這里
//CachingAttribute
/// <summary>
/// 這個Attribute就是使用時候的驗證,把它添加到要緩存數(shù)據(jù)的方法中浦辨,即可完成緩存的操作。注意是對Method驗證有效 /// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true)] public class CachingAttribute : Attribute
{ //緩存絕對過期時間
public int AbsoluteExpiration { get; set; } = 30;
}
2沼沈、添加Common程序集引用流酬,然后修改緩存AOP類方法 BlogCacheAOP=》Intercept,簡單對方法的方法進(jìn)行判斷
//qCachingAttribute 代碼
//Intercept方法是攔截的關(guān)鍵所在列另,也是IInterceptor接口中的唯一定義
public void Intercept(IInvocation invocation)
{ var method = invocation.MethodInvocationTarget ?? invocation.Method; //對當(dāng)前方法的特性驗證
var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; //如果需要驗證
if (qCachingAttribute != null)
{ //獲取自定義緩存鍵
var cacheKey = CustomCacheKey(invocation); //根據(jù)key獲取相應(yīng)的緩存值
var cacheValue = _cache.Get(cacheKey); if (cacheValue != null)
{ //將當(dāng)前獲取到的緩存值芽腾,賦值給當(dāng)前執(zhí)行方法
invocation.ReturnValue = cacheValue; return;
} //去執(zhí)行當(dāng)前的方法
invocation.Proceed(); //存入緩存
if (!string.IsNullOrWhiteSpace(cacheKey))
{
_cache.Set(cacheKey, invocation.ReturnValue);
}
} else {
invocation.Proceed();//直接執(zhí)行被攔截方法
}
}
可見在invocation參數(shù)中,包含了幾乎所有的方法页衙,大家可以深入研究下摊滔,獲取到自己需要的數(shù)據(jù)
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等都不繼續(xù)緩存了,只有咱們特定的 getBlogs()方法廉侧,帶有緩存特性的才可以
5页响、當(dāng)然篓足,這里還有一個小問題,就是所有的方法還是走的切面闰蚕,只是增加了過濾驗證栈拖,大家也可以直接把那些需要的注入,不需要的干脆不注入容器没陡,我之所以需要都經(jīng)過的目的涩哟,就是想把它和日志結(jié)合,用來記錄Service層的每一個請求诗鸭,包括CURD的調(diào)用情況染簇。
二、什么是Redis强岸,為什么使用它
我個人有一個理解锻弓,關(guān)于Session或Cache等,在普通單服務(wù)器的項目中蝌箍,很簡單青灼,有自己的生命周期等,想獲取Session就獲取妓盲,想拿啥就拿傻杂拨,但是在大型的分布式集群中,有可能這一秒的點擊的頁面和下一秒的都不在一個服務(wù)器上悯衬,對不對弹沽!想想如果普通的辦法,怎么保證session的一致性筋粗,怎么獲取相同的緩存數(shù)據(jù)策橘,怎么有效的進(jìn)行消息隊列傳遞?
這個時候就用到了Redis娜亿,這些內(nèi)容丽已,網(wǎng)上已經(jīng)到處都是,但是還是做下記錄吧
Redis是一個key-value存儲系統(tǒng)买决。和Memcached類似沛婴,它支持存儲的value類型相對更多,包括string(字符串)督赤、list(鏈表)嘁灯、set(集合)、zset(sorted set --有序集合)和hash(哈希類型)够挂。這些數(shù)據(jù)類型都支持push/pop旁仿、add/remove及取交集并集和差集及更豐富的操作,而且這些操作都是原子性的。它內(nèi)置復(fù)制枯冈、Lua腳本毅贮、LRU收回、事務(wù)以及不同級別磁盤持久化功能尘奏,同時通過Redis Sentinel提供高可用滩褥,通過Redis Cluster提供自動分區(qū)。在此基礎(chǔ)上炫加,Redis支持各種不同方式的排序瑰煎。為了保證效率,數(shù)據(jù)都是緩存在內(nèi)存中俗孝。區(qū)別的是redis會周期性的把更新的數(shù)據(jù)寫入磁盤或者把修改操作寫入追加的記錄文件酒甸,并且在此基礎(chǔ)上實現(xiàn)了master-slave(主從)同步。
也就是說赋铝,緩存服務(wù)器如果意外重啟了插勤,數(shù)據(jù)還都在,嗯革骨!這就是它的強大之處农尖,不僅在內(nèi)存高吞吐,還能持久化良哲。
Redis支持主從同步盛卡。數(shù)據(jù)可以從主服務(wù)器向任意數(shù)量的從服務(wù)器上同步,從服務(wù)器可以是關(guān)聯(lián)其他從服務(wù)器的主服務(wù)器筑凫。這使得Redis可執(zhí)行單層樹復(fù)制滑沧。存盤可以有意無意的對數(shù)據(jù)進(jìn)行寫操作。由于完全實現(xiàn)了發(fā)布/訂閱機制巍实,使得從數(shù)據(jù)庫在任何地方同步樹時嚎货,可訂閱一個頻道并接收主服務(wù)器完整的消息發(fā)布記錄。同步對讀取操作的可擴展性和數(shù)據(jù)冗余很有幫助蔫浆。
Redis也是可以做為消息隊列的,與之相同功能比較優(yōu)秀的就是Kafka
Redis還是有自身的缺點:
Redis只能存儲key/value類型姐叁,雖然value的類型可以有多種瓦盛,但是對于關(guān)聯(lián)性的記錄查詢,沒有Sqlserver外潜、Oracle原环、Mysql等關(guān)系數(shù)據(jù)庫方便。
Redis內(nèi)存數(shù)據(jù)寫入硬盤有一定的時間間隔处窥,在這個間隔內(nèi)數(shù)據(jù)可能會丟失嘱吗,雖然后續(xù)會介紹各種模式來保證數(shù)據(jù)丟失的可能性,但是依然會有可能,所以對數(shù)據(jù)有嚴(yán)格要求的不建議使用Redis做為數(shù)據(jù)庫谒麦。
三俄讹、Redis的安裝和調(diào)試使用
**1.下載最新版redis,選擇.msi安裝版本绕德,或者.zip免安裝 **(我這里是.msi安裝)
2.雙擊執(zhí)行.msi文件患膛,一路next,中間有一個需要注冊服務(wù)耻蛇,因為如果不注冊的話踪蹬,把啟動的Dos窗口關(guān)閉的話,Redis就中斷連接了臣咖。
3.如果你是免安裝的跃捣,需要執(zhí)行以下語句
啟動命令:redis-server.exe redis.windows.conf
注冊服務(wù)命令:redis-server.exe --service-install redis.windows.conf
去服務(wù)列表查詢服務(wù),可以看到redis服務(wù)默認(rèn)沒有開啟夺蛇,開啟redis服務(wù)(可以設(shè)置為開機自動啟動)
四疚漆、創(chuàng)建appsettings.json數(shù)據(jù)獲取類
如果你對.net 獲取app.config或者web.config得心應(yīng)手的話,在.net core中就稍顯吃力蚊惯,因為不支持直接對Configuration的操作
前幾篇文章中有一個網(wǎng)友說了這樣的方法愿卸,在Starup.cs中的ConfigureServices方法中,添加
Blog.Core.Repository.BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value;
當(dāng)然這是可行的截型,只不過趴荸,如果配置的數(shù)據(jù)很多,比如這樣的宦焦,那就不好寫了发钝。
{ "Logging": { "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Warning" }
}, "Console": { "LogLevel": { "Default": "Warning" }
}
}, //用戶配置信息
"AppSettings": { //Redis緩存
"RedisCaching": { "Enabled": true, "ConnectionString": "127.0.0.1:6379" }, //數(shù)據(jù)庫配置
"SqlServer": { "SqlServerConnection": "Server=.;Database=WMBlogDB;User ID=sa;Password=123;", "ProviderName": "System.Data.SqlClient" }, "Date": "2018-08-28", "Author": "Blog.Core" }
}
當(dāng)然,我受到他的啟發(fā)波闹,簡單做了下處理酝豪,大家看看是否可行
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 當(dāng)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瘫证、如何使用呢,直接引用類庫庄撮,傳遞想要的參數(shù)就行(這里對參數(shù)是有順序要求的背捌,這個順序就是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洞斯、注意:U鼻臁!把appsettings.json文件添加到bin生成文件中!么抗!
如果直接運行毅否,會報錯,提示沒有權(quán)限乖坠,
操作:右鍵appsettings.json =》 屬性 =》 Advanced =》 復(fù)制到輸出文件夾 =》 永遠(yuǎn)復(fù)制 =》應(yīng)用搀突,保存
4、這個時候運行項目熊泵,就可以看到結(jié)果了
五仰迁、創(chuàng)建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); //設(shè)置
void Set(string key, object value, TimeSpan cacheTime); //判斷是否存在
bool Get(string key); //移除
void Remove(string key); //清除
void Clear();
}
因為在開發(fā)的過程中恰起,通過ConnectionMultiplexer頻繁的連接關(guān)閉服務(wù),是很占內(nèi)存資源的趾牧,所以我們使用單例模式來實現(xiàn)
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的方式须床,實現(xiàn)單例模式 /// </summary>
/// <returns></returns>
private ConnectionMultiplexer GetRedisConnection()
{ //如果已經(jīng)連接實例性置,直接返回
if (this.redisConnection != null && this.redisConnection.IsConnected)
{ return this.redisConnection;
} //加鎖伪冰,防止異步編程中,出現(xiàn)單例無效的問題
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[]貌亭,進(jìn)行反序列化
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>
/// 設(shè)置 /// </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);
}
}
}
代碼還是很簡單的认臊,網(wǎng)上都有很多資源圃庭,就是普通的添加,獲取
3失晴、將redis接口和類 在ConfigureServices中 進(jìn)行注入冤议,(注意是構(gòu)造函數(shù)注入)然后在controller中添加代碼測試
services.AddScoped<IRedisCacheManager, RedisCacheManager>();
IAdvertisementServices advertisementServices;
IBlogArticleServices blogArticleServices;
IRedisCacheManager redisCacheManager;//Reids緩存 /// <summary>
/// 構(gòu)造函數(shù) /// </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;
}</pre>
<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;"> /// <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));
} return blogArticleList;
}
4师坎、運行,執(zhí)行Redis緩存堪滨,看到結(jié)果
六胯陋、心結(jié)
今天的講解就到里了,是不是有一種草草收場的感覺,是的遏乔!本來后來應(yīng)該最后一節(jié)义矛。細(xì)心的你應(yīng)該發(fā)現(xiàn)了,我們是在controller進(jìn)行測試盟萨,Redis緩存的是List泛型凉翻,但是呢,AOP切面緩存還是基于內(nèi)存緩存捻激,昨天我本想合并下制轰,奈何AOP切面是通過異步編程,獲取到的Task的List泛型胞谭,在Redis中需要序列化垃杖,鄙人表示不是很懂,希望看到的大神幫忙解決下丈屹,
如何把異步返回的Task<List<T>>結(jié)果调俘,緩存到Redis,并能通過泛型取出來旺垒,有償服務(wù)彩库。感謝!
七先蒋、CODE
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core
QQ群:
867095512 (blod.core)