這次的目標是實作通過標注Attribute實作快取的功能,精簡代碼,減少快取的代碼侵入業務代碼,
快取內容即為Service查詢匯總的內容,不做其他高大上的功能,提升短時間多次查詢的回應速度,適當減輕資料庫壓力,
在做之前,也去看了EasyCaching的原始碼,這次的想法也是源于這里,AOP的方式讓代碼減少耦合,但是快取策略有限,經過考慮決定,自己實作類似功能,在之后的應用中也方便對快取策略的擴展,
本文內容也許有點不嚴謹的地方,僅供參考,同樣歡迎各位路過的大佬提出建議,
在專案中加入AspectCore
之前有做AspectCore的總結,相關內容就不再贅述了,
- ASP.NET Core 3.0 使用AspectCore-Framework實作AOP
- GitHub:相關代碼
在專案中加入Stackexchange.Redis
在stackexchange.Redis和CSRedis中糾結了很久,也沒有一個特別的有優勢,最終選擇了stackexchange.Redis,沒有理由,至于連接超時的問題,可以用異步解決,
- 安裝Stackexchange.Redis
Install-Package StackExchange.Redis -Version 2.0.601
- 在appsettings.json配置Redis連接資訊
{
"Redis": {
"Default": {
"Connection": "127.0.0.1:6379",
"InstanceName": "RedisCache:",
"DefaultDB": 0
}
}
}
- RedisClient
用于連接Redis服務器,包括創建連接,獲取資料庫等操作
public class RedisClient : IDisposable
{
private string _connectionString;
private string _instanceName;
private int _defaultDB;
private ConcurrentDictionary<string, ConnectionMultiplexer> _connections;
public RedisClient(string connectionString, string instanceName, int defaultDB = 0)
{
_connectionString = connectionString;
_instanceName = instanceName;
_defaultDB = defaultDB;
_connections = new ConcurrentDictionary<string, ConnectionMultiplexer>();
}
private ConnectionMultiplexer GetConnect()
{
return _connections.GetOrAdd(_instanceName, p => ConnectionMultiplexer.Connect(_connectionString));
}
public IDatabase GetDatabase()
{
return GetConnect().GetDatabase(_defaultDB);
}
public IServer GetServer(string configName = null, int endPointsIndex = 0)
{
var confOption = ConfigurationOptions.Parse(_connectionString);
return GetConnect().GetServer(confOption.EndPoints[endPointsIndex]);
}
public ISubscriber GetSubscriber(string configName = null)
{
return GetConnect().GetSubscriber();
}
public void Dispose()
{
if (_connections != null && _connections.Count > 0)
{
foreach (var item in _connections.Values)
{
item.Close();
}
}
}
}
- 注冊服務
Redis是單執行緒的服務,多幾個RedisClient的實體也是無濟于事,所以依賴注入就采用singleton的方式,
public static class RedisExtensions
{
public static void ConfigRedis(this IServiceCollection services, IConfiguration configuration)
{
var section = configuration.GetSection("Redis:Default");
string _connectionString = section.GetSection("Connection").Value;
string _instanceName = section.GetSection("InstanceName").Value;
int _defaultDB = int.Parse(section.GetSection("DefaultDB").Value ?? "0");
services.AddSingleton(new RedisClient(_connectionString, _instanceName, _defaultDB));
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.ConfigRedis(Configuration);
}
}
- KeyGenerator
創建一個快取Key的生成器,以Attribute中的CacheKeyPrefix作為前綴,之后可以擴展批量洗掉的功能,被攔截方法的方法名和入參也同樣作為key的一部分,保證Key值不重復,
public static class KeyGenerator
{
public static string GetCacheKey(MethodInfo methodInfo, object[] args, string prefix)
{
StringBuilder cacheKey = new StringBuilder();
cacheKey.Append($"{prefix}_");
cacheKey.Append(methodInfo.DeclaringType.Name).Append($"_{methodInfo.Name}");
foreach (var item in args)
{
cacheKey.Append($"_{item}");
}
return cacheKey.ToString();
}
public static string GetCacheKeyPrefix(MethodInfo methodInfo, string prefix)
{
StringBuilder cacheKey = new StringBuilder();
cacheKey.Append(prefix);
cacheKey.Append($"_{methodInfo.DeclaringType.Name}").Append($"_{methodInfo.Name}");
return cacheKey.ToString();
}
}
寫一套快取攔截器
- CacheAbleAttribute
Attribute中保存快取的策略資訊,包括過期時間,Key值前綴等資訊,在使用快取時可以對這些選項值進行配置,
public class CacheAbleAttribute : Attribute
{
/// <summary>
/// 過期時間(秒)
/// </summary>
public int Expiration { get; set; } = 300;
/// <summary>
/// Key值前綴
/// </summary>
public string CacheKeyPrefix { get; set; } = string.Empty;
/// <summary>
/// 是否高可用(例外時執行原方法)
/// </summary>
public bool IsHighAvailability { get; set; } = true;
/// <summary>
/// 只允許一個執行緒更新快取(帶鎖)
/// </summary>
public bool OnceUpdate { get; set; } = false;
}
- CacheAbleInterceptor
接下來就是重頭戲,攔截器中的邏輯就相對于快取的相關策略,不用的策略可以分成不同的攔截器,
這里的邏輯參考了EasyCaching的原始碼,并加入了Redis分布式鎖的應用,
public class CacheAbleInterceptor : AbstractInterceptor
{
[FromContainer]
private RedisClient RedisClient { get; set; }
private IDatabase Database;
private static readonly ConcurrentDictionary<Type, MethodInfo> TypeofTaskResultMethod = new ConcurrentDictionary<Type, MethodInfo>();
public async override Task Invoke(AspectContext context, AspectDelegate next)
{
CacheAbleAttribute attribute = context.GetAttribute<CacheAbleAttribute>();
if (attribute == null)
{
await context.Invoke(next);
return;
}
try
{
Database = RedisClient.GetDatabase();
string cacheKey = KeyGenerator.GetCacheKey(context.ServiceMethod, context.Parameters, attribute.CacheKeyPrefix);
string cacheValue = https://www.cnblogs.com/king-23100/p/await GetCacheAsync(cacheKey);
Type returnType = context.GetReturnType();
if (string.IsNullOrWhiteSpace(cacheValue))
{
if (attribute.OnceUpdate)
{
string lockKey = $"Lock_{cacheKey}";
RedisValue token = Environment.MachineName;
if (await Database.LockTakeAsync(lockKey, token, TimeSpan.FromSeconds(10)))
{
try
{
var result = await RunAndGetReturn(context, next);
await SetCache(cacheKey, result, attribute.Expiration);
return;
}
finally
{
await Database.LockReleaseAsync(lockKey, token);
}
}
else
{
for (int i = 0; i < 5; i++)
{
Thread.Sleep(i * 100 + 500);
cacheValue = await GetCacheAsync(cacheKey);
if (!string.IsNullOrWhiteSpace(cacheValue))
{
break;
}
}
if (string.IsNullOrWhiteSpace(cacheValue))
{
var defaultValue = CreateDefaultResult(returnType);
context.ReturnValue = ResultFactory(defaultValue, returnType, context.IsAsync());
return;
}
}
}
else
{
var result = await RunAndGetReturn(context, next);
await SetCache(cacheKey, result, attribute.Expiration);
return;
}
}
var objValue = await DeserializeCache(cacheKey, cacheValue, returnType);
//快取值不可用
if (objValue == null)
{
await context.Invoke(next);
return;
}
context.ReturnValue = ResultFactory(objValue, returnType, context.IsAsync());
}
catch (Exception)
{
if (context.ReturnValue == null)
{
await context.Invoke(next);
}
}
}
private async Task GetCacheAsync(string cacheKey)
{
string cacheValue = null;
try
{
cacheValue = await Database.StringGetAsync(cacheKey);
}
catch (Exception)
{
return null;
}
return cacheValue;
}
private async Task
- 注冊攔截器
在AspectCore中注冊CacheAbleInterceptor攔截器,這里直接注冊了用于測驗的DemoService,
在正式專案中,打算用反射注冊需要用到快取的Service或者Method,
public static class AspectCoreExtensions
{
public static void ConfigAspectCore(this IServiceCollection services)
{
services.ConfigureDynamicProxy(config =>
{
config.Interceptors.AddTyped<CacheAbleInterceptor>(Predicates.Implement(typeof(DemoService)));
});
services.BuildAspectInjectorProvider();
}
}
測驗快取功能
- 在需要快取的介面/方法上標注Attribute
[CacheAble(CacheKeyPrefix = "test", Expiration = 30, OnceUpdate = true)]
public virtual DateTimeModel GetTime()
{
return new DateTimeModel
{
Id = GetHashCode(),
Time = DateTime.Now
};
}
- 測驗結果截圖
請求介面,回傳時間,并將回傳結果快取到Redis中,保留300秒后過期,

相關鏈接
- GitHub:本文代碼
- GitHub:EasyCaching
- 官方檔案:EasyCaching
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/5913.html
標籤:ASP.NET
上一篇:aspx.designer.cs沒有自動生成代碼(沒有自動注冊)
下一篇:.net 呼叫C++ dll
