缓存 订阅
缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。 展开全文
缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。
信息
外文名
cache
类    别
高速缓存、磁盘缓存
原    理
程序局部性原理
中文名
缓存
学    科
计算机
目    的
提高数据存取速度
缓存简介
缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。L1 Cache(一级缓存)是CPU第一层高速缓存。内置的L1高速缓存的容量和结构对CPU的性能影响较大,不过高速缓冲存储器均由静态RAM组成,结构较复杂,在CPU管芯面积不能太大的情况下,L1级高速缓存的容量不可能做得太大。一般L1缓存的容量通常在32—256KB。L2 Cache(二级缓存)是CPU的第二层高速缓存,分内部和外部两种芯片。内部的芯片二级缓存运行速率与主频相同,而外部的二级缓存则只有主频的一半。L2高速缓存容量也会影响CPU的性能,原则是越大越好,普通台式机CPU的L2缓存一般为128KB到2MB或者更高,笔记本、服务器和工作站上用CPU的L2高速缓存最高可达1MB-3MB。由于高速缓存的速度越高价格也越贵,故有的计算机系统中设置了两级或多级高速缓存。紧靠内存的一级高速缓存的速度最高,而容量最小,二级高速缓存的容量稍大,速度也稍低 [1]  。缓存只是内存中少部分数据的复制品,所以CPU到缓存中寻找数据时,也会出现找不到的情况(因为这些数据没有从内存复制到缓存中去),这时CPU还是会到内存中去找数据,这样系统的速率就慢下来了,不过CPU会把这些数据复制到缓存中去,以便下一次不要再到内存中去取。随着时间的变化,被访问得最频繁的数据不是一成不变的,也就是说,刚才还不频繁的数据,此时已经需要被频繁的访问,刚才还是最频繁的数据,又不频繁了,所以说缓存中的数据要经常按照一定的算法来更换,这样才能保证缓存中的数据是被访问最频繁的。
收起全文
精华内容
参与话题
问答
  • 缓存的基本概念和常用的缓存技术

    万次阅读 2018-04-04 11:15:22
    摘要: 介绍缓存的基本概念和常用的缓存技术,给出了各种技术的实现机制的简单介绍和适用范围说明,以及设计缓存方案应该考虑的问题(共17页)1 概念1.1 缓存能解决的问题· 性能——将相应数据存储起来以避免数据...

    摘要: 介绍缓存的基本概念和常用的缓存技术,给出了各种技术的实现机制的简单介绍和适用范围说明,以及设计缓存方案应该考虑的问题(共17页)

    1         概念

    1.1   缓存能解决的问题

    · 性能——将相应数据存储起来以避免数据的重复创建、处理和传输,可有效提高性能。比如将不改变的数据缓存起来,例如国家列表等,这样能明显提高web程序的反应速度;

    · 稳定性——同一个应用中,对同一数据、逻辑功能和用户界面的多次请求时经常发生的。当用户基数很大时,如果每次请求都进行处理,消耗的资源是很大的浪费,也同时造成系统的不稳定。例如,web应用中,对一些静态页面的呈现内容进行缓存能有效的节省资源,提高稳定性。而缓存数据也能降低对数据库的访问次数,降低数据库的负担和提高数据库的服务能力;

    · 可用性——有时,提供数据信息的服务可能会意外停止,如果使用了缓存技术,可以在一定时间内仍正常提供对最终用户的支持,提高了系统的可用性。

    1.2   理解状态

    在深入介绍缓存技术之前,需要对状态有一个认识,因为缓存可以说是状态管理的框架。理解状态的含义和它的一些特性——比如生存期和生存范围——对决定是否缓存和选择合适的缓存技术有很大帮助。状态是指一些数据,在应用系统某个时间点上,数据的状态和条件。这些数据可能是永久性的存储在数据库中,可能是只在内存里停留一会,也可能是按照某个逻辑存活(比如多长时间后释放),它的应用范围可能是所有用户可访问,可能是单个用户有权限;

    1.2.1  状态的生存期

    生存期是指数据保持有效性的时间区间,也就是从创建到移除的时间间隔。通常的生存期有以下几种:

    ·永久状态Permanent State——应用程序使用的永久数据;

    ·进程状态Process State——只在进程周期内有效;

    ·会话状态Session State——和特定的用户会话有关;

    ·消息状态Message State——处理某个消息的时间内有效;

    1.2.2  状态的范围

    状态的范围指对该状态有访问权限的物理或逻辑范围。

    ·物理范围指可以被访问到的状态数据存放的物理位置,通常包括:

    1、  组织Organization——在一个组织内的所有应用程序可以访问状态数据;

    2、  场Farm——在应用场范围内的任何机器上都可以访问;

    3、  机器Machine——单个机器范围内可以访问;

    4、  进程Process——进程内的访问许可;

    5、  应用域AppDomain——应用程序域内的访问许可。

    ·逻辑范围指可访问状态数据的逻辑范围,常见的有:

    1、  应用程序Application;

    2、  业务流程Business Process;

    3、  角色Role;

    4、  用户User;

    1.2.3  状态数据的陈旧

    缓存的状态数据只是主数据(Master State Data)的快照,由于数据源可能被修改,所以状态数据就有会陈旧的特性。合理利用此特性和将数据陈旧的负面影响最小化是缓存状态数据的一个重要任务。你可以以一下方式定义数据的陈旧依据:

    ·主数据更改的可能性——随着时间的推进,主数据更改的可能是否大大增加?安照这一点来决定缓存状态数据的陈旧;

    ·更改的相关性——主数据更新时,缓存的状态数据不相应更新是不是造成影响系统的使用?比如,更改系统的外观风格并不会对业务造成很大影响。

    1.2.4  状态数据陈旧的容忍度

    缓存状态数据的陈旧对业务流程的影响称为容忍度,应用系统的可以为不能容忍(No Tolerance)和一定程度的容忍(some Tolerance),前者必须和主数据同步更新,后者允许一定时间或一定范围的陈旧,判断标准就是对业务流程的影响度。

    1.2.5  理解状态数据的转换过程

    状态的另一个属性是在不同阶段的表现形式。在数据库中存储的是原始格式的数据,业务流程中的是处理过的数据,给最终用户呈现的则是另外的形式。如下表所示:

    表现形式

    描述

    举例

    原始数据

    数据的原始形式

    如数据库中的数据

    处理过的数据

    业务流程中对原始数据加工后的数据

    业务过程中的数据形式

    呈现形式

    可呈现给最终用户的形式

    HTML或可理解的文字说明

    当决定缓存数据时,应该考虑缓存哪个阶段(哪种形式)的状态数据。以下方针有助于你做决定:

    · 当业务逻辑可以容忍缓存数据的陈旧时就缓存原始数据;原始数据可以缓存在数据库访问组件和服务代理中;

    ·缓存处理过的数据以减少处理时间和资源;处理过的数据可以缓存在业务逻辑组件和服务接口中。

    ·当需要呈现的数据量很大并且控件的呈现时间很长时,缓存呈现数据(比如包含大数据量的Treeview控件)。这种数据应该被缓存在UI控件中。

    1.3   为什么要缓存数据

    在应用程序中缓存数据有以下好处:

    ·减少交互的通讯量——缓存数据能有效减少在进程和机器间的传输量;

    ·降低系统中的处理量——减少处理次数;

    ·降低需要做的磁盘访问次数——比如缓存在内存中的数据。

    1.4   数据应该被缓存在哪里

    缓存数据只是一份主数据的拷贝,它可能在内存中或以不同的表现形式保存在硬盘上,也就是说,离说句的使用者越近越好。所以,除了考虑要缓存哪些数据以外,数据缓存在哪里也是一个主要的考量点。这个问题分为以下两个范围:

    1、  存储类型Storage Type——数据可用的物理存储位置;

    2、  层间的架构元素(Layered architecture elements)——数据可用的逻辑存储位置。

    1.4.1  存储类型

    缓存有很多实现方法,所有这些可以被分为两类,基于内存的缓存和基于磁盘的缓存:

    1、  内存驻留缓存——包含在内存中临时存储数据的所有实现方法,通常在以下情况下使用:

    a)       应用程序频繁使用同样的数据;

    b)       应用程序需要经常获取数据;

    通过将数据保留在内存中,你可以有效降低昂贵的磁盘访问操作,也可以通过将数据保留在使用者进程中来最大程度的减少跨进程的数据传输。

    2、  磁盘驻留缓存——这种技术包含所有使用磁盘作为存储介质的缓存技术,如文件和数据库。在以下情况下基于磁盘的缓存是很有效的:

    a)       处理大数据量时;

    b)       应用服务提供的数据可能并不是总能使用(比如离线的情况);

    c)       缓存的数据必须能在进程回收和机器重启的情况下保持有效;

    通过缓存处理过的数据,你可以有效降低数据处理的负担,同时可减少数据交互的代价。

    1.4.2  架构间元素

    应用程序中的每个逻辑层的组件都会处理数据,下图标识了一些通用组件:


    当使用这些组件进行工作时,你需要考虑哪些数据可以被缓存起来,还有以哪种方式进行缓存会对程序的整体性能和可用性有帮助,以上的这些元素都可以缓存相应的数据。当然,要考虑的远不止这些。

    1.5   实施缓存时的考虑

    当设计一个缓存方案时,不但要考虑缓存哪些数据、数据缓存到哪里,还有其它的因素需要考虑。

    1.5.1  格式和访问模式

    当决定是否缓存一个对象时,关于数据的格式和访问机制,你需要考虑三个主要问题:

    1、  线程安全——当缓存的内容可以被多个线程访问时,使用某种锁定机制来保证数据不会被两个线程同时操作;

    2、  序列化——将一个对象缓存时,需要将它序列化以便保存,所以包缓存的对象必须支持序列化;

    3、  规格化缓存数据——缓存数据时,相对于要使用的数据格式而言,要保证数据的格式是优化过的。

    1.5.2  内容加载

    在使用缓存数据前,必须将数据加载到缓存中,有两种机制来加载数据:

    ·提前加载Proactive Load——使用这种方式时,你提前将所有的状态数据加载到缓存中,可能在应用程序或线程启动时进行,然后在应用程序或线程的生存期内一直缓存;

    ·动态加载Reactive Load——或称反应式加载,当使用这种方法时,在应用程序请求数据时取到数据,并且将它缓存起来以备后续使用。

    1.5.3  过期策略

    另外一个关键因素是如何保持缓存数据和主数据(文件或数据库或其他的应用程序资源)的一致性,你可以定义过期策略来决定缓存中的内容,如已经缓存的时间或者收到其他资源的通知。

    1.5.4  安全性

    当缓存数据时,需要非常清楚缓存中数据的潜在安全威胁。缓存中的数据可能会被别的进程访问或修改,而此进程对主数据是没有权限的。原因是当数据存储在原始位置时,有相应的安全机制来保护它,当数据被带出传统的安全边界时,需要有同等的安全机制。

    1.5.5  管理

    当你缓存数据时,应用系统需要的维护工作加大了。在发布应用程序时,需要配置相应的属性,比如缓存的大小限制和清除策略。同时要使用某种机制来监控缓存的效率(比如事件日志和性能计数器)

    1.6   小结

    第一节内容简单介绍了缓存技术中的概念、缓存数据的原因和方案、优势、实施缓存方案时的考虑等基本内容。现在你对缓存中涉及的内容有了一个大致了解,下面着重介绍可用的缓存技术。

    2         缓存技术

    本节将介绍以下技术:

    使用Asp.Net缓存;

    使用Remoting Singleton缓存;

    使用内存映射文件;

    使用SQL Server缓存;

    使用静态变量缓存;

    使用Asp.net 会话状态(Session State);

    使用Asp.net客户端缓存和状态;

    使用Internet Explorer缓存。

    2.1             Asp.net缓存

    将常用的数据保存在内存中对asp的开发人员来说并不陌生,Session对象和Application对象提供键值对来缓存数据,Session对象保存和单个用户有关的数据,Application对象可保留和应用程序有关的数据,每个用户都可以访问。

    在Asp.net中,提供了专门用于缓存数据的Cache对象,它的应用范围是应用程序域。生存期是和应用程序紧密相关的,每当应用程序启动的时候就重新创建Cache对象。它域Application对象的主要区别就是提供了专门用于缓存管理的特性,比如依赖和过期策略。

    你可以使用Cache对象和它的属性来实现高级的缓存功能,同时可以利用Asp.net Cache来对客户端输出的响应内容进行缓存。关于Asp.net中的缓存技术,有以下内容要介绍:

    2.1.1  编程缓存Programmatic Caching

    Cache对象定义在System.Web.Caching命名空间,可以使用HttpContext类的Cache属性或Page对象的Cache属性来得到Cache的引用,Cache对象除了存储键值对以外,还可以存储.net框架的对象。下面介绍相应的依赖和过期策略。

    2.1.1.1 依赖和过期策略

    当向缓存中加数据时,可以指定它的依赖关系来实现在某些情况下强制移除它。可用的方案包括以下几种:

    ·文件依赖(File Dependency)——当硬盘上的某个(某些)文件更改时,强制移除缓存数据;如:

    CacheDependency cDependency = new

    CacheDependency(Server.MapPath("authors.xml"));

    Cache.Insert("CachedItem", item, cDependency);

    ·键值依赖(Key Dependency)——指定缓存中的某个数据项更改时移除。如:

    // Create a cache entry.

    Cache["key1"] = "Value 1";

    // Make key2 dependent on key1.

    String[] dependencyKey = new String[1];

    dependencyKey[0] = "key1";

    CacheDependency dependency = new CacheDependency(null, dependencyKey);

    Cache.Insert("key2", "Value 2", dependency);

    ·基于时间的过期策略——按照预先定义的时间策略来使数据失效,可以是绝对时间(如某个日期的18:00)也可以是相对现在的相对时间。如:

    /// Absolute expiration

    Cache.Insert("CachedItem", item, null, DateTime.Now.AddSeconds(5),Cache.NoSlidingExpiration);

    /// Sliding expiration

    Cache.Insert("CachedItem", item, null, Cache.NoAbsoluteExpiration,

    TimeSpan.FromSeconds(5));

    使用太短和太长的过期时间都不行,不是造成用不上的缓存数据,就是缓存了陈旧的数据并加重了缓存负担,所以可以使用高并发的测试来决定过期时间的最佳值。

    ·另外有个问题就是如何实现对数据库的依赖,这就要求实现自己的通知机制,当数据库数据改变时能够通知你的缓存数据改变。可参考http://www.gotdotnet.com/team/rhoward的示例。

    由于数据会过期,所以当使用缓存中的数据时,必须检查数据的有效性。如以下代码:

    string data = (string)Cache["MyItem"];

    if (data == null)

    {

    data = GetData();

    Cache.Insert("MyItem", data);

    }

    DoSomeThingWithData(data);

    依赖和过期策略指定了缓存中数据的移除方式,有时候你可能需要在移除发生时做一些工作,这能靠写代码来实现这一点,这就是我们要讲到的。

    2.1.1.2 使用缓存回调(Cache Callback)

    你可以定义回调,这样当移除自动发生时, 你可以不移除它或者使用新的数据来替换它。如:

    CacheItemRemovedCallback onRemove = new CacheItemRemovedCallback(this.RemovedCallback);

    Cache.Insert("CachedItem",

    item,

    null,

    Cache.NoAbsoluteExpiration,

    Cache.NoSlidingExpiration,

    CacheItemPriority.Default,

    onRemove);

    // Implement the function to handle the expiration of the cache.

    public void RemovedCallback(string key, object value, CacheItemRemovedReason r)

    {

    // Test whether the item is expired, and reinsert it into the cache.

    if (r == CacheItemRemovedReason.Expired)

    {

    // Reinsert it into the cache again.

    CacheItemRemovedCallback onRemove = null;

    onRemove = new CacheItemRemovedCallback(this.RemovedCallback);

    Cache.Insert(key,

    value,

    null,

    Cache.NoAbsoluteExpiration,

    Cache.NoSlidingExpiration,

    CacheItemPriority.Default,

    onRemove);

    }

    }

    2.1.1.3 对缓存项使用优先级

    当运行应用程序的服务器内存不足时,会自动清除缓存中的数据,称为“清除scavenging”。此时,Cache对象根据缓存项的优先级来决定先移除哪些缓存数据,你可以在代码中指定缓存项的优先级。参看MSDN中“CacheItemPriority 枚举”,如:

    Cache.Insert("DSN", connectionString, null, d, t, CacheItemPriority.High, onRemove);

    2.1.1.4 刷新数据(清除缓存)

    没有直接的方法来刷新Asp.net的输出缓存,但是有替代方法(设置所有数据失效),比如:

    Response.Cache.SetExpires(DateTime.Now)

    这可以清除缓存,但页面上并不立刻体现出来,直到最初的缓存期结束,比如:

    <%@ OutputCache Duration="10" VaryByParam="none" %>指令指定的缓存只会在10秒后才清除。通常并不需要清除所有缓存项,你只要重新加载数据更新缓存就够了。

    2.1.2  输出缓存(Output Cache)

    你可以使用两种方式的输出缓存来缓存需要传输和显示到客户端浏览器上的数据——页面输出缓存(Page Output Cache)和页面片断缓存(Page Fragment Cache)。当整个页面相对变化较少时,可以缓存整个页面;如果只是页面的一部分经常变化,可以使用片断缓存。

    2.1.2.1 页面输出缓存

    Page Output Caching将对页面请求的响应放入缓存中,后续对此页面的请求将直接从缓存中得到信息而不是重建此页面。可以通过添加Page指令(高级别,声明实现)来实现,也可以使用HTTPCachePolicy类来实现(低级别,程序实现)。本指南不打算介绍技术细节,只给出如何更好使用的指南和最佳实践。有四方面的内容:

    1、 决定缓存的内容

    页面输出缓存可缓存各种信息,缓存这些信息意味着你不需要经常处理同样的数据和结果,包括:

    ·经常被请求但不不改变的静态页面;

    ·更新频率和时间已知的页面(如显示股票价格的页面);

    ·根据HTTP参数,有几个可能输出的页面(如根据城市的代号显示该城市天气情况的页面);

    ·从Web Service返回的结果;如:

    [WebMethod(CacheDuration=60)]

    public string HelloWorld()

    {

    return "Hello World";

    }

    2、 缓存动态页面

    基于输入参数、语言和浏览器类型改变的动态网页经常用到。你可以使用OutputCache的以下属性来实现对动态页面的缓存:

    VaryByParam——基于输入参数不同缓存同一页面的多个版本;

    VaryByHeader——基于Page Header的内容不同缓存页面的多个版本;

    VaryByCustom——通过声明属性和重载GetVaryByCustomString方法来定制缓存处理页面的多个版本;

    VaryByControl——基于控件中asp对象属性的不同来缓存控件。

    对多个版本页面的缓存会降低可用内存,所以要仔细衡量缓存策略。s

    3、 控制缓存的位置

    你可以使用@OutputCache指令的OutputCacheLocation属性的枚举值来指定缓存的位置,如:

    <%@ outputcache duration="10" varybyparam="none" Location="Server" %>

    4、 配置页面输出缓存

    有两种方式控制,你可以使用Page指令,也可以使用Cache API编程实现。参考以下两段代码:

    //代码1,使用指令

    <%@ OutputCache Duration="20" Location="Server" VaryByParam="state" VaryByCustom="minorversion" VaryByHeader="Accept-Language"%>

    //代码2,编程实现

    private void Page_Load(object sender, System.EventArgs e)

    {

    // Enable page output caching.

    Response.Cache.SetCacheability(HttpCacheability.Server);

    // Set the Duration parameter to 20 seconds.

    Response.Cache.SetExpires(System.DateTime.Now.AddSeconds(20));

    // Set the Header parameter.

    Response.Cache.VaryByHeaders["Accept-Language"] = true;

    // Set the cached parameter to 'state'.

    Response.Cache.VaryByParams["state"] = true;

    // Set the custom parameter to 'minorversion'.

    Response.Cache.SetVaryByCustom("minorversion");

    }

    2.1.2.2 页面片断缓存

    有时候缓存整个页面并不灵活,同时内存的发但也比较大,这时候应考虑片断缓存。页面片断缓存适合以下类型的数据:

    ·创建开销很大的页面片断(控件);

    ·包含静态数据的页面片断;

    ·可被多个用户使用的页面片断;

    ·多个页面共享的页面片断(如公用菜单条)

    以下是缓存部分页面的例子:

    // Partial caching for 120 seconds

    [System.Web.UI.PartialCaching(120)]

    public class WebUserControl : System.Web.UI.UserControl

    {

    // Your Web control code

    }

    2.1.3  在非Web项目中使用Asp.net缓存

    Asp.net Cache位于System.Web命名空间,但由于它是一个通用的方案,所以仍然可以在引用此命名空间的任何非Web项目中使用它。

    System.Web.Caching.Cache 类是对象的缓存,它可以通过System.Web.HttpRuntime.Cache 的静态属性或System.Web.UI.Page 和System.Web.HttpContext.Cache来访问。因此在请求上下文之外也可以存在,在每个应用程序域中只有一个实例,所以HttpRuntime.Cache对象可以在Aspnet_wp.exe之外的每个应用程序域中存在。以下代码演示了在普通应用里访问Cache对象:

    HttpRuntime httpRT = new HttpRuntime();

    Cache cache = HttpRuntime.Cache;

     

    2.2             使用Remoting Singleton缓存

    .Net Remoting提供了跨应用程序域、跨进程、跨计算机的程序运行框架。服务器激活的对象有两种激活模式,其中Singleton 类型任何时候都不会同时具有多个实例。如果存在实例,所有客户端请求都由该实例提供服务。如果不存在实例,服务器将创建一个实例,而所有后继的客户端请求都将由该实例来提供服务。由于 Singleton类型具有关联的默认生存期,即使任何时候都不会有一个以上的可用实例,客户端也不会总接收到对可远程处理的类的同一实例的引用。所以将数据缓存起来可以在多个客户端之间共享状态信息。

    为了使用.Net Remoting实现缓存方案,要保证远程对象的租约不过期,并且远程对象没有被垃圾回收器销毁(对象租约是指在系统删除该对象前它在内存中的生存期)。当实现缓存时,重载MarshalByRefObject的InitializeLifetimeService方法并且返回null,这样就能保证租约永远不过期并且相关的对象生存期是无限的。以下代码是一个示例:

    public class DatasetStore : MarshalByRefObject

    {

    // A hash table-based data store

    private Hashtable htStore = new Hashtable();

    //Returns a null lifetime manager so that GC won't collect the object

    public override object InitializeLifetimeService() { return null; }

    // Your custom cache interface

    }

    注意:由于这种方案的成本较高、性能上的限制并且可能造成系统不稳定,通常采用基于Sql Server的方案来替代。

    2.3             使用内存映射文件(Memory-Mapped File)

    内存映射文件提供独一无二的特性,允许应用程序通过指针来访问磁盘上的文件——与访问动态内存趣的方式一样。所以你可以将应用程序进程中的某个地址段的数据映射到文件中,供多个跨应用程序域或跨进程访问。

    在windows中,代码和数据是以以种方式处理的,表现形式都是内存页,而在内存页背后都是磁盘上的文件。唯一的不同磁盘上的文件类型不同。代码后面是可执行的镜像,而数据后面则是系统的页面文件。当多个应用程序共享内存时,系统的性能会有明显提升。

    你可以使用内存映射文件的这种特性来实现同一台机器上的跨进程和跨应用程序域的缓存解决方案。基于内存映射文件的缓存方案包含以下组件:

    ·windows NT服务——启动时创建内存映射文件,停止时删除它。功能是向使用缓存的进程提供句柄。当然,也可以使用命名的内存映射文件来提供操作接口;

    ·缓存托管组件(Cache Management Dll)——实现特定的缓存功能,比如:

    a.     插入和删除数据项到缓存中;

    b.     使用算法清除缓存,比如最后使用算法(Least Recently Used);

    c.     保证数据不被篡改;

    基于内存映射文件的缓存方案可以用在应用程序的每个层中,但由于使用win32 API调用,所以并不容易实现。.Net 框架不支持内存映射文件,所以只能以非托管代码的方式运行,当然也不能利用.Net框架的有力特性,比如垃圾回收等。同时缓存数据项的管理功能需要定制开发,还要开发性能计数器来监控缓存的效果。

    2.4             使用SQL Server缓存

    如果需要在进程回收(重启)、机器重启或电源故障的过程中保持缓存数据的有效,基于内存的方案并不能满足要求。你可以使用基于永久数据存储的方案,如SQL server数据库或NTFS文件系统。

    SQL Server在使用sql语句或存储过程得到数据时,对varchar和varBinary类型的数据有8k的大小限制,你必须使用.Net 框架提供的Ado.Net SQLDataAdapter对象来访问datatable或dataset。

    使用SQL Server缓存数据的优点:

    ·易于实现——使用.Net 框架和Ado.Net访问数据库相当方便;

    ·完善的安全模型和很高的健壮性;

    ·数据非常方便的共享;

    ·数据的持久保留。

    ·支持很大的数据量。

    ·方便的管理工具

    当然,也有缺点:

    ·需要安装SQL Server,对小型应用来说不合适;

    ·重新构造数据的性能和读取数据库的性能比较;

    ·网络负担。

    2.5             使用静态变量缓存

    静态变量常用来记录类的状态,你可以用它来创建定制的缓存对象。在定制的缓存类中将你的数据存储器声明为静态变量,并且提供维护接口(插入、删除和访问等)。如果没有特殊的缓存需求(比如依赖、失效策略等),使用静态变量缓存数据是很方便的。由于是在内存中,这种方案可提供对缓存数据的直接、高速的访问,当没有替代方案解决键值对的存储且对速度要求很高时,可以使用静态变量。当然,在asp.net中,应该使用Cache对象。

    你可以使用这种方案保存大数据的对象,前提是它不经常更改。由于没有清除机制,大数据的内存消耗会影响性能。

    你需要保证定制线程安全机制,或者使用.Net框架提供的同步对象,比如Hashtable。以下代码是使用Hashtable实现的例子:

    static Hashtable mCacheData = new Hashtable();

    应用范围:本方案的应用范围可以限制到类、模块或整个项目。如果变量定义为public,整个项目中的代码都能访问它,范围是整个应用程序域,实现了高效的共享。而它的生存期是和范围紧密相关的。

    2.6             使用asp.net session state

    你可以使用基于HttpSessionState对象的asp.net session state来缓存单个用户的会话状态信息。它解决了asp中会话状态的很多限制,包括:

    ·asp session要求客户端接受cookies,否则就不能使用session;而asp.net可以配置为不使用cookie;

    ·对web server场的情况,asp的session不能支持;当稳定性和可用性要求很高时,asp.net session state虽然效果不好,但对比较小的单个值scalar Value(比如登录信息),还是很有效。

    Asp.net session有很大改进,下面描述使用范围和使用方式。

    Asp.net session state有三种操作模式:

    1、  进程内模式InProc——Session State信息在asp.net工作进程aspnet_wp.exe的进程的内存中存储。这是默认选项,这种情况下,如果进程或应用程序域被回收,则Session 状态信息也被回收;

    2、   进程外模式State Server——状态信息序列化后保存在独立的状态进程中(AspNet_State.exe),所以状态信息可以保存在专门的服务器上(一个状态服务器State Server);

    3、   Sql server模式——状态信息序列化后保存在SQL Server数据库中。

    你可以通过调整配置文件中<sessionState>标签的mode属性来设置要使用的状态模式,比如使用SQL Server模式来在Web server场中共享状态信息。当然,这个优势也有缺点,就是状态信息需要序列化和反序列化,同时多了对数据库的写入和读取,所以性能上有开销,这是要仔细评估的。

    2.6.1  选择使用模式

    2.6.1.1 使用InProc模式

    当使用进程内模式时,状态信息保存在aspnet_wp.exe的进程中。由于在web场的情况下aspnet_wp.exe的多个实例在同一台服务器上运行,所以进程内模式不适用与web场的情况。

    进程内模式是唯一支持Session_End事件的session模式,当用户会话超时或中止时,可以运行Session_End中的事件处理代码来清除资源。

    2.6.1.2 使用StateServer模式

    StateServer模式使用指定的进程储存状态信息。因为它也是一种进程外模式,所以要保证你存储的对象是可序列化的,以支持跨进程传输。

    当使用Session对象在web场的情况下使用时,必须保证web.config文件中的<MachineKey>元素在所有服务器上是唯一的。这样所有的服务器使用同样的加密方式,才能访问缓存中的数据。参考msdn中的“MachineKey元素”。

    2.6.1.3 使用SQL Server模式

    SQL Server模式下,当你使用信任连接(trusted_connection=true 或 integrated security=sspi)访问Session state信息时,不能在asp.net中使用身份用户模拟。

    默认情况下,SQL Server将状态信息存储在TempDb数据库中,它在每次Sql server服务启动时会自动重新创建,当然,你可以指定自己的数据库以便在数据库重启的过程中也能保持数据。

    2.6.2  决定使用Session对象要存储的内容

    你可以使用Session对象缓存任何类型的.net框架数据,但是要了解对某种类型来说最好的方式是什么。有以下几点需要说明:

    1、  对基本类型(比如Int,Byte,String)来说,可以使用任何方式。因为在选用进程外方式时,asp.net使用一个优化的内部方法来序列化和反序列化基本类型的数据;

    2、  对复杂类型(如ArrayList)来说,只选用进程内方式。因为asp.net使用BinaryFormatter来序列化和反序列化这类数据,而这会影响性能的。当然,只有在State Server和SQL Server的方式下,才会进行序列化操作;

    3、  缓存的安全问题,当在缓存中存储敏感数据时,需要考虑安全性,其它页面可以访问到缓存中的数据;

    4、  避免缓存大数据,那会降低性能;

    5、  这种缓存方式不支持过期策略、清除和依赖。

    2.6.3  实现Session State

    Asp.net提供了简单接口来操作Session State,并可使用Web.Config进行简单设置,当配置文件中的设置改变时,能够在页面上立刻体现出来,而不需要重新启动asp.net进程。

    以下代码演示了使用SQL Server来实现Session数据的存储和使用。

    <sessionState

    mode="SQLServer"

    stateConnectionString="tcpip=127.0.0.1:42424"

    sqlConnectionString="data source=127.0.0.1; Integrated Security=SSPI"

    cookieless="false"

    timeout="20"

    />

    private void SaveSession(string CartID)

    {

    Session["ShoppingCartID"] = CartID;

    }

    private void CheckOut()

    {

    string CartID = (string)Session["ShoppingCartID"];

    if(CartID != null)

    {

    // Transfer execution to payment page.

    Server.Transfer("Payment.aspx");

    }

    else

    {

    // Display error message.

    }

    }

    2.7             使用Asp.net客户端缓存和状态

    你还可以使用客户端存储页面信息的方式来降低服务器的负担,这种方法提供最低的安全保障,但却有最快的性能表现。由于需要将数据发送到客户端存储,所以数据量有限。

    实现客户端缓存的机制有以下五种,接下来将依次介绍:

    ·隐藏栏位(Hidden Field)

    ·View State

    ·隐藏帧(Hidden Frame)

    ·Cookies

    ·Query String

    这五种方式分别适合于存储不同类型的数据。

    2.7.1  使用Hidden Field

    你可以将经常改变的少量数据保存在HtmlInputHidden中来维护页面的状态。当每次页面回送的过程中,这些数据都会包含在表单中大送到服务器,所以你要使用HTTP POST方式来提交页面。

    使用这种方式的优点如下:

    1. 不需要服务器资源,直接从页面中读取;
    2. 几乎所有的浏览器都支持;
    3. 实现简单;
    4. 由于数据在页面中,所以在web Farm的情况下也可使用。

    缺点:

    1. 由于可以通过查看源码看到,所以可能会被篡改;
    2. 不支持复杂格式的数据,复杂数据必须使用解析字符串的方式来间接得到;
    3. 当存储大数据的时候会影响性能。

    示例:

    <input id="HiddenValue" type="hidden" value="Initial Value" runat="server" NAME="HiddenValue">

    2.7.2  使用View State

    所有的Web Form页面和控件都包含有一个ViewState属性,在对同一页面多次请求时可以保持页面内的值。它的内部实现是维护相应的hidden field,只不过是加密了的,所以比hidden field的安全性要好。

    使用View State的性能表现很大程度上依赖于服务器控件的类型。一般来说,Label,TextBox,CheckBox,RadioButton,HyperLink的性能要好一些,而DropdownList,ListBox,DataGrid和DataList就要差很多,因为包含的数据量太大,所以每次页面回送都很耗时间。

    有些情况下不推荐使用ViewState,比如:

    1、  不需要回送的页面避免使用;

    2、  避免使用ViewState保存大数据量;

    3、  在需要使用会话超时的情况下避免使用它,因为它没有超时操作。

    ViewState的性能表现和Hidden Field的是类似的,但是具有更高的安全性。

    优点:

    1. 数据在页面中自动维护,不需要服务器资源;
    2. 实现简单;
    3. 数据是经过加密和压缩的,比hidden field有更高的安全性;
    4. 数据存在客户端,可以在Web Farm情况下使用。

    缺点:

    1. 存储大数据量时会降低性能;
    2. 和hidden field类似,在客户端数据仍然有潜在的安全威胁。

    示例代码如下:

    public class ViewStateSample : System.Web.UI.Page

    {

    private void Page_Load(object sender, System.EventArgs e)

    {

    if (!Page.IsPostBack)

    {

    // Save some data in the ViewState property.

    this.ViewState["EnterTime"] = DateTime.Now.ToString();

    this.ViewState["UserName"] = "John Smith";

    this.ViewState["Country"] = "USA";

    }

    }

    private void btnRefresh_Click(object sender, System.EventArgs e)

    {

    // Get the saved data in the view state and display it.

    this.lblTime.Text = this.ViewState["EnterTime"].ToString();

    this.lblUserName.Text = this.ViewState["UserName"].ToString();

    this.lblCountry.Text = this.ViewState["Country"].ToString();

    }

    }

    2.7.3  使用Hidden Frame

    你可以使用Hidden Frame在客户端缓存数据,这就避免了使用hidden field和使用view state时每次页面回送时的缓存数据往返。比如你可以秘密的加载多个页面所需要的图片,这并不会消耗服务器资源。

    优点:

    a.     可以加载较多数据而不只是单个栏位的值;

    b.     避免了不必要的多次回送中的数据往来;

    c.     可以缓存和读取在不同表单中存储的数据项(可以同时缓存多个页面的数据);

    d.     可以访问同一站点不同frame中的客户端脚本数据。

    缺点:

    a.     有些浏览器不支持frame;

    b.     源代码可以在客户端看到,有潜在的安全威胁;

    c.     隐藏frame的数量没有限制,如果框架页面包含较多hidden frame的话,在首次加载时速度会有限制。

    示例代码如下:

    <FRAMESET cols="100%,*">

    <FRAMESET rows="100%,*">

    <FRAME src="contents_of_frame1.html">

    </FRAMESET>

    <FRAME src="contents_of_hidden_frame.html">

    <FRAME src="contents_of_hidden_frame.html" frameborder="0" noresize scrolling="yes">

    <NOFRAMES>

    <P>This frameset document contains:

    <A href="contents_of_frame1.html" TARGET="_top">Some neat contents</A>

    </NOFRAMES>

    </FRAMESET>

    2.7.4  使用Cookies

    Cookie是可以在客户端存储数据另一种方案,这里不过多介绍。

    优点:

    1. 不需要服务器资源;数据保存在客户端,在用户请求时发送到服务器上。
    2. 使用简单。Cookie包含简单的键值对,主要保存轻量级的文本数据。
    3. 支持过期策略;可以指定当会话结束时过期,也可指定一个时间策略。

    缺点:

    1. 数据量的限制;
    2. 用户可能设置为拒绝Cookie;
    3. 安全问题;用户可能更改机器上的cookie信息,造成基于cookie的系统运行失败;
    4. 可能过期或被用户删除,造成一定程度的不可用。

    参看示例代码:

    public class CookiesSample : System.Web.UI.Page

    {

    private void Page_Load(object sender, System.EventArgs e)

    {

    if (this.Request.Cookies["preferences1"] == null)

    {

    HttpCookie cookie = new HttpCookie("preferences1");

    cookie.Values.Add("ForeColor","black");

    cookie.Values.Add("BackColor","beige");

    cookie.Values.Add("FontSize","8pt");

    cookie.Values.Add("FontName","Verdana");

    this.Response.AppendCookie(cookie);

    }

    }

    private string getStyle(string key)

    {

    string val = null;

    HttpCookie cookie= this.Request.Cookies["preferences1"];

    if (cookie != null)

    {

    val = cookie.Values[key];

    }

    return val;

    }

    }

    2.7.5  使用Query String

    Query String是在用户请求的URL后加上相应的参数来使用的,只能在使用HTTP GET方式调用URL时可用。

    优点:

    d.     不需要服务器资源,参数附在URL里面;

    e.     应用面广,几乎所有浏览器都支持;

    f.     实现简单,服务端使用Request对象可直接读取。

    缺点:

    a.     参数直接对用户可见,不安全;

    b.     URL长度的限制,多数浏览器不支持超过255字符的URL。

    示例代码:

    http://www.cache.com/login.asp?user=ronen

    string user = Request.QueryString["User"];

    2.7.6  小结

    下表是使用客户端缓存的建议:

    缓存机制

    适用情况

    Hidden Field

    当安全性要求不高时,在页面中存储少量数据以提交到服务器上的本页面或其它页面。

    ViewState

    在单个页面中存储少量信息满足页面多次回传的要求。提供基本的安全机制。

    Hidden Frame

    在客户端存储数据,避免了数据到服务器的回传。

    Cookie

    当安全性要求不高时,存储少量数据在客户端。

    Query String

    当使用页面地址连接页面时传输少量参数。

     

    2.8             使用Internet Explorer缓存

    IE提供了缓存机制,可以实现对页面的数据进行缓存,同时可以指定过期时间。用户在IE中请求此页面,如果当过期时间没有到,则自动从缓存中提取并呈现;否则,就到服务器上获取新版本。IE对页面的缓存可以在IIS中设置。

    适合在Internet Explorer中缓存的内容

    1. 页面中的图像文件;
    2. 静态的文本内容;
    3. 页面的标题栏和页脚内容——改变频率很低,可以给用户一个迅速相应;
    4. 网站的首页——更改次数页时相对较少的;
    5. 使用动态HTML在客户端保存的特定数据。比如客户自定义的颜色和布局设置信息。

    优点:

    1. 减少对服务器的请求和网络负担;
    2. 支持离线浏览;
    3. 可以实现基于XML的客户端复杂应用。

    缺点:

    1. 客户端的过期时间必须预先指定而不能依赖于服务器更新;IE采用的是Lazy更新机制,优先从缓存中提取数据;
    2. 对其它客户端浏览器没有作用;
    3. 存储的数据没有加密,不能保证客户端数据安全。

    示例代码:

    <META HTTP-EQUIV="expires" CONTENT="Tue, 23 Jun 2002 01:46:05 GMT">

    3         总结

    本文档介绍了缓存和状态数据存储的相关概念,以及可供使用的缓存技术,介绍了各种技术的适用范围,并对其优缺点进行了说明,另外有简单的性能比较和简单的示例代码。更多内容请参看相应的参考资料。

    展开全文
  • 缓存穿透,缓存击穿,缓存雪崩解决方案分析

    万次阅读 多人点赞 2017-01-06 11:12:50
    设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。 缓存穿透 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则...

    前言

    设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。

    缓存穿透

    缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

    解决方案

    有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

    缓存雪崩

    缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

    解决方案

    缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

    缓存击穿

    对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

    缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

    解决方案

    1.使用互斥锁(mutex key)

    业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

    SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在redis2.6.1之前版本未实现setnx的过期时间,所以这里给出两种版本代码参考:

    //2.6.1前单机版本锁
    String get(String key) {  
       String value = redis.get(key);  
       if (value  == null) {  
        if (redis.setnx(key_mutex, "1")) {  
            // 3 min timeout to avoid mutex holder crash  
            redis.expire(key_mutex, 3 * 60)  
            value = db.get(key);  
            redis.set(key, value);  
            redis.delete(key_mutex);  
        } else {  
            //其他线程休息50毫秒后重试  
            Thread.sleep(50);  
            get(key);  
        }  
      }  
    }
    最新版本代码:

    public String get(key) {
          String value = redis.get(key);
          if (value == null) { //代表缓存值过期
              //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
    		  if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
                   value = db.get(key);
                          redis.set(key, value, expire_secs);
                          redis.del(key_mutex);
                  } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                          sleep(50);
                          get(key);  //重试
                  }
              } else {
                  return value;      
              }
     }
    memcache代码:

    if (memcache.get(key) == null) {  
        // 3 min timeout to avoid mutex holder crash  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            value = db.get(key);  
            memcache.set(key, value);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    } 

    2. "提前"使用互斥锁(mutex key):

    在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下:

    v = memcache.get(key);  
    if (v == null) {  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            value = db.get(key);  
            memcache.set(key, value);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    } else {  
        if (v.timeout <= now()) {  
            if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
                // extend the timeout for other threads  
                v.timeout += 3 * 60 * 1000;  
                memcache.set(key, v, KEY_TIMEOUT * 2);  
      
                // load the latest value from db  
                v = db.get(key);  
                v.timeout = KEY_TIMEOUT;  
                memcache.set(key, value, KEY_TIMEOUT * 2);  
                memcache.delete(key_mutex);  
            } else {  
                sleep(50);  
                retry();  
            }  
        }  
    } 

    3. "永远不过期":  

    这里的“永远不过期”包含两层意思:

    (1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

    (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

            从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

    String get(final String key) {  
            V v = redis.get(key);  
            String value = v.getValue();  
            long timeout = v.getTimeout();  
            if (v.timeout <= System.currentTimeMillis()) {  
                // 异步更新后台异常执行  
                threadPool.execute(new Runnable() {  
                    public void run() {  
                        String keyMutex = "mutex:" + key;  
                        if (redis.setnx(keyMutex, "1")) {  
                            // 3 min timeout to avoid mutex holder crash  
                            redis.expire(keyMutex, 3 * 60);  
                            String dbValue = db.get(key);  
                            redis.set(key, dbValue);  
                            redis.delete(keyMutex);  
                        }  
                    }  
                });  
            }  
            return value;  
    }

    4. 资源保护:

    采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。

    四种解决方案:没有最佳只有最合适

    解决方案 优点 缺点
    简单分布式互斥锁(mutex key)

     1. 思路简单

    2. 保证一致性

    1. 代码复杂度增大

    2. 存在死锁的风险

    3. 存在线程池阻塞的风险

    “提前”使用互斥锁  1. 保证一致性 同上 
    不过期(本文)

    1. 异步构建缓存,不会阻塞线程池

    1. 不保证一致性。

    2. 代码复杂度增大(每个value都要维护一个timekey)。

    3. 占用一定的内存空间(每个value都要维护一个timekey)。

    资源隔离组件hystrix(本文)

    1. hystrix技术成熟,有效保证后端。

    2. hystrix监控强大。

     

     

    1. 部分访问存在降级策略。



    四种方案来源网络,详文请链接:http://carlosfu.iteye.com/blog/2269687?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

    总结

    针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。

    最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。

    展开全文
  • 什么是缓存

    千次阅读 2019-05-26 15:08:23
    硬件的缓存? cpu缓存:位于cpu和内存之间的临时存储器 软件缓存? 软件缓存分为三级 内存缓存(预先将数据写到容器(list,map,set)等数据存储单元中,就是软件内存缓存) 数据库缓存 网络缓存 内存缓存淘汰机制...
    1. 硬件的缓存?
      cpu缓存:位于cpu和内存之间的临时存储器
    2. 软件缓存?
      软件缓存分为三级
      1. 内存缓存(预先将数据写到容器(list,map,set)等数据存储单元中,就是软件内存缓存)
      2. 数据库缓存
      3. 网络缓存
    3. 内存缓存淘汰机制分为三种
      1. FIFO(First In,First Out)先进先出
        优点:是先进先出的数据缓存器,他与普通存储器的区别是没有外部读写地址线,这样使用起来非常简单。
        缺点:只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址

      2. LFU(Least Freauently Used)
        最不经常使用页置换算法,清理掉留给经常使用的使用

      3. LRU(Least Recently Used)喜新厌旧
        内存管理的一种页面置换算法,新加入的数据放到链表的头部,当缓存命中(被访问)数据移到链表的头部,当链表满的时候,将链表尾部的数据丢弃。

            //单链表
          public class LinkedList<T> {
          
              public LinkedList() {
                  size = 0;
              }
          
              Node list;
              //链表有多少个节点
              int size;
          
              //添加节点
              //在头部添加节点
              public void add(T data) {
                  Node head = list;
                  Node curNode = new Node(data, list);
                  list = curNode;
                  size++;
          
              }
          
              //在链表的index位置插入一个新数据
              public void add(int index, T data) {
                  checkPositionIndex(index);
                  Node cur = list;
                  Node hrad = list;
                  for (int i = 0; i < index; i++) {
                      hrad = cur;
                      cur = cur.next;
                  }
                  Node node = new Node(data, cur);
                  hrad.next = node;
                  size++;
          
              }
          
              // 检查index 是否在链表范围结构
              public void checkPositionIndex(int index) {
                  if (!(index >= 0 && index <= size)) {
                      throw new IndexOutOfBoundsException("index:" + index + ",size" + size);
                  }
              }
          
              //删除节点
              public T remove() {
                  if (list != null) {
                      Node node = list;
                      list = list.next;
                      //gc
                      node.next = null;
                      size--;
                      return node.data;
                  }
          
                  return null;
              }
          
              public T remove(int index) {
                  checkPositionIndex(index);
                  Node cur = list;
                  Node hrad = list;
                  for (int i = 0; i < index; i++) {
                      hrad = cur;
                      cur = cur.next;
                  }
                  hrad.next = cur.next;
                  //gc
                  cur.next = null;
                  size--;
                  return null;
          
              }
          
              public T removeLast() {
                  if (list != null) {
                      Node node = list;
                      Node cur = list;
                      while (cur.next != null) {
                          node = cur;
                          cur = cur.next;
                      }
                      //gc
                      node.next = null;
                      size--;
                      return cur.data;
                  }
                  return null;
              }
          
              //修改节点
          
              public void updata(int index, T newData) {
                  checkPositionIndex(index);
                  Node head = list;
                  for (int i = 0; i < index; i++) {
                      head = head.next;
                  }
                  head.data = newData;
              }
          
              //查询节点
              public T get() {
                  Node node = list;
                  if (node != null) {
                      return node.data;
          
                  } else {
                      return null;
                  }
              }
          
              public T get(int index) {
                  checkPositionIndex(index);
                  Node node = list;
                  for (int i = 0; i < index; i++) {
                      node = node.next;
                  }
                  return node.data;
              }
          
          
              class Node {
                  T data;
                  Node next;
          
                  public Node(T data,
                              Node next) {
                      this.data = data;
                      this.next = next;
                  }
              }
          
              @Override
              public String toString() {
                  Node node = list;
                  for (int i = 0; i < size; i++) {
                      System.out.println("" + node.data);
          
                      node = node.next;
                  }
                  return super.toString();
              }
          }
        
        public class LruLinhedList<T> extends LinkedList<T> {
        
            // 用于限定内存空间的大小
            int memorySize;
            static final int DEFAULT_CAP = 6;
        
            public LruLinhedList() {
                this(DEFAULT_CAP);
            }
        
            public LruLinhedList(int memorySize) {
                this.memorySize = memorySize;
            }
        
            //    LRU 添加节点
            public void lruAdd(T data) {
                if (size >= memorySize) {
                    removeLast();
                    add(data);
                } else {
                    add(data);
                }
            }
        
            // 删除节点
            public T lruRmove() {
                return removeLast();
            }
        
            public T lruGet(int index) {
                checkPositionIndex(index);
                Node node = list;
                Node pro = list;
                for (int i = 0; i < index; i++) {
                    pro = node;
                    node = node.next;
                }
                T reData = node.data;
                //访问节点移开头
                pro.next = node.next;
                Node head = list;
                node.next = head;
                list = node;
                return reData;
        
            }
        
            public static void main(String[] args) {
                LruLinhedList<String> LinkedList = new LruLinhedList<>(6);
                for (int i = 0; i < 6; i++) {
                    LinkedList.lruAdd("" + i);
                }
                LinkedList.toString();
                System.out.println("" + LinkedList.lruGet(4));
        
                LinkedList.toString();
                LinkedList.lruAdd(90+"");
                LinkedList.toString();
        //        LinkedList.lruGet(23);
        //        LinkedList.toString();
            }
        
        }
        
        
    展开全文
  • 各种缓存介绍

    万次阅读 2017-07-27 10:30:58
    说起缓存相关技术,老多了, memcache、redis、squid、varnish、web cache、 CDN等等。缓存技术五花八门,但这些技术间有什么共性的地方,又有什么不同的地方呢?答案肯定是有的,这次为大家分享及整理一下缓存方面...

    说起缓存相关技术,老多了, memcache、redis、squid、varnish、web cache、 CDN等等。缓存技术五花八门,但这些技术间有什么共性的地方,又有什么不同的地方呢?答案肯定是有的,这次为大家分享及整理一下缓存方面的技术,主要分为三个系列展开:

    缓存随谈系列之一:数据库缓存

    缓存随谈系列之二:静态缓存

    缓存随谈系列之三:动态缓存


    一、什么是数据库缓存 


    我们知道常见的数据库,比如oracle、mysql等,数据都是存放在磁盘中。虽然在数据库层也做了对应的缓存,但这种数据库层次的缓存一般针对的是查询内容,而且粒度也太小,一般只有表中数据没有变更的时候,数据库对应的cache才发挥了作用。但这并不能减少业务系统对数据库产生的增、删、查、改的庞大IO压力。所以数据库缓存技术在此诞生,实现热点数据的高速缓存,提高应用的响应速度,极大缓解后端数据库的压力。


    以下为memcache数据库缓存为例,以图说明一下什么是数据库缓存:

    022f6d6489e6facab3ee148f0150bdc7aa5bfe32

    二、数据库缓存的技术特点


    性能优越


    数据库缓存的第一个技术特点就是提高性能,所以数据库缓存的数据基本上都是存储在内存中,相比io读写的速度,数据访问快速返回。而且在mysql 5.6的版本开始,已经把memcache这种跟数据库缓存直接挂钩的中间件直接集成进去了,已经等不及我们自己去单独部署对应数据库缓存的中间件了。


    应用场景


    针对数据库的增、删、查、改,数据库缓存技术应用场景绝大部分针对的是“查”的场景。比如,一篇经常访问的帖子/文章/新闻、热门商品的描述信息、好友评论/留言等。因为在常见的应用中,数据库层次的压力有80%的是查询,20%的才是数据的变更操作。所以绝大部分的应用场景的还是“查”缓存。当然,“增、删、改”的场景也是有的。比如,一篇文章访问的次数,不可能每访问一次,我们就去数据库里面加一次吧?这种时候,我们一般“增”场景的缓存就必不可少。否则,一篇文章被访问了十万次,代码层次不会还去做十万次的数据库操作吧。


    数据一致性


    在很多应用场景中,当一个数据发生变更的时候,很多人在考虑怎么样确保缓存数据和数据库中数据保存一致性,确保从缓存读取的数据是最新的。甚至,有人在对应数据变更的时候,先更新数据库,然后再去更新缓存。我觉得这个考虑不太现实,一方面这会导致代码层次逻辑变得复杂,另外一方面也真想不明白还要缓存干什么了。在绝大多数的应用中,缓存中的数据和数据库中的数据是不一致的。即,我们牺牲了实时性换回了访问速度。比如,一篇经常访问的帖子,可能这篇帖子已经在数据库层次进行了变更。而我们每次访问的时候,读取的都是缓存中的数据(帖子)。既然是缓存,那么必然是对实时性可以有一定的容忍度的数据,容忍度的时间可以是5分钟,也可以是5小时,取决于业务场景的要求。相反,一定要求是实时性的数据库,就不应该从缓存里读取,比如库存,再比如价格。

    高可用

    自从有了缓存,代码每天快乐的去缓存中愉快的玩耍。为什么说高可用呢,我们知道缓存为数据库抵挡了很多压力,同时也为应用提供了良好的访问速度。但同时有没有想过缓存的感受,如果当数据库缓存“罢工”了,这会出现什么后果?特别在一些高并发的应用中,数据库层肯定是“消化不良“,最终导致应用全面崩溃。所以缓存的高可用显得非常重要。

    三、数据库缓存常见开源技术

    要说用于数据库缓存场景的开源技术,那必然是memcache和redis这两个中间件。

    74ba1c9a1c6cd0a7d59872b5b8e77f366ff6ce50

    因为都是专注于内存缓存领域,memcache和redis向来都有争议。比如性能,到底是memcache性能好,还是redis性能更好等。同样都是内存缓存技术,它们都有自己的技术特性。没有更好的技术,只有更合适的技术。个人总结一下,有持久化需求或者对数据结构和处理有高级要求的应用,选择redis。其他简单的key/value存储,选择memcache。所以根据自身业务特性,数据库缓存来选择适合自己的技术。

     

    暂不说用不用数据库缓存,见过有人把session存储在数据库中的,也见过把视频/文件转化成二进制存储在数据库的,这种行为无疑是逆天的。合理应用数据库缓存技术,且行且珍惜,切勿走向误区。


    静态缓存

    上次写了一篇数据库缓存,由于快餐式的风格,遭到了广大读友的吐槽。上篇风格有点 “ 虚 ”,我本身是一个技术控,偏向经验/干货的分享,本文主要描述静态缓存方面的一些心得及分享。作为系列二,有所不足之处,依旧希望大家踊跃“ 亮砖 ”。

    说起静态缓存技术,CDN是经典代表之作。静态缓存技术面非常广,涉及的开源技术包含apache、Lighttpd、nginx、varnish、squid等。

    静态缓存,一般指 web 类应用中,将图片、js、css、视频、html等静态文件/资源通过磁盘/内存等缓存方式,提高资源响应方式,减少服务器压力/资源开销的一门缓存技术。

    本文主要通过:浏览器缓存、磁盘缓存、内存缓存、nginx的内存缓存、CDN五个方面围绕静态缓存而展开。

    一、浏览器缓存

    浏览器缓存,也称为客户端缓存,是静态缓存中最常见最直接的表现形式,很多时候都往往被人忽略掉。

    案例1:

    我们经常在nginx的配置文件中看到以下缓存配置:

    6d000037ed9f276fc6c

    案例2:

    在经常写 jsp 的时候,html 标签中关于 http 头信息也可以注意到“ expires ”的字样:

    6d100037e15c8ded90d

    对于案例1和案例2中(nginx设置的expires优先级大于代码中设置的expires优先级),expires是给一个资源设定一个过期时间,也就是说无需去服务端验证,直接通过浏览器自身确认是否过期即可,所以不会产生额外的流量。此种方法非常适合不经常变动的资源。如果文件变动较频繁,就不要使用 expires 来缓存。

    比如对于常见类web网站来说,css 样式和 js 脚本基本已经定型,所以最适合的方法是 expires 来缓存一些内容到访问者浏览器。

    案例3:

    通过 chrome 访问服务器端的一张图片,用F12键打开开发者前端调试工具:

    6d000037eda08b47417

    第一次访问,响应200状态,当第二次及后续访问的时候,变成304状态,客户端已经开始获取浏览器缓存内容,而不需要去服务器端获取对应的请求内容,即 nginx 中 expires 参数设置已经生效。等待客户端缓存时间过期后,会再次请求服务器端内容来更新本地缓存。

    6d000037edbc442d9b9

    介绍到这里,突然想起一个有意思的需求。比如,访问一张静态文件,不想客户端缓存,需要每次都去服务器端取数据。我们可以用“ last-modified ”参数来实现,即“ last-modified ”是根据文件更新时间来确定是否再次发送加载。

    Nginx核心配置如下:

    6d000037ed8acbc1f97

    我们更改掉服务器传回客户端的“ last-modified ”文件修改时间参数的值,这样导致客户端本地保存的文件时间每次跟服务器端传回来的时间不一致,所以每次客户端“ 误认为 ”服务器端有静态文件更新,每次都会去服务器端取“ 所谓的最新数据 ”。这样我们可以看到,不管在浏览器访问多少次,返回的 http 状态都是200,再也找不到304状态了。

    误区:在 nginx 中设置 expires,并不是指把静态内容缓存在 nginx 中,而是设置客户端浏览器缓存的时间,这是很多人的误区所在。

    二、磁盘缓存

    除了存储在客户端的静态缓存(浏览器静态)技术外,在服务器端的静态缓存技术主要分为磁盘缓存和内存缓存两大类。单纯围绕 nginx 的 squid、varnish 等一类中间件,处理静态数据的性能十分优秀。核心是 nginx 基于 epoll 网络模型,而相比 apache 基于 select 网络模型。所以 apache 的优势在于密计算型,稳定性好。而 nginx 偏向静态处理,反向代理,高并发。比如 apache+php 的稳定性比 nginx+php 要好,而性能是明显 nginx 要优秀许多。

    以上仅单纯是对磁盘中静态数据处理的能力,所谓磁盘缓存,指另外的一种缓存静态文件的技术。以 nginx 配置为例:

    6d100037e19d4127df6

    可以看出 nginx 主要通过 proxy_cache 来实现 web cache,熟悉 nginx 的同学,不难看出,以上配置在 location 这里,不仅可以实现静态文件的缓存,还可以实现动态文件的缓存(这里放在下章节详细介绍)。我们编写个 test.html测试文件,然后并访问。test.html 源码如下:

    6d000037edc9ba52346

    我们发现服务器的 cache 目录里面,多了两个缓存文件:

    6d100037e1746809432

    有意思的,这两个文件里面的内容分别为(通过 less 命令查看):

    6d100037e1ae69f0753

    (b0ad5d3e7f099bfff9e4fc6a159d868c)

    6d100037e1e193e6dbb

    (53edc39ed253e14415a29412cfc01faf)

    所以不难看出,nginx 把 html 内容和图片二进制全部缓存到本地磁盘上了。下次用户再次来访问 test.html 的时候,nginx 直接将缓存在本地磁盘的文件返回给用户。特别是后端如若是部署的 tomcat、iis 等,nginx 强大的静态缓存能力,有效减少了服务器压力。

    三、内存缓存

    紧接上面描述的磁盘缓存,内存缓存顾名思义,就是把静态文件缓存在服务器端的内存中。所以这种缓存,如若命中缓存的话,取内存中的缓存数据返回比取磁盘中的缓存数据返回,性能要高很多。以 varnish 为例,varnish 核心配置如下:

    启动命令:

    6d100037e18e22f534b

    参数简介:

    6d000037ee413494231

    default.vcl核心配置如下:

    6d000037ee6dd062ae8

    Varnish对.gif、.jpg、.jpeg、.png等结尾的 URL 缓存时间设置1小时。varnish设置完毕后,我们用命令行方式,通过查看网页头来查看命中情况:

    6d000037ee881153360

    6d100037e203342923b

    最后,我们可以通过 varnishadm 命令来清理缓存,也可以通过 varnishstat 命令来查看 varnish 系统缓存状态。

    四、Nginx 的内存缓存

    以上主要以 Varnish 为例,介绍了内存缓存静态资源的方法。其实 nginx 也有内存缓存,相比 squid、varnish 而言,nginx 的内存缓存需要通过编码实现。如下配置:

    6d000037ee7f70a3812

    memcached_pass 指定服务器地址,使用变量 $memcache_key 为 key 查询值,去 memcache 查询对应 value 值。

    如我们访问:http://***.***.***.***/image/test.jpg ,则 nginx 去 memcache 中查询key 为“ test.jpg ”的 value 值并返回。如果没有相应的值,则返回 error_page 404。介绍到这里,关键在于存储在 memcache 中的静态文件,需要通过代码写入 memcache 中。怎么样通过 php/java 等代码把静态资源的数据写入 memcache 中,关于这块的示例就不再过多介绍了。

    Nginx的内存缓存因为需要通过编码实现,所以灵活性特别高。这块可以结合自身业务系统的特点,让静态缓存的灵活性和效率都能得到保障。可能唯一的缺陷就是,通过编码实现的方式,给我们维护管理带来了负担。在之前我曾参与的一个电商系统,就是把客户的订单照片通过 php 代码写入 memcache,客户访问取图的时候,从 memcache 中获取,速度效率特别高。Nginx 作为一款在七层无所不能且轻量级高性能的中间件,能够直接去 memcache 中取数据,来实现静态缓存的效果,这块相应的功能是其他软件无法相媲美的。

    五、CDN

    说起 CDN,大家都不陌生,它是静态缓存加速最典型的代表。CDN技术并不是一门新的技术,它是基于传统 nginx、squid、varnish 等 web 缓存技术,结合 DNS 智能解析的静态缓存加速技术。值得注意的是,他对动态链接访问并没有加速效果。架构原理图如下:

    6d100037e2349afb0db

    所以CDN的静态缓存技术核心主要在于两点:

    节点缓存:对需要加速的网站应用,相应的静态资源通过内存缓存+磁盘缓存的方式缓存在服务器端。

    精准调度:对访问的用户 ip 进行智能解析调度,实现就近缓存节点访问。比如以上图例中,北京用户访问 www.a.com。通过 dns 解析的时候,分析用户 ip,发现是北京用户。则 dns 返回对应北京缓存节点的 ip 地址给到用户,则用户 www.a.com 默认访问北京服务器上面的缓存数据,实现就近访问的策略,大大提升了访问速度。



    展开全文
  • 高并发场景下缓存处理的一些思路

    千次阅读 2019-06-18 09:46:01
    来源:Java知音(微信号:Java_friends) 在实际的开发当中,我们经常需要进行磁盘数据的读取和搜索,因此经常会有出现从数据库读取数据的场景出现。但是当数据访问量次数增大的时候,过多的磁盘读取可能会最终成为...
  • 初步了解HTTP缓存

    2020-11-18 18:37:28
    2、当浏览器向服务器请求资源的时候,都会率先抵达浏览器缓存,如果浏览器有这个资源请求的副本,就可以直接从浏览器获取资源,而不用去请求浏览器 3、常用的http请求只能缓存get请求响应的资源,对于其他类型的响应...
  • 缓存

    2019-06-16 07:56:00
    动态网站的问题在于它是动态的,也就是说每次用户访问呢,...Django提供了不同粒度数据的缓存,你可以缓存整个页面,也可以缓存某个部分,甚至整个网站。 设定缓存 缓存系统需要一些少量的设定工作,也就是说,...
  • 彻底吃透浏览器的缓存机制!

    千次阅读 多人点赞 2019-04-15 15:49:05
    一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。 对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器...
  • 缓存Cache详解

    千次阅读 2018-02-11 13:05:21
    一、什么是缓存Cache? 为什么人们要使用它?一个使用缓存Cache的站点会监听客户端向服务器端发出的请求,并保存服务器端的回应——比如HTML页面、图片等文件。接着,如果有另外一个使用相同URL发送请求,他能够使用...
  • 缓存技术的详解

    万次阅读 2017-10-24 16:01:06
    一、缓存概述 缓存是分布式系统中的重要组件,主要解决高并发,大数据场景下,热点数据访问的性能问题。提供高性能的数据快速访问。 1、缓存的原理 将数据写入/读取速度更快的存储(设备); ...
  • 我没想到他会这么问
  • 缓存穿透、缓存击穿、缓存雪崩区别和解决方案

    万次阅读 多人点赞 2018-09-19 14:35:57
    一、缓存处理流程  前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。     二、缓存穿透  描述:  缓存...
  • Redis缓存穿透、缓存雪崩问题分析

    万次阅读 多人点赞 2018-06-01 22:16:35
    把redis作为缓存使用已经是司空见惯,但是使用redis后也可能会碰到一系列的问题,尤其是数据量很大的时候,经典的几个问题如下: (一)缓存和数据库间数据一致性问题 分布式环境下(单机就不用说了)非常容易出现...
  • 》,今天给大家整理一篇关于Redis经常被问到的问题:缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等概念的入门及简单解决方案。 一、缓存雪崩 缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未...
  • 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。 解决办法: 1.布隆过滤 对所有...
  • 缓存读取术之防止缓存雪崩

    千次阅读 2018-02-14 23:59:46
    重点关注如何防止缓存雪崩。 1. 缓存读操作 引入缓存后,读数据的流程如下: (1)先读缓存,如果缓存中有数据(hit),则返回缓存中的结果; (2)如果缓存中没有数据(miss),则回源到database获取,然后...
  • 一、什么是缓存雪崩 缓存雪崩就是指缓存由于某些原因(比如 宕机、cache服务挂了或者不响应)整体crash掉了,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难。 下面的就是一个雪崩的简单...
  • Redis缓存的使用可以极大的...另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。本篇文章,并不是要更加完美的解决这三个问题,也不是要颠覆业界流行的解决方案。而是...
  • 快速了解缓存穿透与缓存雪崩

    千次阅读 多人点赞 2019-07-26 08:00:00
    缓存穿透 缓存系统,一般流程都是按照key去查询缓存,如果不存在对应的value,就去后端系统(例如:持久层数据库)查找。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的...
  • Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,...

空空如也

1 2 3 4 5 ... 20
收藏数 1,963,682
精华内容 785,472
关键字:

缓存