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

此次的目標是實現經過標註Attribute實現緩存的功能,精簡代碼,減小緩存的代碼侵入業務代碼。html

緩存內容即爲Service查詢彙總的內容,不作其餘高大上的功能,提高短期屢次查詢的響應速度,適當減輕數據庫壓力。git

在作以前,也去看了EasyCaching的源碼,此次的想法也是源於這裏,AOP的方式讓代碼減小耦合,可是緩存策略有限。通過考慮決定,本身實現相似功能,在以後的應用中也方便對緩存策略的擴展。github

本文內容也許有點不嚴謹的地方,僅供參考。一樣歡迎各位路過的大佬提出建議。redis

在項目中加入AspectCore

以前有作AspectCore的總結,相關內容就再也不贅述了。shell

在項目中加入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服務器,包括建立鏈接,獲取數據庫等操做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);
    }
}
  • 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秒後過時。

相關連接

相關文章
相關標籤/搜索