惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
S
Securelist
博客园 - Franky
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
IT之家
IT之家
GbyAI
GbyAI
Microsoft Azure Blog
Microsoft Azure Blog
The Cloudflare Blog
云风的 BLOG
云风的 BLOG
N
News and Events Feed by Topic
AI
AI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Schneier on Security
Schneier on Security
Attack and Defense Labs
Attack and Defense Labs
Vercel News
Vercel News
腾讯CDC
Google DeepMind News
Google DeepMind News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
M
MIT News - Artificial intelligence
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
Netflix TechBlog - Medium
量子位
S
Schneier on Security
Hacker News: Ask HN
Hacker News: Ask HN
Cyberwarzone
Cyberwarzone
S
Security Affairs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News and Events Feed by Topic
T
Tenable Blog
PCI Perspectives
PCI Perspectives
MyScale Blog
MyScale Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
W
WeLiveSecurity
N
News | PayPal Newsroom
P
Proofpoint News Feed
O
OpenAI News
C
CERT Recently Published Vulnerability Notes
B
Blog
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
MongoDB | Blog
MongoDB | Blog
大猫的无限游戏
大猫的无限游戏
A
Arctic Wolf
Y
Y Combinator Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Spread Privacy
Spread Privacy

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
再话 AOP,从简化缓存操作说起 - 元视角
飞鸿踏雪 · 2021-08-05 · via 元视角

AOP,即:面向切面编程,关于这个概念,博主其实写过好几篇博客啦!从这个概念,我们可以引申出诸如代理模式、动态代理、装饰器模式、过滤器、拦截器等等相互关联的概念。从实现方式上而言,微软官方的 .NET Remoting 提供了真实代理和透明代理的支持,我们熟悉的 WebServiceWCF 均和这项技术息息相关,作为最早的分布式 RPC 解决方案,其本身更是与客户端的动态代理密不可分。或许,各位曾经接触过 UnityCastleAspectCorePostSharp 等等这些支持 AOP 特性的库,那么,我们是否已经抵达了 AOP 的边界呢?事实上,如果你仔细研究过 StubMock 这样两个术语,你就发现 AOP 的应用范围远比我们想象的宽广。今天这篇文章,我不打算再介绍一遍这些第三方库的“奇技淫巧”,我更想聊聊,如何通过 AOP 来简化一个缓存操作。

缓存,一个面试时命中率 100%的话题,曾记否?来自面试官的灵魂发问三连:缓存击穿缓存穿透缓存雪崩。与此同时,缓存是一个令人爱恨交加的东西,其一致性、持久化、高可用等等,均是实际应用中需要去考虑的东西。狭义的缓存主要指 RedisMemcached 等分布式缓存系统,而广义的缓存则可以是 HTTP 响应缓存、EF/EF Core 查询缓存、二级缓存等等。我们都知道,使用缓存可以显著地提升软件性能,而究其本质,则是因为减少了和数据库交互的频次。于是,我们注意到,大多数的缓存代码,都是下面这样的风格:

var cacheKey = "GetAllStudents";
var students = new List<Student>();
var cacheValue = distributedCache.GetString(cacheKey);
if (string.IsNullOrEmpty(cacheValue))
{
    // 未命中缓存:从数据库查询数据 + 写缓存
    students = repository.GetAll().ToList();
    var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(students));
    distributedCache.Set(cacheKey, bytes);
} 
else
{
    // 命中缓存:读缓存
    students = JsonConvert.DeserializeObject<List<Student>>(cacheValue);
}

return students;

正所谓:大道至简,“高端的食材,往往只需要最朴素的烹饪方式”。故而,最朴素的思想就是,首先从缓存中查询数据,如果数据存在则直接返回,否则从数据库中查询数据,并执行一次写缓存操作。这的确是个朴实无华的方案,因为我们每一次都要写这样的代码,其程度丝毫不亚于永远不会缺席的 xxx != null。写到这里,博主不由得陷入了沉思:难道真的没有更简单点的方案了吗?后来的故事大家都知道了,我们可以在方法的参数上附加 [NotNull] 特性。所以,接下来,我们会用类似的方案来解决缓存的问题,换言之,我们可以把我们经常写、写到不愿意再写的代码交给代理类来做,既然缓存本质上是为了查询数据,那我们就只需要关心查询数据这个行为本身。具体怎么实现的呢?我们一起来看下面的代码。

此时此刻,假设我们有这样一个接口:IFakeService,它通过GetColors()方法返回一组颜色:

interface IFakeService
{
    [Cacheable(CacheKeyPrefix = "Fake", Expiration = 180)]
    List<string> GetColors();
}

我们希望,在调用这个方法的时候,可以对其返回值进行缓存,所以,可以注意到,这里添加了一个[Cacheable]的特性。其定义如下:

[AttributeUsage(AttributeTargets.Method)]
public class CacheableAttribute : Attribute
{
    public string CacheKeyPrefix { get; set; }
    public int Expiration { get; set; }
}

其中,CacheKeyPrefix用于指定缓存键名前缀,Expiration用于指定缓存过期时间,单位为秒。接下来,博主通过DispatchProxy来实现动态代理,它可以视为RealProxy在后.NET 时代的替代品:

public class CacheInterceptor<TCacheService> : DispatchProxy
{
    private TCacheService _realObject => ServiceProvider.GetRequiredService<TCacheService>();
    private ICacheSerializer _cacheSerializer => ServiceProvider.GetRequiredService<ICacheSerializer>();
    private IDistributedCache _distributedCache => ServiceProvider.GetRequiredService<IDistributedCache>();

    public IServiceProvider ServiceProvider { get; set; }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        byte[] cacheValue;
        var returnType = targetMethod.ReturnType;

        // void && Task
        if (returnType == typeof(void) || returnType == typeof(Task))
            return targetMethod.Invoke(_realObject, args);

        if (IsAsyncReturnValue(targetMethod))
            returnType = targetMethod.ReturnType.GetGenericArguments()[0];

        var cacheableAttribute = targetMethod.GetCustomAttribute<CacheableAttribute>();
        if (cacheableAttribute != null)
        {
            var cacheKey = GetCacheKey(cacheableAttribute, targetMethod);
            cacheValue = _distributedCache.Get(cacheKey);
            if (cacheValue != null)
            {
                // Task<T>
                if (IsAsyncReturnValue(targetMethod))
                    return Task.FromResult(_cacheSerializer.Deserialize(cacheValue, returnType));

                return _cacheSerializer.Deserialize(cacheValue, returnType);
            }

            dynamic returnValue = targetMethod.Invoke(_realObject, args);
            cacheValue = _cacheSerializer.Serialize(returnValue);

            // Task<T>
            if (IsAsyncReturnValue(targetMethod))
                cacheValue = _cacheSerializer.Serialize(returnValue.Result);

            var cacheOptions = new DistributedCacheEntryOptions() { 
              AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(cacheableAttribute.Expiration) 
            };
            _distributedCache.Set(cacheKey, cacheValue, cacheOptions);
            return returnValue;
        }

        return targetMethod.Invoke(_realObject, args);
    }
}

这里,最为关键的地方是Invoke()方法,它负责对被代理对象的方法进行拦截,这里的被代理对象,其实就是_realObject,即真实对象,因为,我们最终调用的,实际上是真实对象上对应的方法。因为DispatchProxy在创建代理对象时,要求这个代理基类,即这里的拦截器,必须要有一个无参的构造函数。所以,我们这里用属性注入的方式来注入IServiceProvider。说回这个方法,首先,我们会判断它的返回值类型是不是void或者Task,因为无返回值的方法本身就不需要缓存。接下来,我们会检查当前方法上是否附加了[Cacheable]特性,因为我们只需要处理有这个特性的方法。接下来,通过GetCacheKey()方法来生成一个唯一的键名,通过这个键名我们就可以在缓存中查询数据啦,该方法的实现细节如下:

private string GetCacheKey(CacheableAttribute cacheableAttribute, MethodInfo methodInfo)
{
    var segments = new List<string>();

    if (!string.IsNullOrEmpty(cacheableAttribute.CacheKeyPrefix))
        segments.Add(cacheableAttribute.CacheKeyPrefix);

    segments.Add(methodInfo.DeclaringType.FullName.Replace(".", "_"));

    segments.Add(methodInfo.Name);

    methodInfo.GetParameters().ToList().ForEach(x => segments.Add(x.Name));

    return string.Join("_", segments);
}

对于分布式缓存,博主这里使用的是微软提供的IDistributedCache这个接口,接下来的事情就变得朴实无华起来,因为它和我们一开始写的代码一脉相承,唯一的不同是,这里考虑了Task<T>这种异步的返回值类型,同时对序列化/反序列化进行了抽象,即这里注入的ICacheSerializer接口,注意到IDistributedCache接口的Set()方法需要传入一个byte[],显然二进制的序列化方案如 ProtobufMessagePack 会更加得心应手一点。所以,我们将这一层单独抽象出来。至此,我们已经完成了最核心的部分。

对于一开始的IFakeService,我们提供一个简单的实现,并通过让线程阻塞的方式来模拟一个耗时操作:

public class FakeService : IFakeService
{
    public List<string> GetColors()
    {
        Thread.Sleep(500);
        return new List<string> { "Red", "Yellow", "Green" };
    }
}

下面是一个简单的示例:

// 注入IFakeService、ICacheSerializer、IDistributedCache
var services = new ServiceCollection();
services.AddTransient<IFakeService, FakeService>();
services.AddTransient<ICacheSerializer, JsonCacheSerializer>();
services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "Caching.AOP.Test";
});

// 生成代理对象
var serviceProvider = services.BuildServiceProvider();
var fakeServiceProxy = DispatchProxy.Create<IFakeService, CacheInterceptor<IFakeService>>();
(fakeServiceProxy as CacheInterceptor<IFakeService>).ServiceProvider = serviceProvider;

// 调用代理对象
for (var i = 0; i < 5; i++)
{
    var stopWatch = new Stopwatch();
    stopWatch.Start();
    var colors = fakeServiceProxy.GetColors();
    stopWatch.Stop();
    Console.WriteLine($" {i} - Invoke GetColors used {stopWatch.Elapsed.TotalMilliseconds} ms");
}

此时,我们可以得到下面的结果,可以注意到,第一次调用的时候,因为缓存不存在,调用的时间相对更长一点,而当缓存存在的时候,调用的时间会明显缩短。

有无缓存对调用时长的影响 有无缓存对调用时长的影响

虽然这个性能提升与缓存不无关系,可对于调用者来说,它完全不用关心缓存里有没有数据这件事情,它只需要像往常一样调用接口方法即可,这就是 AOP 之于缓存的意义所在,为了证明我没有说谎,我们可以看到 Redis 中对应的数据:

Redis中对应的缓存数据 Redis中对应的缓存数据

需要说明的是,这个思路同样可以扩展到UnityCastleAspectCorePostSharp 这些第三方库,实现方式上大同小异,大家可以结合自己的业务场景做相应的调整。其实,从业务上抽离出通用组件、功能作为公共库或者下沉到框架中,是及其自然而然的一件事情。这里面最关键的问题是,基础组件或者框架相对于业务方的职责范围,因为如果基础组件或者框架做得太多,业务上往往难以定制或者扩展;而如果基础组件或者框架做得太少,业务上就要写大量的辅助代码。写这篇文章的原因是,我对于一个缓存方案设计上的疑问,业务上想要缓存一张表中的数据,至少需要写 20 行代码,在下觉得这简直太离谱了,更不用说,业务方还要关心这个缓存是否可用。有人说,一个合格的前任就应该像死了一样,那么,我是不是可以说,一个合格的中间件,就应该像它从来没有来过一样,你甚至都感觉不到它的存在,可事实上它总是无所不在。也许,这听起来有点科幻的色彩,可这的确是我期待的某种自洽的、优雅的设计思路。

本文小结

本文分享了通过 AOP 来简化缓存操作的一种思路,考虑到常规的缓存代码写法,读/写缓存与业务代码严重耦合在一起,而博主心目中的缓存应该像水、电、煤气一样普普通通,你只需要告诉我哪些数据需要缓存,而无需关心这些数据怎么缓存。基于这样一种考虑,博主基于DispatchProxy实现了一个针对缓存的 AOP 方案,我们只需要在接口上打上[Cachable]标签,它会自动对方法的返回值进行缓存,从而简化我们平时使用缓存的流程。Catcher Wong 大佬在其缓存框架 EasyCaching 同样集成了这一特性,如果大家有类似的使用场景,可以直接使用这个框架。如果大家对此有更好的想法或者思路,欢迎大家在评论区留言,本文示例已上传至 Github,供大家学习或者参考。好了,以上就是这篇博客的全部内容啦,谢谢大家!