原文作者:老張的哲學
零、今天完成的大紅色部分
image
一、給緩存增加驗證篩選特性
1、自定義緩存特性
在解決方案中添加新項目Blog.Core.Common,然後在該Common類庫中添加 特性文件夾 和 特性實體類,以後特性就在這裏
//CachingAttribute
using System;
namespace Blog.Core.Common.Attribue
{
/// <summary>
/// 這個Attribute就是使用時候的驗證,把它添加到要緩存數據的方法中,
/// 即可完成緩存的操作。注意是對Method驗證有效
/// </summary>
public class CachingAttribue : Attribute
{
/// <summary>
/// 緩存絕對過期時間
/// </summary>
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();//直接執行被攔截方法
}
}
3、在service層中增加緩存特性
在指定的Service層中的某些類的某些方法上增加特性(一定是方法,不懂的可以看定義特性的時候AttributeTargets.Method)
using Blog.Core.Common.Attribue;
using Blog.Core.IRepository;
using Blog.Core.IServices;
using Blog.Core.Model;
using Blog.Core.Services.Base;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Blog.Core.Services
{
public class ArticleServices : BaseServices<Article>, IArticleServices
{
readonly IArticleRepository dal;
public ArticleServices(IArticleRepository dal)
{
this.dal = dal;
base.baseDal = dal;
}
/// <summary>
/// 獲取博客列表
/// </summary>
/// <returns></returns>
[CachingAttribue(AbsoluteExpiration = 10)] // 添加特性
public async Task<List<Article>> getBlogs()
{
var blogList = await dal.Query(a => a.bID > 0,a => a.bID);
return blogList;
}
}
}
4、特定緩存效果展示
運行項目,打斷點,就可以看到,普通的Query或者CURD等都不繼續緩存了,只有咱們特定的 getBlogs()方法,帶有緩存特性的纔可以
image
**
當然,這裏還有一個小問題,就是所有的方法還是走的切面,只是增加了過濾驗證,大家也可以直接把那些需要的注入,不需要的乾脆不注入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、保存數據不經常變化
image
2、如果數據經常變化,就需要取操作Redis和持久化數據層的動作了,保證所有的都是最新的,實時更新Redis 的key到數據庫,data到Redis中,但是要注意高併發
三、Redis的安裝和調試使用
**1.下載最新版redis,選擇.msi安裝版本,或者.zip免安裝 **(我這裏是.msi安裝)
image
2.雙擊執行.msi文件,一路next,中間有一個需要註冊服務,因爲如果不註冊的話,把啓動的Dos窗口關閉的話,Redis就中斷連接了。
image
3.如果你是免安裝的,需要執行以下語句
啓動命令:redis-server.exe redis.windows.conf
註冊服務命令:redis-server.exe --service-install redis.windows.conf
去服務列表查詢服務,可以看到redis服務默認沒有開啓,開啓redis服務(可以設置爲開機自動啓動)
image
還有要看Redis服務是否開啓
image
更新:這裏有個小插曲,如果你第一次使用,可以修改下 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 =》 複製到輸出文件夾 =》 永遠複製 =》應用,保存
image
4、運行項目,查看效果
image
五、基於Controller的Redis緩存
1、自定義序列化幫助類
在Blog.Core.Common的Helper文件夾中,添加SerializeHelper.cs 對象序列化操作,以後再擴展
using Newtonsoft.Json;
using System.Text;
namespace Blog.Core.Common.Helper
{
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);
}
}
}
image.png
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 層,大家知道怎麼用就行。
image
添加nuget包後,然後引用
using StackExchange.Redis;
using Blog.Core.Common.Helper;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Text;
namespace Blog.Core.Common.Redis
{
class RedisCacheManager : IRedisCacheManager
{
private readonly string redisConnectionString;
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.redisConnectionString = 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(redisConnectionString);
}
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 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 bool Get(string key)
{
return redisConnection.GetDatabase().KeyExists(key);
}
public string GetValue(string key)
{
return redisConnection.GetDatabase().StringGet(key);
}
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
image
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緩存,看到結果
image
六、基於AOP的Redis緩存
1、核心:Redis緩存切面攔截器
在上篇文章中,我們已經定義過了一個攔截器,只不過是基於內存Memory緩存的,並不適應於Redis,上邊咱們也說到了Redis必須要存入指定的值,比如字符串,而不能將異步對象 Task<T> 保存到硬盤上,所以我們就修改下攔截器方法,一個專門應用於 Redis 的切面攔截器:
using Blog.Core.Common.Attribue;
using Blog.Core.Common.Redis;
using Castle.DynamicProxy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Blog.Core.API.AOP
{
/// <summary>
/// 通過注入的方式,把Redis緩存操作接口通過構造函數注入
/// </summary>
public class BlogCacheRedisAOP : BaseCacheAOP
{
private readonly IRedisCacheManager _cache;
public BlogCacheRedisAOP(IRedisCacheManager cache)
{
_cache = cache;
}
/// <summary>
/// Intercept方法是攔截的關鍵所在,也是IInterceptor接口中的唯一定義
/// </summary>
/// <param name="invocation"></param>
public override 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 = 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();
}
}
}
}
BaseCacheAOP.cs:
using Castle.DynamicProxy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Blog.Core.API.AOP
{
public abstract class BaseCacheAOP : IInterceptor
{
public abstract void Intercept(IInvocation invocation);
/// <summary>
/// 定義緩存鍵
/// </summary>
/// <param name="invication"></param>
/// <returns></returns>
protected string CustomCacheKey(IInvocation invication)
{
var typeName = invication.TargetType.Name;
var methodName = invication.Method.Name;
// 獲取參數列表,我最多需要三個即可
var methodArguments = invication.Arguments.Select(GetArgumentValue).Take(3).ToList();
string key = $"{typeName}:{methodName}";
foreach (var param in methodArguments)
{
key += $"{param}";
}
return key.TrimEnd(':');
}
/// <summary>
/// object 轉 string
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
protected string GetArgumentValue(object arg)
{
if (arg is int || arg is long || arg is string)
return arg.ToString();
if (arg is DateTime)
return ((DateTime)arg).ToString("yyyyMMddHHmmss");
return "";
}
}
}
上邊紅色標註的,是和之前不一樣的,整體結構還是差不多的,相信都能看的懂的,最後我們就可以很任性的在Autofac容器中,進行任意緩存切換了,是不是很棒!