缓存

最近在狂补优化方面的知识,缓存也是优化的一大方向。之前关于缓存只是知道它的功能,再多不知道了,这里整理缓存相关的知识,算是优化入门吧。

相关概念

  • 缓存

    是“存贮使用频繁的数据的临时地方,因为取原始数据的代价太大了,所以我可以取得快一些。”

    缓存可以认为是数据的池,这些数据是从数据库里的真实数据复制出来的,并且为了能别取回,被标上了标签(键 ID)。

  • 缓存分类

    • 硬件缓存

      它指的是一块芯片,可以被集成到硬盘或者是CPU上。它的作用就是充当硬盘(CPU)与外界接口(通常是内存)之间的暂存器。利用缓存可以减轻系统的负荷,同时提高数据的传输速率,如硬盘缓存和CPU缓存。

    • 客户端缓存

      某些应用,如浏览器,手机淘宝等,为了实现能够快速响应用户的请求,会把用户之前浏览的东西(如图片等)存在本地。在下次访问时,如果本地的缓存里有请求的内容,那么就直接展示出来,不用再次向服务器请求。

    • 服务端缓存

      它与客户端缓存目的相同,只不过是站在服务器这边考虑的。如果每次接到客户端请求都要连接一次数据库,当用户请求多的时候,负载过大。这时可以把一些经常被请求的数据存放在内存中,当有请求时直接返回,不用经过数据库。这样就可以减轻数据库的负担。

  • 缓存的工作原理

    当客户端向服务器请求一个资源时,服务器首先在缓存中找,如果在缓存中,那么直接返回,不需要连接数据库;如果请求的资源不在缓存中,这时再去数据库中找,找到后返回给客户端,并将这个资源加入缓存中。

  • 命中(HIT)

    当客户发端起一个请求,如果被请求的资源在缓存中,这个资源就会被使用,我们就叫它缓存命中。

  • 未命中(MISS)

    当客户端发起一个请求,如果没有在缓存中找到,我们称这种情况为缓存未命中。这时就需要查询数据库,并且将查询结果加入缓存中。 

  • 存储成本

    当未命中时,我们会从数据库取出数据,然后加入缓存。把这个数据放入缓存所需要的时间和空间,就是存储成本。

  • 失效

    当缓存中的数据需要更新时,就意味着缓存中的这个数据失效了。

    一种是数据库中的数据发生了变化,那么如果这个数据在缓存,相应的缓存数据也要同步更新。例如原来 id=1 的编程语言叫 Java,结果管理员修改成了 PHP,那么缓存里面的 id=1 的值就要同步成最新的值。

    另一种情况是该缓存过了失效时间。因为缓存会占用内存,太多的缓存是不合理的,我们可以通过设置失效时间的方式让缓存定时过期失效。

  • 失效策略(缓存算法)

    如果缓存满了,而当前请求又没有命中缓存,那么就会按照某一种策略,把缓存中的某个旧资源剔除,而把新的资源加入缓存。这些决定应该剔除哪个旧资源的策略统称为失效策略。

  • 替代策略

    当缓存没有命中时,并且缓存容量已经满了,就需要在缓存中踢出一个老的条目,加入一条新的条目,而到底应该踢出什么条目,就由替代策略决定。

  • 最优替代策略

    最优的替代策略就是想把缓存中最没用的条目给踢出去,但是未来是不能够被预知的,所以这种策略是不可能实现的。但是有很多策略,都是朝着这个目前去努力。

缓存算法

  • 原则上,失效策略需要考虑到以下这几点

    • 成本

      如果缓存对象有不同的存储成本,应该把那些难以获得的对象保存下来;

    • 容量

      如果缓存对象有不同的大小,应该把那些大的缓存对象清除,让更多的小缓存对象进来了;

    • 时间

      一些缓存设定有过期时间,应该在到时间之后将它们失效。
      应该根据缓存对象的性质选择合适的失效策略,常见的失效策略有这么几种:

      关于常见的缓存算法见博客:缓存那些事–常用的缓存失效策略

缓存中存在的问题

在缓存中存在缓存击穿、缓存雪崩、缓存并发等问题

缓存击穿

  • 问题描述

    缓存穿透是指查询一个一定不存在的数据,此时缓存是不被把命中的,那么就需要查数据库来获取这个值并写入缓存后返回,但是由于在数据库中也没有这个值,所以结果也不会写入缓存中。这将导致这个存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

    在流量大的时候,可能数据库就崩溃了。另外要是有人利用不存在的 key 频繁请求数据库,这就是系统的漏洞。

  • 解决方法:常见的有两种:过滤器和设定指定值。

    (1)过滤器

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

    (2)设定指定值

    这种方法比较巧妙,如果一个查询返回的数据为空(可能由于数据不存在,也可能是系统故障),我们仍然把这个空结果进行缓存,缓存的值设定为一个指定值,同时设置它的过期时间很短,最长不超过五分钟。(因为缓存会占用内存,长时间缓存一个不存在的值比较耗资源。)在这五分钟内,这个值可能由于写入操作从而不再是一个不存在的值,这是就要更新缓存,用真实值替代指定值。

    比如,设定不存在的值缓存为 &&,也就是 “key” ===> “&&”。当返回这个 && 值的时候,就可以认为这是不存在的 key,然后决定是继续等待访问,还是放弃掉这次操作。如果继续等待访问,那么经过一个时间轮询点后,再次请求这个 key,如果取到的值不再是 &&,则可以认为这时候 key 有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。

缓存雪崩

  • 问题描述

    缓存雪崩指的是在某一个时刻,大量缓存同时失效,请求全部转到了数据库,导致数据库压力过大。

    引起这个问题的主要原因还是高并发。平时我们设定一个缓存的过期时间时,可能有一些会设置1分钟后或5分钟后。当并发很高时,会出在某一个时间同时生成了很多的缓存,并且过期时间都一样,这个时候就可能引发一当过期时间到后,这些缓存同时失效,请求全部转发到 DB ,DB 可能会压力过重。

    当发生大量的缓存穿透,例如对某个失效的缓存的高并发访问也会造成缓存雪崩。

  • 解决方法

    缓存失效时的雪崩效应对底层系统的冲击非常可怕。遗憾的是,这个问题目前并没有很完美的解决方案。

    一种常用的方法是,用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。

    另一种方法是合理设计的缓存过期时间,将数据失效时间均匀地分布在时间轴上,一定程度上能够避免缓存同时失效带来的雪崩效应。例如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存并发

  • 问题描述

    有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询 DB,同时设置缓存的情况,如果并发确实很大,这也可能造成 DB 压力过大,还有缓存频繁更新的问题。

  • 解决方法

    常用的解决方法是对缓存查询加锁,如果 KEY 不存在,就加锁,然后查 DB 后写入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入 DB 查询。

    这种解决方式会造成部分请求等待。

缓存算法

缓存算法是指令的一个明细表,用于决定缓存系统中哪些数据应该被删去。

缓存需要被清理时(比如空间占用已经接近临界值了),需要使用某种淘汰算法来决定清理掉哪些数据。常用的淘汰算法有下面几种:

  • FIFO:First In First Out,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰。
  • LRU:Least Recently Used,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。
  • LFU:Least Frequently Used,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。
  • ARC自适应缓存替换算法(ARC):

    在IBM Almaden研究中心开发,这个缓存算法同时跟踪记录LFU和LRU,以及驱逐缓存条目,来获得可用缓存的最佳使用。

  • MRU:Most Recently Used,最近最常使用算法,和 LRU 正好相反。它会移除最近最多被使用的对象。

LRU类算法对比
由于不同的访问模型导致命中率变化较大,此处对比仅基于理论定性分析,不做定量分析

对比点对比
命中率LRU-2 > MQ(2) > 2Q > LRU
复杂度LRU-2 > MQ(2) > 2Q > LRU
代价LRU-2 > MQ(2) > 2Q > LRU

LRU

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

  • 实现,最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

    • 新数据插入到链表头部;
    • 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
    • 当链表满的时候,将链表尾部的数据丢弃。
  • 命中率:当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

  • 复杂度:实现简单。

  • 代价:命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

MySQL的InnoDB引擎设置有索引及数据缓存池,其中用到的LRU算法来维持缓存的命中率

LRU-K

  • 原理

    LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

  • 实现

    相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下:

    • 数据第一次被访问,加入到访问历史列表;
    • 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
    • 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;
    • 缓存数据队列中被再次访问后,重新排序;
    • 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。

  • 命中率:LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。

  • 复杂度:LRU-K队列是一个优先级队列,算法复杂度和代价比较高。

  • 代价:由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多;当数据量很大的时候,内存消耗会比较可观。

LRU-K需要基于时间进行排序(可以需要淘汰时再排序,也可以即时排序),CPU消耗比LRU要高。

参考:

  1. 缓存那些事–什么是缓存,为什么要用缓存
  2. 缓存那些事–缓存中常见的问题
  3. 缓存那些事–分布式缓存
  4. 缓存、缓存算法和缓存框架简介
  5. 缓存淘汰算法–LRU算法
  6. 缓存那些事–常用的缓存失效策略

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

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

相关文章

mysql中as用法

重命名列名或者表名。as可理解为:用作、当成,作为;一般是重命名列名或者表名。(主要为了查询方便) 重命名列。如:表text, 列 column_1,column_2 你可以写成: 1select column_1 as …

C#中window窗体和控件

C#中window窗体和控件 布局与事件–界面的基本问题 布局 就是拉动窗体的时候,按钮也在跟着变动。 事件 //简单的计算器 using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; namespa…

PHP自动加载机制

概述首先,为什么PHP需要自动加载呢? 在PHP面向对象(OO)编程中,为了方便管理,我们都会把一个类写在一个单独的文件中,那么如果想在A类中使用B类的功能,就需要把B类加载到A类。对于这样的需求在最原始的时候&…

操作系统知识——互斥和死锁

银行家算法银行家算法是最有代表性的避免死锁算法,是Dijkstra提出的。这是由于该算法能用于银行系统现金贷款的发放而得名。 我们可以把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户…

C#常用的控件

C#常用的控件 窗体 菜单、工具栏、对话框 用户控件及控件的继承

C++输入函数的应用

最近因为找工作的事情,在刷一些编程题,也陆陆续续参加了一些笔试,一般编程中使用的语言是C,因为编程题会有时间的限制,C的效率最高,不容易出现超时的问题所以使用最多的语言就是C。 在编程中,其…

C#绘图及图像

C#绘图及图像 绘图的基本方法 绘图的应用

PHP开发API

引言如何使用 PHP 开发 API(Application Programming Interface,应用程序编程接口) 呢? 做过 API 的人应该了解,其实开发 API 比开发 WEB 更简洁,但可能逻辑更复杂,因为 API 其实就是数据输出&a…

热部署和热加载

2018-8-22今天入职第二天,看到老大在群里发了一个go热加载实现的链接,之前没有听说过热加载,所以搜索了一下,下面是关于热部署和热加载的相关整理。 引言在项目线上更新时,可能会遇到需要热部署的情况,虽然…

C#实现定时器

C#实现定时器 方法一 布局 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms;namespace…

HTTP请求流程

hhtp请求流程首先http是一个应用层的协议,在这个层的协议,只是一种通讯规范,也就是因为双方要进行通讯,大家要事先约定一个规范。 http请求与响应的步骤如下: URL解析、域名解析 输入URL的时候,首先浏览器会…

PHP执行外部程序的方法

引言在一些特殊情况下,会使用PHP调用外部程序执行,比如:调用shell命令、shell脚本、可执行程序等等 PHP执行外部程序的方法 system(),system函数将执行的结果直接输出exec():执行一个外部程序,将输出结果保…

C语言函数变量的引用

C语言函数变量的引用 有时候&#xff0c;你想引用函数中的某些变量。 #include <stdio.h>int main() {struct guoba{int pang;int piqi;};void tong(int *p){int month1 3;int month5 8;struct guoba xiaoguoba;xiaoguoba.pang month1 month5;*p xiaoguoba.pang ;…

php 终止程序的方法——return、exit()、die()

php中终止程序的运行有三种方式&#xff1a; return&#xff0c;主脚本程序中使用exit()&#xff0c;脚本中使用die()&#xff0c; 脚本中使用三者所在的脚本文件他们之后的代码都不会执行了。php开发模式下想直接看到变量的值使用exit()、die()。 returnreturn是语言结构而不是…

Eric Evans说DDD还未结束

在Explore DDD 2018大会上&#xff0c;Eric Evans做了“怀疑、乐观和实用主义”的主题演讲&#xff0c;他在演讲中表示&#xff0c;“DDD还没有结束”。在“领域驱动设计”这本书出版十五年之后&#xff0c;作者对这个想法长期以来如此流行表示了惊讶之情。他强调&#xff0c;D…

mark

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

C语言的进阶-指针的应用

指针的应用 #include <stdio.h> void swap(int *p,int *q); int main() {int a 5;int b 8;swap(&a,&b);printf("a%d,b%d\n",a,b);return 0; }void swap(int *p,int *q) {int t *p;*p *q;*q t; }函数只能返回一个值&#xff01; 可以通过参数返回多…

Go简单开始

Go是一门编译型语言&#xff0c;Go语言的工具链将源代码及其依赖转换成计算机的机器指令2。Go语言提供的工具都通过一个单独的命令 go 调用&#xff0c; go 命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件&#xff0c;链接库文件&am…

C语言进阶-指针与数组

C语言进阶-指针与数组 q可以改变指向的内容&#xff0c;但不能改变指向的地址 p可以改变指向地址&#xff0c;但不能改变指向内容