• 论缓存之道


    /** 
    * 作者:ahuaxuan 
    * 日期:2009-03-08 
    */ 

    看一粒沙中的世界, 一朵野花中的天堂。 把无限握于掌中, 把永恒握于瞬间。——威廉• 布莱克 

    开始讨论缓存之前,让我们先来讨论讨论另外一个问题:理论和实践.从ahuaxuan接触的程序员来看,有的程序员偏实践,有的程序员偏理论,但是这都是不好的行为,理论和实践同样重要,我们在做很多核心的算法的时候,没有理论根本无从下手,而在我们多年的实践中,不总结理论就不能加深自己的理解.所以理论和实践同等重要. 

    缓存是当今各种软件或者硬件系统中不可缺少的技术之一,所以对每个程序员来说都显得异常重要,对ahuaxuan来说亦是如此.如果说用dfa实现文字过滤是从理论到实践,那么本文便是从实践中总结出得理论. 

    在讨论缓存功能之前,我们首先来了解一下缓存这个东西本身.ahuaxuan根据自己的经验把缓存问题细分为4类小问题. 

    1缓存为什么要存在? 
    2缓存可以存在于什么地方? 
    3缓存有哪些属性? 
    4缓存介质? 

    搞清楚这4个问题,那么我们就可以随意的通过应用的场景来判断使用何种缓存了. 

    下面ahuaxuan和大家一一分析这4个问题. 
    1. 缓存为什么要存在? 
    一般情况下,一个网站,或者一个应用,它的一般形式是,浏览器请求应用服务器,应用服务器做一堆计算后再请求数据库,数据库收到请求后再作一堆计算后把数据返回给应用服务器,应用服务器再作一堆计算后把数据返回给浏览器.这个是一个标准流程.但是随着互连网的普及,上网的人越来越多,网上的信息量也越来越多,在这两个越来越多的情况下,我们的应用需要支撑的并发量就越来越多.然后我们的应用服务器和数据库服务器所做的计算也越来越多,但是往往我们的应用服务器资源是有限的,数据库每秒中接受请求的次数也是有限的(谁叫俺们的硬盘转速有限呢).如果利用有限的资源来提供尽可能大的吞吐量呢,一个办法:减少计算量,缩短请求流程(减少网络io或者硬盘io),这时候缓存就可以大展手脚了.缓存的基本原理就是打破上图中所描绘的标准流程,在这个标准流程中,任何一个环节都可以被切断.请求可以从缓存里取到数据直接返回.这样不但节省了时间,提高了响应速度,而且也节省了硬件资源.可以让我们有限的硬件资源来服务更多的用户. 

    2  缓存可以存在于什么地方?
     
    Java代码  收藏代码
    1. 浏览器---浏览器和app之间---分过层的app-数据库  

     
    在上图中,我们可以看到一次请求的一般流程,下面我们重新绘制这张图,让我们的结构稍微复杂一点点. 
    (将app分层) 
    浏览器---浏览器和app之间---分过层的app-数据库 


    理论上来将,请求的任何一个环节都是缓存可以作用的地方.第一个环节,浏览器,如果数据存在浏览器上,那么对用户来说速度是最快的,因为这个时候根本无需网络请求.第二个环节,浏览器和app之间,如果缓存加在这个地方,那么缓存对app来说是透明的.而且这个缓存中存放的是完整的页面.第三个节点,app中本身就有几个层次,那么缓存也可以放在不同的层次上,这一部分是情况或者场景比较复杂的部分.选择缓存时需要谨慎.第四个环节,数据库中也可以有缓存,比如说mysql的querycache. 

    那么也就是说在整个请求流程的任何一点,我们都可以加缓存.但是是所有的数据都可以放进缓存的吗.当然不是,需要放进缓存的数据总是有一些特征的,要清楚的判断数据是否可以被缓存,可以被怎样缓存就必须要从数据的变化特征下手. 

    数据有哪些变化特征?最简单的就是两种,变和不变.我们都知道,不会变化的数据不需要每次都进行计算.问题是难道所有的数据理论上来讲都会变化,变化是世界永恒的主题.也就是说我们把数据分为变和不变两种是不对的,那么就让我们再加一个条件:时间.那么我们就可以把数据特征总结为一段时间内变或者不变.那么根据这个数据特征,我们就可以在合适的位置和合适的缓存类型中缓存该数据. 

    3缓存有哪些属性 
    从面向对象的角度来看,缓存就是一个对象,那么是对象,必然有属性.那么下面我们来探讨一下缓存有哪些属性.以下列举我们常用到的3个属性. 
    (1) 命中率 
    命中率是指请求缓存次数和缓存返回正确结果次数的比例.比例越高,就证明缓存的使用率越高. 

    命中率问题是缓存中的一个非常重要的问题,我们都希望自己缓存的命中率能达到100%,但是往往事与愿违,而且缓存命中率是衡量缓存有效性的重要指标. 

    (2) 最大元素 
    缓存中可以存放得最大元素得数量,一旦缓存中元素数量超过这个值,那么将会起用缓存清空策略,根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率.从而更有效的时候缓存. 

    (3) 清空策略 

    1 FIFO ,first in first out ,最先进入缓存得数据在缓存空间不够情况下(超出最大元素限制时)会被首先清理出去 
    2 LFU , Less Frequently Used ,一直以来最少被使用的元素会被被清理掉。这就要求缓存的元素有一个hit 属性,在缓存空间不够得情况下,hit 值最小的将会被清出缓存。 
    2 LRU ,Least Recently Used ,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。 

    4缓存介质 
    从硬件介质上来将无非就是两种,内存和硬盘(对应应用层的程序来讲不用考虑寄存器等问题).但是往往我们不会从硬件上来划分,一般的划分方法是从技术上划分,可以分成几种,内存,硬盘文件.数据库. 
    (1) 内存.将缓存放在内存中是最快的选择,任何程序直接操作内存都比操作硬盘要快的多,但是如果你的数据要考虑到break down的问题,因为放在内存中的数据我们称之为没有持久话的数据,如果硬盘上没有备份,机器down机之后,很难或者无法恢复. 

    (2) 硬盘.一般来说,很多缓存框架会结合使用内存和硬盘,比如给内存分配的空间有满了之后,会让用户选择把需要退出内存空间的数据持久化到硬盘.当然也选择直接把数据放一份到硬盘(内存中一份,硬盘中一份,down机也不怕).也有其他的缓存是直接把数据放到硬盘上. 


    (3) 数据库.说到数据库,可能有的人会想,之前不是讲到要减少数据库查询的次数,减少数据库计算的压力吗,现在怎么又用数据库作为缓存的介质了呢.这是因为数据库又很多种类型,比如berkleydb,这种db不支持sql语句,没有sql引擎,只是key和value的存储结构,所以速度非常的快,在当代一般的pc上,每秒中十几w次查询都是没有问题的(当然这个是根据业务特征来决定的,如果您访问的数据在分布上是均匀的,那ahuaxuan可不能保证这个速度了). 

    除了缓存介质之外,ahuaxuan根据缓存和应用的耦合程度将其划分为local cache和remote cache. 
    Local cache是指包含在应用之中的缓存组件.而remote cache指和应用解耦在应用之外的缓存组件.典型的local cache有ehcache,oscache,而remote cache有大名鼎鼎的memcached. 

    Localcache最大的优点是应用和cache的时候是在同一个进程内部,请求缓存非常快速,完全不需要网络开销等.所以单应用,不需要集群或者集群情况下cache node不需要相互通知的情况下使用local cache比较合适.这也是java中ehcache和oscache这么流行的原因. 
    但是Local cache是有一定的缺点的,一般这种缓存框架(比如java中的ehcache或者oscache)都是local cache.也就是跟着应用程序走的,多个应用程序无法直接共享缓存,应用集群的情况下这个问题更加明显,当然也有的缓存组件提供了集群节点相互通知缓存更新的功能,但是由于这个是广播,或者是环路更新,在缓存更新频繁的情况下会导致网络io开销非常大,严重的时候会影响应用的正常运行.而且如果缓存中数据量较大得情况下使用localcache意味着每个应用都有一份这么大得缓存,着绝对是对内存的浪费. 

    所以这个情况下,往往我们会选择remote cache,比如memcached.这样集群或者分布式的情况下各个应用都可以共享memcached中的数据,这些应用都通过socket和基于tcp/ip协议上层的memcached协议直接连接到memcached,有一个app更新了memcached中的值,所有的应用都能拿到最新的值.虽然这个时候多了很多了网络上的开销,但是往往这种方案要比localcache广播或环路更新cache节点要普遍的多,而且性能也比后者高.由于数据只需要保存一份,所以也提高了内存的使用率. 

    通过以上分析可以看出,不管是local cache,还是remote cache在缓存领域都有自己的一席之地,所以ahuaxuan建议在选择或者使用缓存时一定要根据缓存的特征和我们的业务场景准确判断使用何种缓存.这样才能充分发挥缓存的功能. 

    Ahuaxuan认为,缓存的使用是架构师的必备技能,好的架构师能够根据数据的类型,业务的场景来准确的判断出使用何种类型的缓存,并且如何使用这种类型的缓存.在缓存的世界里也没有银弹,目前还没有一种缓存可以解决任何的业务场景或者数据类型,如果这种技术出现了,那架构师就又更不值钱了.呵呵. 

    本文是ahuaxuan从自己的实践中总结出来的一些小小的心得,未参考任何文章,,所以可能未必好,未必全面,未必令您满意,欢迎拍砖. 

    最后说一说写这篇文章的初衷,周末有人让我说说我对缓存的理解,我的回答的是对缓存的理解无法用一句话来表述,起码写5篇文章.那本文只是第一篇. 待续------------- 

    近水楼台先得月,向阳花木易为春--------苏麟 

    缓存的作用在第一论http://www.iteye.com/topic/345693中已有部分阐述,下面ahuaxuan和大家一起来学习一下缓存得另外一个重要的规则,近和快. 

    在我们打开浏览器,决定浏览某个网页之前(指人眼看到屏幕上的内容之前),一般来说浏览器有几个事情要做,首先根据url请求服务器端的html数据------,然后解析html,------下载css,和js,--------将html显示到屏幕上等等. ---------然后眼睛才能感受到,--------接着大脑才能感受到. 

    在这个流程中,那么怎么才能让大脑尽可能快的接受到这个信息呢,我想最快的方式是在大脑里放一份该屏幕的拷贝,下次想看这份内容的时候直接拿出大脑的拷贝就可以了.如果大脑容量有限,那我们可以考虑把这份拷贝放到眼睛里,如果眼睛也放不下,那我们可以考虑把这份拷贝放到浏览器里,从这个逻辑上看,越靠近大脑的数据越能快速的被我们接受到. 


    那么本文的目的其实就是为了研究如何使用大脑和眼睛来缓存数据------------------------吃惊吧,ahuaxuan瞎扯的,回到正题,上面这段调侃不是为了说明别的,而是为了说明越靠近用户的数据被用户感受到的速度就越快.也就是近与快的关系. 

    接着再让我们抛开缓存先不说,来说说CDN和镜像的问题,CDN的英文名字叫CDN,中文名字一般还是CDN(请换个调朗诵).呵呵,CDN中文名字是内容分布网络,简单来说就是把内容分布出去,比如放到全国几个地方,举例来说做一个图片服务,上海的用户请求某个图片服务器,那么只需要返回某个离上海最近的CDN节点上的图片,而不需要路由到北京或者云南的节点上去取数据,您要问为啥呢,因为快啊,上海的用户访问北京节点的数据显然在路由层次上,网络时间消耗上都要多出很多,这说明啥呀,还是那个理儿:近就会快啊 

    一般来说CDN都是放一些图片,视频,文档之类的数据,那么元数据呢,放一块儿,当然也不是,这时候可以用镜像来解决元数据的问题,于是变成了上海的用户访问上海的镜像,北京的用户访问北京的镜像.这还不是就地取材比较方便嘛. 

    嗯,说到这里,想必大家对近和快的关系有了一定的认识了,下面我们来看看如何把这种原理或者规则运用到缓存中去. 

    下面让ahuaxuan和大家先调查一下离眼睛最近的是什么,显示器(别跟我说是屏幕保护膜和键盘哈,鼠标也不行),不过这些是硬件呀,那软的呢,非浏览器莫数了.也就是说如果我们把一些可以缓存在浏览器上的数据缓存到浏览器上,那就能加快我们的页面响应速度了.也就是说我们现在找到一个地方,也许可以放一点可以缓存的数据. 

    下面我们要考察考察什么样的数据可以缓存在浏览器上,以及缓存在浏览器上的一些优缺点或者限制因素 
    什么样的数据可以缓存在浏览器上? 
    浏览器上无法就几种数据,html,css,js,image,等.那么接着我们来看看他们的变化特性, 
    html数据很多情况下是动态的,但是也有很多情况下是某个时间段内可以是静态的. 
    Css一般是静态的 
    Js一般也是静态的 
    Image一般也是静态的. 

    哟,看上去后几者基本都可以缓存在浏览器,而html是否缓存要看html中数据的特性了.那么问题来了,浏览器是依据什么设置来缓存html,或者css,或者js的呢.答曰,expires或者max-age. 
    Expires代表该份数据缓存到浏览器上,直到某个时间点后过期,而max-age表示缓存在浏览器上直到某个时间段之后过期. 

    对于静态内容:设置文件头过期时间Expires的值为“Never expire”(永不过期) 
    动态页面,在代码中添加cache-control,表示多少时间之后过期,如: 
    response.setHeader("Cache-Control", "max-age=3600");表示1个小时后过期,即在浏览器上缓存一个小时. 

    但是这样问题又来了,如果设置10天后过期,那我明天就要改变,css,js都变了,咋办呐,答曰,加版本号吧,这样浏览器就会重新加载新的css和js了. 
    但是如果是动态数据,那就没有折了,所以动态数据的max-age一般不推荐太大,否则啊,您呐,就得挨个通知您得用户按一下Ctril+F5了. 

    一般来说静态数据需要缓存,我们一般通过webserver(如apache,lighttpd之流),只需要配置一下即可,而动态数据是比较重要的,因为其改变的周期段,而且只能由servlet容器或者fastcgi之类的服务器返回,所以其应付大量并发的能力是有限的.那么这里理论上可能有一个瓶颈,就是如果访问的独立用户较多,那么这份动态数据还是会被请求1*用户数 = n次,那么我们可以想象,一样的请求对于我们的servlet容器或者fastcgi来说其实是多余的,我们可以想一个方法,把这些一样的请求挡在servlet容器或者fastcgi进程之前. 

    正如在第一说中说到的,在浏览器和servlet容器或者fastcgi进程之间,还有很大的空间可以发挥,在这一部分的缓存,ahuaxuan称之为webcache. 

    目前在webcache届,最流行的估计就属squid了,然后还有varnish等等.为了有一个比较直观的感受,我们来看看下面这张图呗: 
     


    从这张图上,我们可以看出,浏览器1在请求了一份数据之后,其实这份数据已经在webcache上了,浏览器再来请求2的时候,请求到了webcache这层就返回了,这样就降低了servlet container的压力了.虽然说我们在servlet容器上也是可以建page cache,但是毕竟servlet本身的并发能力有限.(如何在servlet container上使用page cache见:http://www.iteye.com/topic/128458

    而且更重要的是一般webcache的并发能力要比servlet container或者fastcgi process要高出很多 (没办法,谁叫它是专业的cache呢).所以使用webcache也能够提供更高访问量的服务.一举多得,何乐而不为呢.但是声明一下,您呐,别以为上面这种方式是标准方式,我们还有webserver,负载均衡器等等,上图只是为了便于说明本文的论点,而且互连网需求和解决方案层出不穷,切不可以胡搬乱套,还是要分析分析再分析,思考,思科再思考. 

    说到这里即使以前没有接触过得筒子大概也明白了web cache得作用了.下面我们再来看看如何使用web cache呢,呵呵,其实和浏览器上缓存数据得方式一样.也是通过在response header中指定expires或者max-age来实现的.(但是据ahuaxuan观察在使用squid的时候有一个要求,浏览器的请求必须满足http的语义,也就是说只有method=get的时候web cache才能缓存数据,如果是post,那么web cache认为这个是一个创建数据的请求,并不会缓存其返回结果.) 

    Squid,如果您要系统的学习squid,请看: 
    http://www.squid-cache.org/ 
    http://blog.s135.com/book/squid/ 

    varnish如果您想了解varnish,请看附件 

    补充,在有些情况下,web cache中的数据很有可能是有状态的.比如根据浏览器的locale返回不同的数据,那么虽然访问的url是一样的,但是返回的值却是不一样的,咋办呢,别担心,我们有vary,只要在response里指定vary参数为accept-language就ok.您也可以指定为cookie中的值,这就完全看您的需要了.如果您还是不明白vary的作用,请看:http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44 

    总结: 
    说到这里,关于近和快的话题也基本可以结束了(这个话题再写下去就变成裹脚布了).所以一般情况下,我们可以认为有如下事实:”近==快”,但是近和快并不只是表现在人的体验上,如果换个角度,速度的感受者不是人,而是机器,那么我们也可以这么认为local cache比remote cache更靠近cpu,所以local cache的速度更快(当然他们的功能不是重叠的,各自适用的场景不一样而已,而且很多情况下他们可以配合使用,在后续的文章中将会讨论这个问题). 

    还是那句话:本文是ahuaxuan从自己的实践中总结出来的一些小小的心得,未参考任何文章,所以可能未必好,未必全面,未必令您满意,欢迎拍砖. 


    注: 
    按照看一送一的原则,本文还赠送以下内容: 
    如何在只使用tomcat的情况下,自动缓存js和css或者image等文件. 
    该方法分为以下3个步骤 
    第一步:写一个filter,可以根据路径的正则来判断该路径的请求是否需要设置max-age: 
    Java代码  收藏代码
    1. /** 
    2.  *  
    3.  * @author ahuaxuan 
    4.  * @date 2008-12-4 
    5.  * @version $id$ 
    6.  */  
    7. public class CacheFilter implements Filter{  
    8.   
    9.     private static transient Log logger = LogFactory.getLog(CacheFilter.class);  
    10.       
    11.     private Integer cacheTime = 3600 * 24;  
    12.     private List<Pattern> patternList = new ArrayList<Pattern>();  
    13.       
    14.     private static ResourceBundle rb = ResourceBundle.getBundle("cache-pattern");  
    15.     public void destroy() {  
    16.           
    17.     }  
    18.   
    19.     public void doFilter(ServletRequest rq, ServletResponse rqs,  
    20.             FilterChain fc) throws IOException, ServletException {  
    21.           
    22.         fc.doFilter(rq, rqs);  
    23.         if (rq instanceof HttpServletRequest && rqs instanceof HttpServletResponse) {  
    24.             HttpServletRequest request = (HttpServletRequest) rq;  
    25.             HttpServletResponse response = (HttpServletResponse) rqs;  
    26.               
    27.             if (matchPattern(request.getRequestURI())) {  
    28.                 response.setHeader("Cache-Control""max-age=" + cacheTime);  
    29.                 if (logger.isDebugEnabled()) {  
    30.                     StringBuilder sb = new StringBuilder();  
    31.                     sb.append(" set cache control for uri = ").append(request.getRequestURI());  
    32.                     sb.append(" and the cache time is ").append(cacheTime).append(" second");  
    33.                     logger.debug(sb.toString());  
    34.                 }  
    35.             }  
    36.           
    37.         } else {  
    38.             if (logger.isWarnEnabled()) {  
    39.                 logger.warn("---- the request instance is not instanceof HttpServletRequest ---");  
    40.                 logger.warn("---- the response instance is not instanceof HttpServletResponse ---");  
    41.             }  
    42.         }  
    43.           
    44.     }  
    45.   
    46.     public void init(FilterConfig arg0) throws ServletException {  
    47.         Enumeration<String> keys = rb.getKeys();  
    48.         while (keys.hasMoreElements()) {  
    49.             String p = keys.nextElement();  
    50.             String value = rb.getString(p);  
    51.             patternList.add(Pattern.compile(value, Pattern.CASE_INSENSITIVE));  
    52.               
    53.             if (logger.isInfoEnabled()) {  
    54.                 logger.info(">>>>>>>>>>> init the cache pattern " + value);  
    55.             }  
    56.         }  
    57.           
    58.         if (arg0 != null) {  
    59.             String ct = arg0.getInitParameter("cache-time");  
    60.             if (!"".equals(ct) && null != ct) {  
    61.                 cacheTime = new Integer(ct);  
    62.                 if (logger.isInfoEnabled()) {  
    63.                     logger.info(">>>>>>>>>> the cache time is " + cacheTime);  
    64.                 }  
    65.             }  
    66.         }  
    67.     }  
    68.       
    69.     private boolean matchPattern(String url) {  
    70.         for (Pattern pattern : patternList) {  
    71.             if (pattern.matcher(url).matches()) {  
    72.                 return true;  
    73.             }  
    74.         }  
    75.           
    76.         return false;  
    77.     }  
    78.   
    79.     public static void main(String [] args) throws ServletException {  
    80.         CacheFilter cf = new CacheFilter();  
    81.         cf.init(null);  
    82.         System.out.println(cf.matchPattern("/css/prototype.CSS"));  
    83.     }  
    84. }  
    第二步:在classpath路径下创建一个cache-pattern.properties文件,内容如下: 
    Java代码  收藏代码
    1. 1 = .*ext-all.js  
    2. 2 = .*prototype.js  
    3. 3 = .*/css/.*\.css  

    在这个配置文件中,您可以根据js和css的路径来配置哪些目录,或者哪些文件需要设置max-age. 

    第三步: 
    在web.xml添加如下内容: 
    Java代码  收藏代码
    1. <filter>  
    2.          <filter-name>cache-filter</filter-name>  
    3.          <filter-class>com.filter.CacheFilter</filter-class>  
    4.          <init-param>  
    5.             <param-name>cache-time</param-name>  
    6.             <param-value>86000</param-value>  
    7.         </init-param>  
    8.     </filter>  
    9.   
    10. <filter-mapping>  
    11.         <filter-name>cache-filter</filter-name>  
    12.         <url-pattern>*.js</url-pattern>  
    13.     </filter-mapping>  
    14.       
    15.     <filter-mapping>  
    16.         <filter-name>cache-filter</filter-name>  
    17.         <url-pattern>*.css</url-pattern>  
    18. </filter-mapping>  


    如此3步,就可以将js和css文件缓存于无形.快哉. 

    仓卒之间成文,再加上ahuaxuan水平有限,本文如有纰漏之处,还望各位看官您不吝指正,先谢过了. 


  • 相关阅读:
    如何让百度网盘下载速度达60MB/s!
    记一次内存溢出问题的排查、分析过程及解决思路
    使用maven命令打包可执行jar方法
    java实现四则运算
    POI如何合并单元格
    我是如何从功能测试成功转型自动化测试人员的?
    Edgar:Netflix分布式系统的可视化问题诊断平台实践
    Uber的API生命周期管理平台边缘网关(Edge Gateway)的设计实践
    UBer面向领域的微服务体系架构实践
    技术团队:问题被过度的夸大小题大做,你该怎么办?
  • 原文地址:https://www.cnblogs.com/daichangya/p/12959285.html
Copyright © 2020-2023  润新知