AOP的姿势之 简化混用 MemoryCache 和 DistributedCache 的方式

0. 前言

之前写了几篇文章介绍了一些AOP的知识,
但是还没有亮出来AOP的姿势,
也许姿势漂亮一点,
大家会对AOP有点兴趣
内容大致会分为如下几篇:(毕竟人懒,一下子写完太累了,没有动力)

AOP的姿势之 简化 MemoryCache 使用方式
AOP的姿势之 简化混用 MemoryCache 和 DistributedCache 使用方式
AOP的姿势之 如何把 HttpClient 变为声明式
至于AOP框架在这儿示例依然会使用我自己基于emit实现的动态代理AOP框架: //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 放在 //github.com/fs7744/AopDemoList/tree/master/MultipleCache/MultipleCache

Tags: