乐趣区

关于c#:AOP的姿势之-简化混用-MemoryCache-和-DistributedCache-的方式

0. 前言

之前写了几篇文章介绍了一些 AOP 的常识,
然而还没有亮进去 AOP 的姿态,
兴许姿态丑陋一点,
大家会对 AOP 有点趣味
内容大抵会分为如下几篇:(毕竟人懒,一下子写完太累了,没有能源)

AOP 的姿态之 简化 MemoryCache 应用形式
AOP 的姿态之 简化混用 MemoryCache 和 DistributedCache 应用形式
AOP 的姿态之 如何把 HttpClient 变为申明式
至于 AOP 框架在这儿示例仍然会应用我本人基于 emit 实现的动静代理 AOP 框架:https://github.com/fs7744/Nor…
毕竟是本人写的,魔改 / 加性能都很不便,
万一万一大家如果有疑难,(尽管大略不会有),我也好答复,(当然如果大家认可,在 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

退出移动版