此次的目標是實現經過標註Attribute實現緩存的功能,精簡代碼,減小緩存的代碼侵入業務代碼。html
緩存內容即爲Service查詢彙總的內容,不作其餘高大上的功能,提高短期屢次查詢的響應速度,適當減輕數據庫壓力。git
在作以前,也去看了EasyCaching的源碼,此次的想法也是源於這裏,AOP的方式讓代碼減小耦合,可是緩存策略有限。通過考慮決定,本身實現相似功能,在以後的應用中也方便對緩存策略的擴展。github
本文內容也許有點不嚴謹的地方,僅供參考。一樣歡迎各位路過的大佬提出建議。redis
以前有作AspectCore的總結,相關內容就再也不贅述了。shell
在stackexchange.Redis和CSRedis中糾結了好久,也沒有一個特別的有優點,最終選擇了stackexchange.Redis,沒有理由。至於鏈接超時的問題,能夠用異步解決。數據庫
Install-Package StackExchange.Redis -Version 2.0.601
{ "Redis": { "Default": { "Connection": "127.0.0.1:6379", "InstanceName": "RedisCache:", "DefaultDB": 0 } } }
用於鏈接Redis服務器,包括建立鏈接,獲取數據庫等操做json
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的方式。c#
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); } }
建立一個緩存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(); } }
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; }
接下來就是重頭戲,攔截器中的邏輯就相對於緩存的相關策略,不用的策略能夠分紅不一樣的攔截器。
這裏的邏輯參考了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(); } }
[CacheAble(CacheKeyPrefix = "test", Expiration = 30, OnceUpdate = true)] public virtual DateTimeModel GetTime() { return new DateTimeModel { Id = GetHashCode(), Time = DateTime.Now }; }
請求接口,返回時間,並將返回結果緩存到Redis中,保留300秒後過時。