使用AspectCore實現AOP模式的Redis緩存


這次的目標是實現通過標注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秒后過期。

相關鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM