程序员修神之路--缓存架构不够好,系统容易瘫痪

灵魂拷问

  • 缓存能大幅度提高系统性能,也能大幅度提高系统瘫痪几率

  • 怎么样防止缓存系统被穿透?

  • 缓存的雪崩是不是可以完全避免?

前几篇文章我们介绍了缓存的优势以及数据一致性的问题,在一个面临高并发系统中,缓存几乎成了每个架构师应对高流量的首冲解决方案,但是,一个好的缓存系统,除了和数据库一致性问题之外,还存在着其他问题,给整体的系统设计引入了额外的复杂性。而这些复杂性问题的解决方案也直接了影响系统的稳定性,最常见的比如缓存的命中率问题,在一个高并发系统中,核心功能的缓存命中率一般要保持在90%以上甚至更高,如果低于这个命中率,整个系统可能就面临着随时被峰值流量击垮的可能,这个时候我们就需要优化缓存的使用方式了。

听说你还不会缓存?

谈了千百遍的缓存数据的一致性问题

如果按照传统的缓存和DB的流程,一个请求到来的时候,首先会查询缓存中是否存在,如果缓存中不存在则去查询对应的数据库。假如系统每秒的请求量为10000,而缓存的命中率为60%,则每秒穿透到数据库的请求数为4000,对于关系型数据库mysql来说,每秒4000的请求量对于分了一主三从的Mysql数据库架构来说也已经足够大了,再加上主从的同步延迟等诸多因素,这个时候你的mysql已经行走在down机边缘了。

缓存的最终目的,是在保证请求低延迟的情况下,尽最大努力提高系统的吞吐量

那缓存系统可能会影响系统崩溃的原因有那些呢?

缓存穿透

缓存穿透是指:当一个请求到来的时候,在缓存中没有查找到对应的数据(缓存未命中),业务系统不得不从数据库(这里其实可以笼统的成为后端系统)中加载数据

缓存穿透

发生缓存穿透的原因根据场景分为两种:

请求的数据在缓存和数据中都不存在

当数据在缓存和数据库都不存在的时候,如果按照一般的缓存设计,每次请求都会到数据库查询一次,然后返回不存在,这种场景下,缓存系统几乎没有起任何作用。在正常的业务系统中,发生这种情况的概率比较小,就算偶尔发生,也不会对数据库造成根本上的压力。

最可怕的是出现一些异常情况,比如系统中有死循环的查询或者被黑客攻击的时候,尤其是后者,他会故意伪造大量的请求来读取不存在的数据而造成数据库的down机,最典型的场景为:如果系统的用户id是连续递增的int型,黑客很容易伪造用户id来模拟大量的请求。

请求的数据在缓存中不存在,在数据库中存在

这种场景一般属于业务的正常需求,因为缓存系统的容量一般是有限制的,比如我们最常用的Redis做为缓存,就受到服务器内存大小的限制,所以所有的业务数据不可能都放入缓存系统中,根据互联网数据的二八规则,我们可以优先把访问最频繁的热点数据放入缓存系统,这样就能利用缓存的优势来抗住主要的流量来源,而剩余的非热点数据,就算是有穿透数据库的可能性,也不会对数据库造成致命压力。

换句话说,每个系统发生缓存穿透是不可避免的,而我们需要做的是尽量避免大量的请求发生穿透,那怎么解决缓存穿透问题呢?解决缓存的穿透问题本质上是要解决怎么样拦截请求的问题,一般情况下会有以下几种方案:

回写空值

当请求的数据在数据库中不存在的时候,缓存系统可以把对应的key写入一个空值,这样当下次同样的请求就不会直接穿透数据库,而直接返回缓存中的空值了。这种方案是最简单粗暴的,但是要注意几点:

  • 当有大量的空值被写入缓存系统中,同样会占用内存,不过理论上不会太多,完全取决于key的数量。而且根据缓存淘汰策略,可能会淘汰正常的数据缓存项

  • 空值的过期时间应该短一些,比如正常的数据缓存过期时间可能为2小时,可以考虑空值的过期时间为10分钟,这样做一是为了尽快释放服务器的内存空间,二是如果业务产生相应的真实数据,可以让缓存的空值快速失效,尽快做到缓存和数据库一致。

//获取用户信息public static UserInfo GetUserInfo(int userId){//从缓存读取用户信息var userInfo = GetUserInfoFromCache(userId);if (userInfo == null){//回写空值到缓存,并设置缓存过期时间为10分钟CacheSystem.Set(userId, null,10);}return userInfo;}
布隆过滤器

布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

布隆过滤器有几个很大的优势

  • 占用内存非常小

  • 对于判断一个数据不存在百分百正确

具体可以参见之前的文章或者百度脑补一下布隆过滤器:

优雅快速的统计千万级别uv

由于布隆过滤器基于hash算法,所以在时间复杂度上是O(1),在应对高并发的场景下非常合适,不过使用布隆过滤器要求系统在产生数据的时候需要在布隆过滤器同时也写入数据,而且布隆过滤器也不支持删除数据,因为多个数据可能会重用同一个位置。

image

缓存雪崩

缓存雪崩是指缓存中数据大批量同时过期,造成查询数据库数据量巨大,引起数据库压力过大导致系统崩溃。

与缓存穿透现象不同,缓存穿透是指缓存中不存在数据而造成会对数据库造成大量查询,而缓存雪崩是因为缓存中存在数据,但是同时大量过期造成。但是本质上是一样的,都是对数据库造成了大量的请求。

无论是穿透还是雪崩都面临着同样的数据会有多个线程同时请求,同时查询数据库,同时回写缓存的一致性问题。举例来说,当多个线程同时请求用户id为1的用户,这个时候缓存正好失效,那这多个线程同时会查询数据库,然后同时会回写缓存,最可怕的是,这个回写的过程中,另外一个线程更新了数据库,就造成了数据不一致,这个问题在之前的文章中着重讲过,大家一定要注意。

同样的数据会被多个线程产生多个请求是产生雪崩的一个原因,针对这种情况的解决方案是把多个线程的请求顺序化,使其只有一个线程会产生对数据库的查询操作,比如最常见的锁机制(分布式锁机制),现在最常见的分布式锁是用redis来实现,但是redis实现分布式锁也有一定的坑,可以参见之前的文章(如果使用的是Actor模型的话会在无锁的模式下更优雅的实现请求顺序化)

redis做分布式锁可能不那么简单

多个缓存key同时失效的场景是产生雪崩的主要原因,针对这样的场景一般可以利用以下几种方案来解决

设置不同过期时间

给缓存的每个key设置不同的过期时间是最简单的防止缓存雪崩的手段,整体思路是给每个缓存的key在系统设置的过期时间之上加一个随机值,或者干脆是直接随机一个值,有效的平衡key批量过期时间段,消掉单位之间内过期key数量的峰值。

 public static int SetUserInfo(int userId){//读取用户信息var userInfo = GetUserInfoFromDB(userId);if (userInfo != null){//回写到缓存,并设置缓存过期时间为随机时间var cacheExpire = new Random().Next(1, 100);CacheSystem.Set(userId, userInfo, cacheExpire);return cacheExpire;}return 0;}
后台单独线程更新

这种场景下,可以把缓存设置为永不过期,缓存的更新不是由业务线程来更新,而是由专门的线程去负责。当缓存的key有更新时候,业务方向mq发送一个消息,更新缓存的线程会监听这个mq来实时响应以便更新缓存中对应的数据。不过这种方式要考虑到缓存淘汰的场景,当一个缓存的key被淘汰之后,其实也可以向mq发送一个消息,以达到更新线程重新回写key的操作。

缓存的可用性和扩展性

和数据库一样,缓存系统的设计同样需要考虑高可用和扩展性。虽然缓存系统本身的性能已经比较高了,但是对于一些特殊的高并发的热点数据,还是会遇到单机的瓶颈。举个栗子:假如某个明星出轨了,这个信息数据会缓存在某个缓存服务器的节点上,大量的请求会到达这个服务器节点,当到达一定程度的时候同样会发生down机的情况。类似于数据库的主从架构,缓存系统也可以复制多分缓存副本到其他服务器上,这样就可以将应用的请求分散到多个缓存服务器上,缓解由于热点数据出现的单点问题。

和数据库主从一样,缓存的多个副本也面临着数据的一致性问题,同步延迟问题,还有主从服务器相同key的过期时间问题。

至于缓存系统的扩展性同样的道理,也可以利用“分片”的原则,利用一致性哈希算法将不同的请求路由到不同的缓存服务器节点,来达到水平扩展的要求,这一点和应用的水平扩展道理一样。

写在最后

通过以上可以看出,无论是应用服务器的高可用架构还是数据库的高可用架构,还是缓存的高可用其实道理都是类似的,当我们掌握了其中一种就很容易的扩展到任何场景中。如果这篇文章对你有多帮助,请分享给身边的朋友,最后欢迎大家留言写下你们在日常开发中用到的其他关于缓存高可用,可扩展性,以及防止穿透和雪崩的方案,让我们一起进步!!

更多精彩文章

  • ????分布式大并发系列

  • ????架构设计系列

  • ????趣学算法和数据结构系列

  • ????设计模式系列

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

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

相关文章

Magicodes.IE之花式导出

总体设计Magicodes.IE是一个导入导出通用库,支持Dto导入导出以及动态导出,支持Excel、Word、Pdf、Csv和Html。在本篇教程,笔者将讲述如何使用Magicodes.IE进行花式导出。在本篇教程,笔者主要讲述如何使用IE进行花式导出并满足客户…

京东笔试4.2-19:00随笔

30道选择考察到了数据结构,计网,linux,数据库,java基础,就记得这些了 两道编程题 一道二叉树 一道 动态规划 一道也没AC出来 第一道需要会做的前提是 需要创建二叉树 并给其赋值 然后再谈算法 因为一直刷leetcode,转换…

我又踩坑了!如何为HttpClient请求设置Content-Type标头?

最近在重构认证代码,认证过程相当常规:POST /open-api/v1/user-info?client_id&timstamp&rd12345&sign***&methodhmac content-type: application/json payload: { "token":"AA2917B0-C23D-40AB-A43A-4C4B61CC7C74&qu…

利用数组创建二叉树并赋值

1:二叉树的创建与赋值 (1):前言知识 这里的创建是利用层序序列进行创建,主要就是根节点的坐标为i 的话 那么左节点的坐标为 2i1,右节点的坐标为2i2;开辟一个结构体 struct Node {int val;Node * left;Node * right;Node () : val(-1),left(NULL),right(NULL) {};Node(int x)…

2020 中国开源年会(COSCon'20)再启程:开源向善(Open Source for Good)

中国开源年会COSCon2020正式启动!*本图由开源社设计组叶凯设计时间:2020年10月24-25日线上直播地址:bilibili & Youtube讲师互动平台:Zoom时间弹指飞逝,转眼即过去了一年。不知道各位在这多舛的半年间又和开源这二…

使用Azure Functions玩转Serverless

Serverless&Azure Functions通过无服务器计算,开发者无需管理基础结构,从而可以更快构建应用程序。通过无服务器应用程序,将由云服务提供商自动预配、缩放和管理运行代码所需的基础结构。要理解无服务器计算的定义,注意到服务…

java并发之初识

一:并发编程的难点 1:原子性问题 操作系统做任务切换,可以发生在任何一条CPU指令执行完成后;CPU能保证的原子操作是指令级别的,而不是高级语言的操作符; n不是原子操作的,而是3条指令 2:可见性问题 可见性是指一个线程对一个…

java并发之synchronized实现原理及其优化

1:synchronnized概述 synchronized修饰的方法或代码块相当于并发中的临界区,即在同一时刻jvm只允许一个线程进入执行。synchronized是通过锁机制实现同一时刻只允许一个线程来访问共享资源的。另外synchronized锁机制还可以保证线程并发运行的原子性,有…

.NET 是信息技术应用创新产业重要参与者

今天是国庆节,也是中秋节,月满中秋,举国欢庆,在这里祝各位开发者中秋国庆快乐。放假在家就想把这几年对于.NET发展相关生态做个梳理,写一篇文章来总结一下这两年从腾讯出来自己创业,推动.NET在国内的应用的…

蓝桥杯- 包子凑数

一:题目 题目描述 小明几乎每天早晨都会在一家包子铺吃早餐。他发现这家包子铺有 NN 种蒸笼,其中第 ii 种蒸笼恰好能放 A_iA i ​ 个包子。每种蒸笼都有非常多笼,可以认为是无限笼。 每当有顾客想买 XX 个包子,卖包子的大叔就会迅速选出若…

秋天 | 等疫情过后,我们继续背起相机去旅行

这是头哥侃码的第218篇原创2020年,注定是不平凡的一年。八个月前,我正和家人一起沉浸在春节的喜悦中,可没成想一场疫情的到来彻底改变了欢快的气氛。别的倒没什么,只是之前所有的计划全部被打乱了。先说春节假期,和舅舅…

简单理解CAP-BASE

1、CAPCAP是分布式系统的指导理论,是NoSQL数据库的理论基石。CAP其实就是对分布式系统的特性总结,即一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。…

面试必问系列之在浏览器中输入URL后到网页显示 其间发生了什么?

文章目录[TOC](文章目录)1:解析URL网址,从而生成发送给Web服务器的Http请求信息2:真实地址查询-----(DNS域名解析)3:调用协议栈4:可靠的传输 TCP5:远程定位--IP地址6:两点传输--获取MAC地址7:网卡--出口8:送别者--交换机9:出境大门--路由器10:相互扒皮--服务端与客户端1:解析UR…

C# 中的 is 真的是越来越强大,越来越语义化

一:背景 1. 讲故事最近发现 C#7 之后的 is 是越来越看不懂了,乍一看花里胡哨的,不过当我静下心来仔细研读,发现这 is 是越来越短小精悍,而且还特别语义化,那怎是一个爽字了得????,这一篇就和…

leetcode209. 长度最小的子数组(滑动窗口)

一:题目 二:上码 class Solution { public:/**思路:1.滑动窗口解法1>:确定窗口内是什么2>:确定窗口的起始位置3>:确定窗口的终止位置2.窗口:也就是我们的求的连续字符串的和3.窗口的起始位置: 窗口的起始位置最先是我们数组的起始位置,当窗口内连续数组的和大于targe…

开源特训营 - Lesson 4 - 如何运营社区

编辑:李明康责编:袁睿斌作者:课程助教 - 苏斌主讲教师:庄表伟(开源社理事长)、赵生宇(同济大学)本次课程的主题为《如何运营社区》,主讲者为开源社的理事长庄表伟老师和来…

leetcode904. 水果成篮(滑动窗口)

一:题目 二:上码 class Solution { public:/**思路:滑动窗口1.滑动窗口为我们 两个品种水果的个数2.滑动窗口的起始位置为数组的起始位置,当遇见一个新的水果种类的时候,这时候就需要移动我们的起始位置这里移动起始位置是要注意的,我们需要移动起始到我们的窗口中 完全没有 这…

拥抱.NET 5,从自研微服务框架开始

“ 2016年发布了.NET Core第一个正式版本,而.NET5也将在下个月就正式来临了,技术日新月异,也有点让人应接不暇。在框架设计上,.NET Framework的全家桶理念,培养了一大批的CRUD,而.NET Core转变成了按需使用…

leetcode周赛第一题6037. 按奇偶性交换后的最大数字(冒泡法)

一:题目 二:上码 class Solution { public:int largestInteger(int num) {string str to_string(num);for (int i 0; i < str.size()-1; i) {for (int j i1; j < str.size(); j) {int num1 str[j]-0;int num2 str[i]-0;if (num1%2 num2%2) {//这里我们控制的是奇数…

从零开始打造专属钉钉机器人

一、前言废话不多的实现简单的钉钉机器人。我们分为下面几个步骤来逐步讲解。1.钉钉机器人的类型2.如何打造一个最最简单的钉钉机器人二、钉钉机器人的类型常用的钉钉机器人有两种&#xff0c;1、通知类型的机器人&#xff1a;适合在群内进行一些通知&#xff0c;推荐消息等内容…