1. 程式人生 > 實用技巧 >AOP的姿勢之 簡化混用 MemoryCache 和 DistributedCache 的方式

AOP的姿勢之 簡化混用 MemoryCache 和 DistributedCache 的方式

0. 前言

之前寫了幾篇文章介紹了一些AOP的知識,
但是還沒有亮出來AOP的姿勢,
也許姿勢漂亮一點,
大家會對AOP有點興趣
內容大致會分為如下幾篇:(畢竟人懶,一下子寫完太累了,沒有動力)

AOP的姿勢之 簡化 MemoryCache 使用方式
AOP的姿勢之 簡化混用 MemoryCache 和 DistributedCache 使用方式
AOP的姿勢之 如何把 HttpClient 變為宣告式
至於AOP框架在這兒示例依然會使用我自己基於emit實現的動態代理AOP框架: https://github.com/fs7744/Norns.Urd
畢竟是自己寫的,魔改/加功能都很方便,
萬一萬一大家如果有疑問,(雖然大概不會有),我也好回答, (當然如果大家認可,在github給個star,就實在是太讓人開心了)

1. 非常重要的注意事項

本篇主要目的是介紹如何利用AOP簡化使用Cache的程式碼的方式
但是在真實業務場景如果要混用 MemoryCache 和 DistributedCache,
最好貼合場景好好思考一下,為何要這樣用?
每多加一個cache就是增加一層複雜度,
如果一層cache不能解決問題?
那麼兩層就能嗎?三層就能嗎?特別是快取穿透等等怎麼辦呢?
一層不能解決問題的原因是什麼呢?
希望大家三思而行,哈哈

2. 如何混用呢?

2.1 統一模型,統一介面

MemoryCache 和 DistributedCache 的介面定義雖然相似度和思想很接近,
但是呢,還是存在不一樣,
大家可以看下面的介面定義

    public interface IMemoryCache : IDisposable
    {
        //
        // 摘要:
        //     Create or overwrite an entry in the cache.
        //
        // 引數:
        //   key:
        //     An object identifying the entry.
        //
        // 返回結果:
        //     The newly created Microsoft.Extensions.Caching.Memory.ICacheEntry instance.
        ICacheEntry CreateEntry(object key);
        //
        // 摘要:
        //     Removes the object associated with the given key.
        //
        // 引數:
        //   key:
        //     An object identifying the entry.
        void Remove(object key);
        //
        // 摘要:
        //     Gets the item associated with this key if present.
        //
        // 引數:
        //   key:
        //     An object identifying the requested entry.
        //
        //   value:
        //     The located value or null.
        //
        // 返回結果:
        //     true if the key was found.
        bool TryGetValue(object key, out object value);
    }
    public interface IDistributedCache
    {
        //
        // 摘要:
        //     Gets a value with the given key.
        //
        // 引數:
        //   key:
        //     A string identifying the requested value.
        //
        // 返回結果:
        //     The located value or null.
        byte[] Get(string key);
        //
        // 摘要:
        //     Gets a value with the given key.
        //
        // 引數:
        //   key:
        //     A string identifying the requested value.
        //
        //   token:
        //     Optional. The System.Threading.CancellationToken used to propagate notifications
        //     that the operation should be canceled.
        //
        // 返回結果:
        //     The System.Threading.Tasks.Task that represents the asynchronous operation, containing
        //     the located value or null.
        Task<byte[]> GetAsync(string key, CancellationToken token = default);
        //
        // 摘要:
        //     Refreshes a value in the cache based on its key, resetting its sliding expiration
        //     timeout (if any).
        //
        // 引數:
        //   key:
        //     A string identifying the requested value.
        void Refresh(string key);
        //
        // 摘要:
        //     Refreshes a value in the cache based on its key, resetting its sliding expiration
        //     timeout (if any).
        //
        // 引數:
        //   key:
        //     A string identifying the requested value.
        //
        //   token:
        //     Optional. The System.Threading.CancellationToken used to propagate notifications
        //     that the operation should be canceled.
        //
        // 返回結果:
        //     The System.Threading.Tasks.Task that represents the asynchronous operation.
        Task RefreshAsync(string key, CancellationToken token = default);
        //
        // 摘要:
        //     Removes the value with the given key.
        //
        // 引數:
        //   key:
        //     A string identifying the requested value.
        void Remove(string key);
        //
        // 摘要:
        //     Removes the value with the given key.
        //
        // 引數:
        //   key:
        //     A string identifying the requested value.
        //
        //   token:
        //     Optional. The System.Threading.CancellationToken used to propagate notifications
        //     that the operation should be canceled.
        //
        // 返回結果:
        //     The System.Threading.Tasks.Task that represents the asynchronous operation.
        Task RemoveAsync(string key, CancellationToken token = default);
        //
        // 摘要:
        //     Sets a value with the given key.
        //
        // 引數:
        //   key:
        //     A string identifying the requested value.
        //
        //   value:
        //     The value to set in the cache.
        //
        //   options:
        //     The cache options for the value.
        void Set(string key, byte[] value, DistributedCacheEntryOptions options);
        //
        // 摘要:
        //     Sets the value with the given key.
        //
        // 引數:
        //   key:
        //     A string identifying the requested value.
        //
        //   value:
        //     The value to set in the cache.
        //
        //   options:
        //     The cache options for the value.
        //
        //   token:
        //     Optional. The System.Threading.CancellationToken used to propagate notifications
        //     that the operation should be canceled.
        //
        // 返回結果:
        //     The System.Threading.Tasks.Task that represents the asynchronous operation.
        Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default);
    }

那麼我們為了讓多個不同實現的快取介面能被同一段快取操作程式碼所使用,
就需要定義統一的介面並適配各種不同的快取介面
(當然,我們不這樣做也能湊出程式碼達到相同效果,但是呢,別人看到這樣的實現,難免會吐槽我們的程式碼,如果不小心聽見,面子有點掛不住呀)
這裡呢,我們就這樣簡單定義一個這樣的介面

    public interface ICacheAdapter
    {
        // Cache 實現的名字,以此能指定使用哪種快取實現
        string Name { get; }

        // 嘗試獲取快取
        bool TryGetValue<T>(string key, out T result);

        // 存入快取資料,這裡為了簡單,我們就只支援ttl過期策略
        void Set<T>(string key, T result, TimeSpan ttl);
    }

2.2 適配MemoryCache

    [NonAspect]
    public class MemoryCacheAdapter : ICacheAdapter
    {
        private readonly IMemoryCache cache;

        public MemoryCacheAdapter(IMemoryCache cache)
        {
            this.cache = cache;
        }

        // 取個固定名字
        public string Name => "memory";

        public void Set<T>(string key, T result, TimeSpan ttl)
        {
            cache.Set(key, result, ttl);
        }

        public bool TryGetValue<T>(string key, out T result)
        {
            return cache.TryGetValue(key, out result);
        }
    }

2.3 適配DistributedCache

    [NonAspect]
    public class DistributedCacheAdapter : ICacheAdapter
    {
        private readonly IDistributedCache cache;
        private readonly string name;

        public DistributedCacheAdapter(IDistributedCache cache, string name)
        {
            this.cache = cache;
            this.name = name;
        }

        /// 這裡我們就不固定名字了,大家想用 redis 就可以自己名字取redis
        public string Name => name;

        public void Set<T>(string key, T result, TimeSpan ttl)
        {
            cache.Set(key,
                JsonSerializer.SerializeToUtf8Bytes(result),  // 為了簡單,我們就不在擴充套件更多不同序列化器了,這裡就用System.Text.Json
                new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = ttl });  // 同樣,為了簡單,也只支援ttl快取策略
        }

        public bool TryGetValue<T>(string key, out T result)
        {
            var data = cache.Get(key);
            if (data == null)
            {
                result = default;
                return false;
            }
            else
            {
                result = JsonSerializer.Deserialize<T>(data);
                return true;
            }
        }
    }

2.4 定義CacheAttribute

這裡我們依然使用 attribute 這種對大家使用最簡單的方式
但是呢,由於有多個快取實現使用,
我們直接使用 InterceptorAttribute 很難控制不同快取實現的使用,
所以我們這裡拆分 快取使用的定義 與 真正快取的呼叫邏輯
CacheAttribute 只是快取使用的定義

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class CacheAttribute : Attribute
    {
        // 由於多個快取實現,我們需要有使用順序指定
        public int Order { get; set; }
        public string CacheKey { get; set; }
        public string Ttl { get; set; }
        public string CacheName { get; set; }
    }

2.5 實現CacheInterceptor

    public class CacheInterceptor : AbstractInterceptor
    {
        public override bool CanAspect(MethodReflector method)
        {
            return method.IsDefined<CacheAttribute>();  // 限制只對有快取定義的方法起效
        }

        public override async Task InvokeAsync(AspectContext context, AsyncAspectDelegate next)
        {
            var caches = context.ServiceProvider.GetRequiredService<IEnumerable<ICacheAdapter>>()
                .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);

            var cas = context.Method.GetReflector()
                .GetCustomAttributes<CacheAttribute>()
                .OrderBy(i => i.Order)
                .ToArray();

            // 為了簡單,我們就使用最簡單的反射形式呼叫
            var m = typeof(CacheInterceptor).GetMethod(nameof(CacheInterceptor.GetOrCreateAsync))
                 .MakeGenericMethod(context.Method.ReturnType.GetGenericArguments()[0]);
            await (Task)m.Invoke(this, new object[] { caches, cas, context, next, 0 });
        }

        public async Task<T> GetOrCreateAsync<T>(Dictionary<string, ICacheAdapter> adapters, CacheAttribute[] options, AspectContext context, AsyncAspectDelegate next, int index)
        {
            if (index >= options.Length)
            {
                Console.WriteLine($"No found Cache at {DateTime.Now}.");
                // 所有cache 都找完了,沒有找到有效cache,所以需要拿真正的結果
                await next(context);
                // 為了簡單,我們就只支援 Task<T> 的結果
                return ((Task<T>)context.ReturnValue).Result;
            }

            var op = options[index];
            T result;
            var cacheName = op.CacheName;
            if (adapters.TryGetValue(cacheName, out var adapter))
            {
                if (!adapter.TryGetValue<T>(op.CacheKey, out result))
                {
                    // 當前快取找不到結果,移到下一個快取獲取結果
                    result = await GetOrCreateAsync<T>(adapters, options, context, next, ++index);
                    adapter.Set(op.CacheKey, result, TimeSpan.Parse(op.Ttl)); // 更新當前快取實現的儲存
                    context.ReturnValue = Task.FromResult(result); // 為了簡單,我們就在這兒更新返回結果,其實不該在這兒的,為什麼,大家可以猜一猜為什麼?
                }
                else
                {
                    Console.WriteLine($"Get Cache From {cacheName} at {DateTime.Now}.");
                    context.ReturnValue = Task.FromResult(result); // 為了簡單,我們就在這兒更新返回結果,其實不該在這兒的,為什麼,大家可以猜一猜為什麼?
                }
            }
            else
            {
                throw new ArgumentException($"No such cache: {cacheName}.");
            }

            return result;
        }
    }

2.6 測試

    public class DoCacheTest
    {
        [Cache(CacheKey = nameof(Do), CacheName = "memory", Order = 0, Ttl = "00:00:01")]   // 1秒過期
        [Cache(CacheKey = nameof(Do), CacheName = "distribute", Order = 1, Ttl = "00:00:05")]  // 5秒過期
        public virtual Task<string> Do() => Task.FromResult(DateTime.Now.ToString());
    }

    class Program
    {
        static async Task Main(string[] args)
        {
            var sut = new ServiceCollection()
                   .AddTransient<DoCacheTest>()
                   .ConfigureAop(i => i.GlobalInterceptors.Add(new CacheInterceptor()))  // 設定Cache攔截器
                   .AddMemoryCache()
                   .AddDistributedMemoryCache() // 為了測試,我們就不使用redis之類的東西了,用個記憶體實現模擬就好
                   .AddSingleton<ICacheAdapter, MemoryCacheAdapter>()  // 新增快取介面卡
                   .AddSingleton<ICacheAdapter>(i => new DistributedCacheAdapter(i.GetRequiredService<IDistributedCache>(), "distribute"))
                   .BuildServiceProvider()
                  .GetRequiredService<DoCacheTest>();

            for (int i = 0; i < 20; i++)
            {
                Console.WriteLine($"Get: {await sut.Do()}");
                await Task.Delay(500);  // 每隔半秒,觀察快取變化
            }
        }
    }

結果:

No found Cache at 2021/1/3 11:56:10.
Get: 2021/1/3 11:56:10

Get Cache From memory at 2021/1/3 11:56:10.
Get: 2021/1/3 11:56:10
Get Cache From distribute at 2021/1/3 11:56:11.
Get: 2021/1/3 11:56:10
Get Cache From memory at 2021/1/3 11:56:11.
Get: 2021/1/3 11:56:10
Get Cache From distribute at 2021/1/3 11:56:12.
Get: 2021/1/3 11:56:10
Get Cache From memory at 2021/1/3 11:56:12.
Get: 2021/1/3 11:56:10
Get Cache From distribute at 2021/1/3 11:56:13.
Get: 2021/1/3 11:56:10
Get Cache From memory at 2021/1/3 11:56:13.
Get: 2021/1/3 11:56:10
Get Cache From distribute at 2021/1/3 11:56:14.
Get: 2021/1/3 11:56:10
Get Cache From memory at 2021/1/3 11:56:14.
Get: 2021/1/3 11:56:10

No found Cache at 2021/1/3 11:56:15.
Get: 2021/1/3 11:56:15

Get Cache From memory at 2021/1/3 11:56:15.
Get: 2021/1/3 11:56:15
Get Cache From distribute at 2021/1/3 11:56:16.
Get: 2021/1/3 11:56:15
Get Cache From memory at 2021/1/3 11:56:16.
Get: 2021/1/3 11:56:15
Get Cache From distribute at 2021/1/3 11:56:17.
Get: 2021/1/3 11:56:15
Get Cache From memory at 2021/1/3 11:56:17.
Get: 2021/1/3 11:56:15
Get Cache From distribute at 2021/1/3 11:56:18.
Get: 2021/1/3 11:56:15
Get Cache From memory at 2021/1/3 11:56:18.
Get: 2021/1/3 11:56:15
Get Cache From distribute at 2021/1/3 11:56:19.
Get: 2021/1/3 11:56:15
Get Cache From memory at 2021/1/3 11:56:19.
Get: 2021/1/3 11:56:15

就是這樣,大家就可以很簡單的混用 各種快取了,
但是呢,多個快取有沒有用?快取穿透等等問題需要大家最好想好才使用哦
完整的demo 放在 https://github.com/fs7744/AopDemoList/tree/master/MultipleCache/MultipleCache