自古兵家多谋,《谋攻篇》,“故上兵伐谋,其次伐交,其次伐兵,其下攻城。攻城之法,为不得已”,可见攻城之计有不少种,而爬墙攻城是最不明智的作法,军队疲惫受损、钱粮损耗、百姓遭殃。故而咱们有不少迂回之策,谋略、外交、军事手段等等,每一种都比攻城的代价小,更轻量级,缓存设计亦是如此。java
1、为何要设计缓存呢?
其实高并发应对的解决方案不是互联网首创的,计算机先祖们很早就对相似的场景作了方案。好比《计算机组成原理》这样提到的cpu缓存概念,它是一种高速缓存,容量比内存小可是速度却快不少,这种缓存的出现主要是为了解决cpu运算速度远大于内存读写速度,甚至达到千万倍。redis
传统的cpu经过fsb直连内存的方式显然就会由于内存访问的等待,致使cpu吞吐量降低,内存成为性能瓶颈。同时又因为内存访问的热点数据集中性,因此须要在cpu与内存之间作一层临时的存储器做为高速缓存。算法
随着系统复杂性的提高,这种高速缓存和内存之间的速度进一步拉开,因为技术难度和成本等缘由,因此有了更大的二级、三级缓存。根据读取顺序,绝大多数的请求首先落在一级缓存上,其次二级...数据库
故而应用于SOA甚至微服务的场景,内存至关于存储业务数据的持久化数据库,其吞吐量确定是远远小于缓存的,而对于java程序来说,本地的jvm缓存优于集中式的redis缓存。缓存
关系型数据库操做方便、易于维护且访问数据灵活,可是随着数据量的增长,其检索、更新的效率会愈来愈低。因此在高并发低延迟要求复杂的场景,要给数据库减负,减小其压力。
2、给数据库减负
一、缓存分布式,作多级缓存数据结构
二、读请求时写缓存
写缓存时一级一级写,先写本地缓存,再写集中式缓存。具体些缓存的方法能够有不少种,可是须要注意几项原则:
(1)不要复制粘贴,避免重复代码;
(2)切忌和业务耦合太紧,不利于后期维护;
(3)开发初期刚刚上线阶段,为了排查问题,经常会给缓存设置开关,可是开关设置多了则会同时升高系统的复杂度,须要结合一套统一配置管理系统,京东物流有一套叫作UCC......并发
综上所述,高耦合带来的痛,弥补的代价是很大的,因此能够借鉴Spring cache来实现,实现也比较简单,使用时一个注解就搞定了。框架
三、写缓存失败了怎么办?应该先写缓存仍是数据库呢?
既然是缓存的设计,那么策略必定是保证最终一致性,那么咱们只须要采用异步消息来补偿就行了。异步
大部分缓存应用的场景是读写比差别很大的,读远大于写,在这种场景下,只须要以数据库为主,先写数据库,再写缓存就行了。jvm
最后补充一点,数据库出现异常时,不要一股脑的catch RuntimeException,而是把具体关心的异常往外抛,而后进行有针对性的异常处理。
四、关于其余性能方面
缓存设计都是占用越少越好,内存资源昂贵以及太大很差维护都驱使咱们这样设计。因此要尽量减小缓存没必要要的数据,有的同窗图省事把整个对象序列化存储。另外,序列化与反序列化也是消耗性能的。
五、vs各类缓存同步方案
缓存同步方案有不少种,在考虑一致性、数据库访问压力、实时性等方面作权衡。总的来讲有如下几种方式:
(1)懒加载式
如上段提到的方式,读时顺便加载。为了更新缓存数据,须要过时缓存。
优势:简单直接
缺点:
会形成一次缓存不命中,这样当用户并发很大时,刚好缓存中无数据,数据库承担瞬时流量过大会形成风险。
懒加载式太简单了,没有自动加载,异步刷新等机制,为了弥补其缺陷,请参见接下来的两种方法。
(2)补充式
能够在缓存时,把过时时间等信息写到一个异步队列里,后台起个线程池按期扫描这个队列,在快过时时主动reload缓存,使得数据会一直保持在缓存中,若是缓存没有也没有必要去数据库查询了。常见的处理方式有使用binlog加工成消息供增量处理。
优势:刷新缓存变为异步的任务,对数据库的压力瞬间因为任务队列的介入而下降了,削平并发的波峰。
缺点:消息一旦积压会形成同步延迟,引入复杂度。
(3)定时加载式
这就须要有个异步线程池按期把数据库的数据刷到集中式缓存,如redis里。
优势:保证全部数据最小时间差同步到缓存中,延迟很低。
缺点:如补充式,须要一个任务调度框架,复杂度提高,且要保证任务的顺序。若是递进一步还想加载到本地缓存,就得本地应用本身起线程抓取,方案维护成本高。能够考虑使用mq或者其余异步任务调度框架。
ps:为了防止队列过大调度出现问题,处理完的数据要尽快结转,且要对积压数据以及写入状况作监控。
六、防止缓存穿透
缓存穿透是指查询的key压根不存在,从而缓存查询不到而查询了数据库。如果这样的key刚好并发请求很大,那么就会对数据库形成没必要要的压力。怎么解决呢?
把全部存在的key都存到另一个存储的Set集合里,查询时能够先查询key是否存在。
干脆简单一些,给查询不到的key也加一个标识空值的Value,这样就不会去查询数据库了,好比场景为查询省市区街道对应的移动营业厅,如果某街道确实没有移动营业厅,key规则不变,value能够设置为"0"等无心义的字符。固然此种方案要保证缓存集群的高可用。这些Key可能不是永远不存在,因此须要根据业务场景来设置过时时间。
七、热点缓存与缓存淘汰策略
有一些场景,须要只保持一部分的热点缓存,不须要全量缓存,好比热卖的商品信息,购买某类商品的热门商圈信息等等。
综合来说,缓存过时的策略有如下三种:
(1)FIFO(First In,First Out)
先进先出,淘汰最先进来的缓存数据,一个标准的队列。
以队列为基本数据结构,从队首进入新数据,从队尾淘汰。
(2)LRU(Least RecentlyUsed)
最近最少使用,淘汰最近不使用的缓存数据。若是数据最近被访问过,则不淘汰。
A、和FIFO不一样的是,须要对链表作基本模型,读写的时间复杂度是O(1),写入新数据进入头部,链表满了数据从尾部淘汰;
B、最近时间被访问的数据移动到头部,实现算法有不少,如hashmap+双向链表等等;
C、问题在于如果偶发性某些key被最近频繁访问,而很是态,则数据受到污染。
(3)LFU(Least Frequently used)
最近使用次数最少的数据被淘汰,注意和LRU的区别在于LRU的淘汰规则是基于访问时间。
A、LFU中的每一个数据块都有一个引用计数,数据块按照引用计数排序,如果刚好具备相同引用计数的数据块则按照时间排序;
B、由于新加入的数据访问次数为1,因此插入到队列尾部;
C、队列中的数据被新访问后,引用计数增长,队列从新排序;
D、当须要淘汰数据时,将已经排序的列表最后的数据块删除;
E、有很明显问题是若短期内被频繁访问屡次,好比访问异常或者循环没有控制住,然后很长时间未使用,则此数据会由于频率高而被错误的保留下来没有被淘汰。尤为对于新来的数据,因为其起始的次数是1,因此即使被正常使用也会由于比不过老的数据而被淘汰。因此维基百科说纯粹的LFU算法不常常单独使用而是组合在其余策略中使用。
八、缓存使用的一些常见问题:Q:那么应该选择用本地缓存(local cache)仍是集中式缓存(Cache cluster)呢?A:首先看数据量,看缓存更新的成本,若是总体缓存数据量不是很大,并且变化的不频繁,那么建议本地缓存。 Q:怎么批量更新一批缓存数据?A:依次从数据库读取,而后批量写入缓存,批量更新,设置版本过时key或者主动删除。 Q:若是不知道有哪些key怎么按期删除?A:拿redis来讲keys * 太损耗性能,不推荐。能够指定一个集合,把全部的key都存到这个集合里,而后对整个集合进行删除,这样便能彻底清理了。 Q:一个key包含的集合很大,redis没法作到内存空间上的均匀Shard?A:一、能够简单的设置key过时,这样就要容许有缓存不命中的状况;二、给key设置版本,好比为两天后的当前时间,而后读取缓存时用时间判断一下是否须要从新加载缓存,做为版本过时的策略。