架构演进:从数据库“裸奔”到多级防护

噗,这个标题是不是有点AI味?哈哈,确实有让AI起名,但只是起了个名,我原来的标题是:“给你的数据接口提提速,聊聊二级缓存的架构设计”

前言

前阵子给项目做了点性能优化,最核心的手段就是加上了二级缓存的设计,趁着今天有机会,我想好好聊聊这个话题。

事实上,我们的业务系统一直在采用这一套基于Redis的缓存策略,但最近我们上线的这套系统,是一个可预见的高并发系统,顺利上线的话,可能比以往任何系统的并发量都要高,所以我觉得仅靠Redis就显得有些力不从心了。在大规模流量冲击下,Redis 的网络 I/O、内网带宽以及频繁的反序列化开销,会逐渐成为压垮 Web 服务器CPU的最后一根稻草。

为了提早消除隐患,我稍微改造了一下原有的缓存架构,增加了二级缓存的设计,这其中也踩了点小坑,拿出来一看聊聊。

架构设计

我这里的系统,缓存架构是基于EasyCaching这个第三方库构建的,当然这个不是核心,如果你不喜欢第三方库,完全可以基于原生api自行构造。

一级缓存(L1)

一级缓存的载体就是内存,这是超高并发场景下最有效的防护罩,从计算机硬件上,它离CPU最近,执行速度极快,也没有网络开销,我觉得市面上所有多级缓存的架构设计,第一层基本都得是内存吧。

二级缓存(L2)

二级缓存就是Redis了,它负责数据共享和持久化支撑,是一级缓存穿透后的坚实后盾。因为我们的系统基本都是分布式部署的,所以为了保证架构的简单性,之前的项目里都是直接用Redis做缓存的,引入L1后,它也如释重负了,在高并发的场景下,它的核心作用主要是确保高可用和平衡多个服务节点的数据一致性了。

三级缓存(L3)

其实三级就不叫缓存,就直接落到数据库了,当二级缓存失效,请求最终还是会来到数据库,我们之前写过的检索逻辑还是一切如常,不需要为多级缓存的设计过度修改,但此时它的压力就更小了,在前面两层防护的保护下,即便是高并发的场景,应付起来也能游刃有余。

架构图*

这个架构图如下,需要说明的是,当2级缓存没有命中的话,并不是直接去数据库查询,因为这里还设计了一个信号锁,而关于信号锁的作用主要是预防缓存击穿,当某个热点Key过期后,只有1个线程去查数据库,其他线程会在信号量处等待,然后直接读取第一个线程查出来的缓存。更多的内容大家可以自行GPT一下。实际理解起来,可以跳过这点,认为L2失效就是打到数据库就可以了。

代码

安装依赖

因为我这里依赖了EasyCaching的生态,所以需要先引入EasyCaching.Memory和EasyCaching.CSRedis。

注意EasyCaching对于Redis的封装包有两个,CSRedis和Redis,CSRedis是国人封装的,对中文系统应该来说更接地气一点,配置更简单,而Redis就是基于StackExchange.Redis,底层实现虽有差别,但在EasyCaching里提供的抽象接口都是一致的,所以用谁都可以。

<PackageReferenceInclude="EasyCaching.InMemory"Version="1.9.2"/><PackageReferenceInclude="EasyCaching.CSRedis"Version="1.9.2"/>

注入服务

services.AddEasyCaching(options=>{options.UseCSRedis(configuration,"redis","Easycaching:redisSentinel")options.UseInMemory(conf=>{conf.MaxRdSecond=2;conf.EnableLogging=false;conf.DBConfig=newEasyCaching.InMemory.InMemoryCachingOptions{SizeLimit=1024};},"memory");});

我这里redis是以哨兵集群的方式接入,配置文件如下

"redisSentinel":{"MaxRdSecond":5,"EnableLogging":false,"LockMs":5000,"SleepMs":300,"SerializerName":"redis","dbconfig":{"ConnectionStrings":["略"],"Sentinels":["节点1","节点2","节点3"],"ReadOnly":false}},

需要注意MaxRdSecond这个参数,我这里设置的默认值在redis里是5,内存里是2,这个参数的意义是EasyCaching为了预防出现缓存雪崩的一个小设计,在写入缓存的时候随机加入一个不大于这个MaxRdSecond的时长,所以这个值是多少,或者需不需要用,还是要看你的项目场景。

接口和实现

publicinterfaceIMultiLevelCacheService{Task<T>GetOrCreateAsync<T>(stringkey,Func<Task<T>>factory,int?l2Seconds=null,CancellationTokenct=default);Task<Result<T>>GetOrCreateForResultAsync<T>(stringkey,Func<Task<Result<T>>>factory,int?l2Seconds=null,CancellationTokenct=default);TaskRemoveAsync(stringkey,CancellationTokenct=default);}publicclassMultiLevelCacheService:IMultiLevelCacheService{privatereadonlyIEasyCachingProvider_l2Provider;// RedisprivatereadonlyIEasyCachingProvider_l1Provider;// MemoryprivatereadonlyILogger<MultiLevelCacheService>_logger;privatereadonlyMultiLevelCacheOptions_options;//本地锁,防止同一个 Key 的缓存失效时,大量请求同时冲向数据库(防击穿)privatestaticreadonlyConcurrentDictionary<string,SemaphoreSlim>_locks=new();publicMultiLevelCacheService(IEasyCachingProviderFactoryfactory,IOptions<MultiLevelCacheOptions>options,ILogger<MultiLevelCacheService>logger){_l2Provider=factory.GetCachingProvider("redis");_l1Provider=factory.GetCachingProvider("memory");_logger=logger;_options=options.Value;}publicasyncTask<Result<T>>GetOrCreateForResultAsync<T>(stringkey,Func<Task<Result<T>>>factory,int?l2Seconds=null,CancellationTokenct=default){//预处理过期时间NormalizeL2Seconds(refl2Seconds);varl1Seconds=CalculateL1Seconds(l2Seconds!.Value);//尝试从一级缓存读取 (最快)varl1Result=await_l1Provider.GetAsync<T>(key,ct);if(l1Result.HasValue){ConsoleHelper.WriteLine("1级缓存命中:"+key,ConsoleColor.DarkGreen);returnResult<T>.Success(l1Result.Value);}//尝试从二级缓存读取try{varl2Result=await_l2Provider.GetAsync<T>(key,ct);if(l2Result.HasValue){ConsoleHelper.WriteLine("2级缓存命中:"+key,ConsoleColor.DarkBlue);//取 Redis 剩余 TTL 和配置上限的最小值,塞回L1varttl=await_l2Provider.GetExpirationAsync(key,ct);varremainingL1=NormalizeRemainingSeconds(ttl,l1Seconds);await_l1Provider.SetAsync(key,l2Result.Value,TimeSpan.FromSeconds(remainingL1),ct);returnResult<T>.Success(l2Result.Value);}}catch(Exceptionex){_logger.LogWarning(ex,"L2 缓存读取异常,Key: {Key}",key);}//防击穿加锁回源获取或创建针对该 Key 的信号量varsemaphore=_locks.GetOrAdd(key,_=>newSemaphoreSlim(1,1));awaitsemaphore.WaitAsync(ct);try{//在获取锁的期间,可能上一个线程已经把缓存写好了vardoubleCheck=await_l1Provider.GetAsync<T>(key,ct);if(doubleCheck.HasValue)returnResult<T>.Success(doubleCheck.Value);ConsoleHelper.WriteLine("缓存未命中,回源加载数据:"+key,ConsoleColor.DarkYellow);//执行回源业务逻辑varfreshResult=awaitfactory();//成功则写入双级缓存if(freshResult.IsSuccess){awaitWriteBothAsync(key,freshResult.Value,l2Seconds.Value,l1Seconds,ct);}returnfreshResult;}finally{semaphore.Release();//如果没有人在等待这个锁了,可以从字典中移除(节省内存)if(semaphore.CurrentCount>0)_locks.TryRemove(key,out_);}}publicasyncTask<T>GetOrCreateAsync<T>(stringkey,Func<Task<T>>factory,int?l2Seconds=null,CancellationTokenct=default){//逻辑与上面类似,仅返回值处理不同,此处略varresult=awaitGetOrCreateForResultAsync(key,async()=>{varval=awaitfactory();returnResult<T>.Success(val);},l2Seconds,ct);returnresult.Value;}publicasyncTaskRemoveAsync(stringkey,CancellationTokenct=default){awaitTask.WhenAll(_l1Provider.RemoveAsync(key,ct),_l2Provider.RemoveAsync(key,ct));}}

大概解释下,我这里主要用到的方法实现是GetOrCreateForResultAsync,因为我的数据接口场景里,数据回传到接口层时,外层包了一个统一的Result,接口案例如下

[HttpGet("GetArticleDetail/{id}")]publicasyncTask<IActionResult>GetArticleDetail(longid){stringcacheKey=ApiCachePrefixKeys.BuildKey(ApiCachePrefixKeys.DecArticles,id);;varresult=await_multiLevelCacheService.GetOrCreateForResultAsync(cacheKey,()=>_decArticleRepo.GetArticleDetail(id));//不用缓存时//var result = await _decArticleRepo.GetArticleDetail(id);if(result.IsSuccess){returnOk(ApiResult.Success(result));}returnAccepted(ResultExtensions.ToApiResult(result));}

这是一个读取文章的案例,增加缓存机制后,需要将编译后的委托缓存起来,所以写法看起来是现在这个样子。

如果不需要外层包Result,直接拿到数据对象,那就可以直接使用GetOrCreateAsync就好。

执行效果

最后,看一下执行的效果,第一次请求数据,未命中缓存

此时Redis里可以看到我们刚刚缓存的数据

接下来马上进行第二次请求,如期命中一级缓存L1

再过一小会儿,在请求,按预期命中二级缓存L2

至此,我们的缓存架构就基本完成了,而删除缓存的案例就不演示了,

一个小坑*

如同我前面说到我的场景里特殊的返回值类型,目前是缓存的编译后的委托,而在这之前,为了追求方便我使用的表达式函数,像下面这样

varfreshResult=awaitmethodCall.Compile()();

看起来也不错,而且注意这是2个括号,第一个括号是把表达式函数编译成委托,返回一个Func<Task>,第二个括号才是的到这个函数之后的执行。而问题也就出在这两个“括号”上,这样看起来优雅,实际上隐藏着很大的性能隐患,就在第一个括号执行的时候,会调用CPU执行IL编译,尽管有前面2层缓存做防护,真到了高并发的场景,一下子多次执行编译工作,CPU也得冒烟,这就得不偿失了,最后改成了现在的样子。当然肯定不是所有小伙伴都能遇到这个问题,但如果恰好遇到,又恰好也看到这,只能说咱们太有缘了,点个关注吧哈哈。

结语

最后,我经常听到一些阴阳怪气的声音,什么“就你这业务量,还搞多级缓存?””不想着早点交差,一天在这些地方浪费时间有什么用“…巴拉巴拉。
我说想,有这种声音的人,如果你真的理解技术,理解业务,也亲身验证过这个技术不适合你的业务,那你说什么都OK。
而现实情况是,大部分所谓的专家,基本都不做验证性工作,他们只想着交差,恨不得代码写完再也不改,连自己写的代码都不想多看一眼。。。这种人发出的这种声音,我的态度是,当放屁就好,千万别让他们阻挡了我们的好奇心,对自己产生怀疑,他们的话不值一提。
再聊回多级缓存,这从来都不是高深技术,哪怕是一个日活几百的小系统,只要存在重复读、热点数据或对响应速度有要求,或者作为开发者的你不甘于只做简单架构,那从设计一个多级缓存模块开始吧,它一定可以给你和你的系统带来立竿见影的体验提升。
还有,即便是小项目,你能保证它一辈子当个“小项目”吗?架构设计不是一锤子买卖,而是在演进中预留弹性,提前埋下合理的扩展点,这远比在流量突增时连滚带爬的救火要从容得多。有偏见的开发者,永远写不出好用的系统。
好了,至此,这个多级缓存的话题差不多就聊完了,下次再见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1175554.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qwen3-1.7B微调前后对比,效果提升一目了然

Qwen3-1.7B微调前后对比&#xff0c;效果提升一目了然 1. 引言&#xff1a;为何要对Qwen3-1.7B进行微调&#xff1f; 随着大语言模型在垂直领域应用的不断深入&#xff0c;通用预训练模型虽然具备广泛的知识覆盖能力&#xff0c;但在特定专业场景&#xff08;如医疗、法律、金…

从口语到标准格式|用FST ITN-ZH镜像实现中文逆文本精准转换

从口语到标准格式&#xff5c;用FST ITN-ZH镜像实现中文逆文本精准转换 在语音识别和自然语言处理的实际应用中&#xff0c;一个常被忽视但至关重要的环节是逆文本标准化&#xff08;Inverse Text Normalization, ITN&#xff09;。当用户说出“二零零八年八月八日”或“早上八…

边缘太生硬?开启羽化让AI抠图更自然流畅

边缘太生硬&#xff1f;开启羽化让AI抠图更自然流畅 1. 背景与技术痛点 在图像处理、电商展示、社交媒体内容创作等场景中&#xff0c;高质量的图像抠图是提升视觉表现力的关键环节。传统手动抠图依赖专业设计工具和大量人力操作&#xff0c;效率低下&#xff1b;而早期自动抠…

Wan2.2部署实战:医疗科普动画AI生成的内容合规性把控

Wan2.2部署实战&#xff1a;医疗科普动画AI生成的内容合规性把控 1. 引言 随着人工智能技术的快速发展&#xff0c;文本到视频&#xff08;Text-to-Video&#xff09;生成模型在内容创作领域展现出巨大潜力。特别是在医疗科普场景中&#xff0c;如何高效、准确且合规地生成可…

Qwen3-Embedding-4B镜像推荐:开箱即用的向量服务方案

Qwen3-Embedding-4B镜像推荐&#xff1a;开箱即用的向量服务方案 1. 背景与需求分析 随着大模型在检索增强生成&#xff08;RAG&#xff09;、语义搜索、多模态理解等场景中的广泛应用&#xff0c;高质量文本嵌入&#xff08;Text Embedding&#xff09;能力已成为构建智能系…

Qwen3-Embedding-4B省钱策略:低峰期调度部署方案

Qwen3-Embedding-4B省钱策略&#xff1a;低峰期调度部署方案 1. 背景与问题提出 在大规模语言模型日益普及的今天&#xff0c;向量嵌入服务已成为检索增强生成&#xff08;RAG&#xff09;、语义搜索、推荐系统等应用的核心基础设施。Qwen3-Embedding-4B 作为通义千问系列中专…

小白必看!一键配置Linux开机启动脚本的保姆级指南

小白必看&#xff01;一键配置Linux开机启动脚本的保姆级指南 1. 引言&#xff1a;为什么需要开机启动脚本&#xff1f; 在实际的 Linux 系统运维和开发中&#xff0c;我们常常需要某些程序或脚本在系统启动时自动运行。例如&#xff1a; 启动一个后台服务&#xff08;如 Py…

Qwen2.5-7B显存优化方案:16GB GPU高效运行实战

Qwen2.5-7B显存优化方案&#xff1a;16GB GPU高效运行实战 1. 引言 1.1 业务场景描述 随着大语言模型在实际应用中的广泛落地&#xff0c;如何在有限硬件资源下高效部署高性能模型成为工程团队的核心挑战。通义千问Qwen2.5-7B-Instruct作为最新一代70亿参数级别的指令微调模…

企业级应用:BERT语义填空服务部署最佳实践

企业级应用&#xff1a;BERT语义填空服务部署最佳实践 1. 引言 1.1 业务场景描述 在现代企业级自然语言处理&#xff08;NLP&#xff09;应用中&#xff0c;语义理解能力正成为智能客服、内容辅助创作、教育测评等系统的核心竞争力。其中&#xff0c;语义填空作为一种典型的…

亲测PyTorch-2.x-Universal-Dev-v1.0镜像,Jupyter开箱即用太省心

亲测PyTorch-2.x-Universal-Dev-v1.0镜像&#xff0c;Jupyter开箱即用太省心 1. 镜像核心价值与使用场景 在深度学习开发过程中&#xff0c;环境配置往往是最耗时且最容易出错的环节。无论是依赖版本冲突、CUDA驱动不匹配&#xff0c;还是Jupyter内核无法识别虚拟环境&#x…

自动化翻译平台开发:HY-MT1.5-7B全流程集成指南

自动化翻译平台开发&#xff1a;HY-MT1.5-7B全流程集成指南 1. 引言 随着全球化进程的加速&#xff0c;跨语言沟通已成为企业、开发者乃至个人日常工作的核心需求。传统商业翻译API虽然成熟&#xff0c;但在定制性、成本控制和数据隐私方面存在局限。近年来&#xff0c;开源大…

Unsloth与Hugging Face生态无缝集成使用体验

Unsloth与Hugging Face生态无缝集成使用体验 1. 引言&#xff1a;高效微调时代的到来 在大语言模型&#xff08;LLM&#xff09;快速发展的今天&#xff0c;如何以更低的成本、更高的效率完成模型的定制化微调&#xff0c;成为开发者和研究者关注的核心问题。Unsloth作为一款…

【Java 开发日记】我们来说一下 synchronized 与 ReentrantLock 1.0

【Java 开发日记】我们来说一下 synchronized 与 ReentrantLock 二、详细区别分析 1. 实现层面 synchronized&#xff1a; Java 关键字&#xff0c;由 JVM 底层实现&#xff08;通过 monitorenter/monitorexit 字节码指令&#xff09;。 锁信息记录在对象头的 Mark Word 中。…

亲测PETRV2-BEV模型:星图AI平台训练3D检测效果超预期

亲测PETRV2-BEV模型&#xff1a;星图AI平台训练3D检测效果超预期 1. 引言&#xff1a;BEV感知新范式下的高效训练实践 随着自动驾驶技术的快速发展&#xff0c;基于多摄像头图像的鸟瞰图&#xff08;Birds Eye View, BEV&#xff09;感知已成为3D目标检测的核心方向。传统方法…

混元翻译模型再升级|HY-MT1.5-7B本地化部署全攻略

混元翻译模型再升级&#xff5c;HY-MT1.5-7B本地化部署全攻略 1. 引言&#xff1a;为何选择HY-MT1.5-7B进行本地化部署&#xff1f; 随着全球化交流的不断深入&#xff0c;高质量、低延迟的翻译服务需求日益增长。传统的云端翻译API虽然便捷&#xff0c;但在隐私保护、网络依…

Java SpringBoot+Vue3+MyBatis 保信息学科平台系统源码|前后端分离+MySQL数据库

摘要 随着信息技术的快速发展&#xff0c;高等教育领域对信息化管理的需求日益增长。信息学科作为现代教育体系的重要组成部分&#xff0c;其教学资源、科研数据和学术交流的高效管理成为亟待解决的问题。传统的信息管理方式依赖人工操作&#xff0c;存在效率低、易出错、数据共…

企业级大学城水电管理系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】

摘要 随着高校规模的不断扩大和师生人数的持续增长&#xff0c;大学城的水电资源管理面临着日益复杂的挑战。传统的人工管理方式效率低下&#xff0c;容易出现数据错误和资源浪费&#xff0c;难以满足现代化管理的需求。水电资源的合理分配与监控成为高校后勤管理的重要课题&am…

告别Whisper!SenseVoiceSmall中文识别快又准

告别Whisper&#xff01;SenseVoiceSmall中文识别快又准 1. 引言&#xff1a;语音识别进入“富理解”时代 随着大模型技术的深入发展&#xff0c;语音识别已不再局限于“将声音转为文字”的基础功能。用户对语音交互系统提出了更高要求&#xff1a;不仅要听得清&#xff0c;更…

PyTorch-2.x-Universal-Dev-v1.0部署教程:将本地代码同步到远程容器

PyTorch-2.x-Universal-Dev-v1.0部署教程&#xff1a;将本地代码同步到远程容器 1. 引言 1.1 学习目标 本文旨在帮助深度学习开发者快速掌握如何在 PyTorch-2.x-Universal-Dev-v1.0 镜像环境中&#xff0c;将本地开发的模型代码高效、安全地同步至远程 GPU 容器&#xff0c;…

实战应用:用Whisper-large-v3快速搭建智能会议记录系统

实战应用&#xff1a;用Whisper-large-v3快速搭建智能会议记录系统 在现代企业协作中&#xff0c;高效、准确的会议记录已成为提升沟通效率的关键环节。传统的人工记录方式不仅耗时耗力&#xff0c;还容易遗漏关键信息。随着AI语音识别技术的发展&#xff0c;基于OpenAI Whisp…