這次的目標是實現通過標註Attribute實現緩存的功能,精簡代碼,減少緩存的代碼侵入業務代碼。 緩存內容即為Service查詢彙總的內容,不做其他高大上的功能,提升短時間多次查詢的響應速度,適當減輕資料庫壓力。 在做之前,也去看了EasyCaching的源碼,這次的想法也是源於這裡,AOP的方式讓 ...
這次的目標是實現通過標註Attribute實現緩存的功能,精簡代碼,減少緩存的代碼侵入業務代碼。
緩存內容即為Service查詢彙總的內容,不做其他高大上的功能,提升短時間多次查詢的響應速度,適當減輕資料庫壓力。
在做之前,也去看了EasyCaching的源碼,這次的想法也是源於這裡,AOP的方式讓代碼減少耦合,但是緩存策略有限。經過考慮決定,自己實現類似功能,在之後的應用中也方便對緩存策略的擴展。
本文內容也許有點不嚴謹的地方,僅供參考。同樣歡迎各位路過的大佬提出建議。
在項目中加入AspectCore
之前有做AspectCore的總結,相關內容就不再贅述了。
在項目中加入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 = 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<string> GetCacheAsync(string cacheKey)
{
string cacheValue = null;
try
{
cacheValue = await Database.StringGetAsync(cacheKey);
}
catch (Exception)
{
return null;
}
return cacheValue;
}
private async Task<object> RunAndGetReturn(AspectContext context, AspectDelegate next)
{
await context.Invoke(next);
return context.IsAsync()
? await context.UnwrapAsyncReturnValue()
: context.ReturnValue;
}
private async Task SetCache(string cacheKey, object cacheValue, int expiration)
{
string jsonValue = JsonConvert.SerializeObject(cacheValue);
await Database.StringSetAsync(cacheKey, jsonValue, TimeSpan.FromSeconds(expiration));
}
private async Task Remove(string cacheKey)
{
await Database.KeyDeleteAsync(cacheKey);
}
private async Task<object> DeserializeCache(string cacheKey, string cacheValue, Type returnType)
{
try
{
return JsonConvert.DeserializeObject(cacheValue, returnType);
}
catch (Exception)
{
await Remove(cacheKey);
return null;
}
}
private object CreateDefaultResult(Type returnType)
{
return Activator.CreateInstance(returnType);
}
private object ResultFactory(object result, Type returnType, bool isAsync)
{
if (isAsync)
{
return TypeofTaskResultMethod
.GetOrAdd(returnType, t => typeof(Task)
.GetMethods()
.First(p => p.Name == "FromResult" && p.ContainsGenericParameters)
.MakeGenericMethod(returnType))
.Invoke(null, new object[] { result });
}
else
{
return result;
}
}
}
- 註冊攔截器
在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秒後過期。