缓存
缓存
-
现在互联网应用(网站/App)的整体流程如图,用户请求从界面(浏览器/App)到网络转发、应用服务再到存储(数据库或文件系统),然后返回到界面呈现内容
-
缓存的使用可以出现在1~4的各个环节中,每个环节的缓存方案与使用各有特点
缓存特征
缓存是一个数据模型对象,有它的一些特征
命中率
命中率=返回正确结果数/请求缓存次数
命中率是衡量缓存有效性的重要指标,命中率越高,表明缓存的使用率越高。
最大元素(或最大空间)
缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间)
那么将会触发缓存启动清空策略,根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的使用缓存
淘汰策略
缓存的存储空间有限制,当缓存空间被用满时,由缓存清空策略来保证在稳定服务的同时有效提升命中率
FIFO(first in first out)
- 最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据
- 策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用
LFU(less frequently used)
- 无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间
- 策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略
LRU(least recently used)
- 无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间
- 策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性
缓存介质
从硬件介质上来看,内存和硬盘; 从技术上,可以分成内存、硬盘文件、数据库
- 内存:将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原
- 硬盘:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
- 数据库:前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力,其实,数据库也有很多种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),NOSQL数据库响应速度和吞吐量都远远高于我们常用的关系型数据库
缓存分类和应用场景
根据缓存与应用的耦合度,分为local cache(本地缓存)和remote cache(分布式缓存)
本地缓存
- 指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适
- 缺点也是缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费
编程直接实现缓存
个别场景下,我们只需要简单的缓存数据的功能,而无需关注更多存取、清空策略等深入的特性时,直接编程实现缓存则是最便捷和高效的
成员变量或局部变量实现
以局部变量map结构缓存部分业务数据,减少频繁的重复数据库I/O操作。缺点仅限于类的自身作用域内,类间无法共享缓存。
//一个本地的缓存变量
Map<String, Object> localCacheStoreMap = new HashMap<String, Object>();
List<Object> infosList = this.getInfoList(); for(Object item:infosList){ if(localCacheStoreMap.containsKey(item)){ //缓存命中 使用缓存数据
// todo
} else { // 缓存未命中 I/O获取数据,结果存入缓存
Object valueObject = this.getInfoFromDB();
localCacheStoreMap.put(valueObject.toString(), valueObject);
}
}
}//示例private List<Object> getInfoList(){ return new ArrayList<Object>();
}//示例数据库I/O获取private Object getInfoFromDB(){ return new Object();
}
静态变量实现
- 通过静态变量一次获取缓存内存中,减少频繁的I/O读取,静态变量实现类间可共享,进程内可共享,缓存的实时性稍差
- 这类缓存,优点是能直接在heap区内读写,最快也最方便
- 缺点同样是受heap区域影响,缓存的数据量非常有限,同时缓存时间受GC影响主要满足单机场景下的小数据量缓存需求,同时对缓存数据的变更无需太敏感,如一般配置管理、基础静态数据等场景
Ehcache
纯Java开源缓存框架,配置简单、结构清晰、功能强大,是一个非常轻量级的缓存实现,我们常用Hibernate里面就集成了相关缓存功能
Guava Cache
Guava Cache是Google开源的Java重用工具集库Guava里的一款缓存工具,实现的缓存功能
分布式缓存
指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存
memcached缓存
-
在服务端,memcached集群环境实际就是一个个memcached服务器的堆积,环境搭建较为简单
-
cache的分布式主要是在客户端实现,通过客户端的路由处理来达到分布式解决方案
- 客户端路由原理:
应用服务器在每次存取某key的value时,通过某种算法把key映射到某台memcached服务器nodeA上,因此这个key所有操作都在nodeA上 - memcached客户端采用一致性hash算法作为路由策略
- 客户端路由原理:
Redis缓存
缓存位置
浏览器
当 HTTP 响应允许进行缓存时,浏览器会将 HTML、CSS、JavaScript、图片等静态资源进行缓存
ISP
网络服务提供商(ISP)是网络访问的第一跳,通过将数据缓存在 ISP 中能够大大提高用户的访问速度
反向代理
反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求反向代理时就可以直接使用缓存进行响应。
本地缓存
使用 Guava Cache 将数据缓存在服务器本地内存中,服务器代码可以直接读取本地内存中的缓存,速度非常快。
分布式缓存
使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。
相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。不仅如此,服务器集群都可以访问分布式缓存,而本地缓存需要在服务器集群之间进行同步,实现难度和性能开销上都非常大。
数据库缓存
MySQL 等数据库管理系统具有自己的查询缓存机制来提高查询效率。
Java 内部的缓存
Java 为了优化空间,提高字符串、基本数据类型包装类的创建效率,设计了字符串常量池及 Byte、Short、Character、Integer、Long、Boolean 这六种包装类缓冲池。
CPU 多级缓存
CPU 为了解决运算速度与主存 IO 速度不匹配的问题,引入了多级缓存结构,同时使用 MESI 等缓存一致性协议来解决多核 CPU 缓存数据一致性的问题。
缓存问题
缓存穿透
-
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。
-
解决方案:
- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤
缓存雪崩
-
指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。
-
解决方案:
- 进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用
缓存一致性
-
缓存一致性要求数据更新的同时缓存数据也能够实时更新
-
要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据
-
解决方案:
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新
如何解决缓存失效问题
-
主要因素是高并发下,我们一般设定一个缓存的过期时间较长时,并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间在同一时刻,这个时候就可能引发——当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重
-
将缓存失效时间分散开,不设置统一的过期时间;例如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
-
缓存失效时产生的雪崩效应,将所有请求全部放在数据库上,这样很容易就达到数据库的瓶颈,导致服务无法正常提供。尽量避免这种场景的发生