- 缓存
- 缓存的分类
- 缓存特征
- 命中率
- 最大元素(或最大空间)
- 清空策略
- 缓存介质
- 缓存分类和应用场景
- 缓存的问题
- 缓存和数据库一致性
- 缓存穿透
- 缓存击穿
- 缓存雪崩
- 缓存污染
- 数据漂移
- 缓存踩踏
- 热点key
- 本地缓存
- 编程直接实现缓存
- a. 成员变量或局部变量实现
- b. 静态变量实现
- Ehcache
- Guava Cache
- Caffeine
- 分布式缓存
- memcached缓存
- Redis缓存
-
客户端缓存
客户端的的缓存主要在浏览器和APP客户端上。这是与用户最近的缓存,一般也没有多大,使用的时候需要注意对浏览器和APP的影响。
-
网络转发
这部分的缓存主要是利用CDN实现。CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。主要应用于图片小文件、大文件下载、视音频点播、直播流媒体、全站加速、安全加速。
有一个专门的例子 阿里技术提供解释CND工作原理。https://www.zhihu.com/question/36514327
-
单机缓存
- CPU缓存,一般的CPU缓存被分为L1/L2/L3三级缓存。基础知识:
https://zhuanlan.zhihu.com/p/80672073
-
数据库缓存
mysql的Query cache机制,这是个针对select语句的特性,会把查询语句和查询结果保存在一张hash表中,下一次用同样的sql语句查询时,mysql会先从这张hash表中获取数据,如果缓存没有命中,则解析sql语句,查询数据库。 当缓存的数据达到最大值(query_cache_size) 后,mysql会把老的数据删除掉,写入新的数据。
https://www.cnblogs.com/yuexiaoyun/articles/14435103.html
-
分布式缓存
一般是指采用独立的缓存组件,与业务应用分离。多个应用可以共享缓存。常用的分布式缓存组件有memcached,Redis。还有一些nosql查询的组件,比如hbase。
命中率=返回正确结果数/请求缓存次数,命中率问题是缓存中的一个非常重要的问题,它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。
最大元素(或最大空间)缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。最大元素指的就是缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间),那么将会触发缓存启动清空策略根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的时候缓存。
清空策略- FIFO(first in first out) : 先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
- LFU(less frequently used) :最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。
- LRU(least recently used) :最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
- 根据过期时间判断,清理过期时间最长的元素;
- 根据过期时间判断,清理最近要过期的元素;
- 随机清理;
- 根据关键字(或元素内容)长短清理等。
虽然从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,可以分成内存、硬盘文件、数据库。
- **内存:**将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常关闭而重新启动,数据很难或者无法复原。
- **硬盘:**一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
- **数据库:**前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力。现在使用数据库做缓存介质是不是又回到了老问题上了?其实,数据库也有很多种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。
缓存有各类特征,而且有不同介质的区别,那么实际工程中我们怎么去对缓存分类呢?在目前的应用服务框架中,比较常见的,时根据缓存与应用的藕合度,分为local cache(本地缓存)和remote cache(分布式缓存):
- 本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
- 分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
目前各种类型的缓存都活跃在成千上万的应用服务中,还没有一种缓存方案可以解决一切的业务场景或数据类型,我们需要根据自身的特殊场景和背景,选择最适合的缓存方案。
缓存的问题 缓存和数据库一致性在读的时候,不会出现不一致,主要是数据更新的时候,会出现数据的不一致。不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。
常规的解决方案:
缓存更新:
https://coolshell.cn/articles/17416.html
缓存穿透缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。查询的是数据库中不存在的数据,没有命中缓存而数据库查询为空,也不会更新缓存。导致每次都查库,如果不加处理,遇到恶意攻击,会导致数据库承受巨大压力,直至崩溃。
常规的解决方案:
-
接口层增加校验,如用户鉴权校验,key做基础校验,非常规的key的直接拦截;
-
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击;
-
布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小。
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
常规的解决方案:
-
设置热点数据永远不过期。
-
接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。
-
加互斥锁。
大批量的数据在同一时刻过期,大量的请求有在同一时间打到下游数据库,从而造成底层存储压力。同样的情况还发生在缓存宕机的时候。
常规的解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
- 设置热点数据永远不过期。
缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。或者是正常的缓存数据总是被其他非主线操作影响,导致被替换失效。
常规的解决方案:
- 配置合适的清空策略
- 缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。
数据漂移多发生在分布式缓存使用一致性hash集群模式下,当某一节点宕机,原本路由在此节点的数据,将被映射到下一个节点。
常规的解决方案:
这个问题,是我们使用一致性hash来保证缓存集群机器宕机时不会造成缓存大量失效方案带来的一些附加问题。因此需要保证一致性hash尽量的均匀(一致性hash虚拟节点的运用),防止数据倾斜的节点的宕机和恢复对其他节点造成冲击。
缓存踩踏缓存踩踏其实只是一种缓存失效场景的提法,底层原因是缓存为空或还未生效。关键是因为上游调用超时后唤起重试,引发恶性循环。
常规的解决方案:
发生这种踩踏的底层原因是对缓存这类公共资源拼抢,那么,就把公共资源加锁,消除并发拼抢。但是,加锁在解决公共资源拼抢的同时,引发了另一个问题,即没有抢占到锁的线程会阻塞等待唤醒,当锁被释放时,所有线程被一同唤醒,大量线程的阻塞和唤醒是对服务器资源极大的消耗和浪费,即惊群效应。
promise的工作原理
promise的原理其实是一种代理模式,,实际的缓存值被promise代替,所有的线程获取promise 并等待promise返回给他们结果 , 而promise负责去底层存储获取数据,通过异步通知方式,最终将结果返回给各工作线程。这样,就不会发生大量并发请求同时操作底层存储的情况。
热点key热点key的是指,某个key的请求飙升,导致缓存这个key的服务器宕机,请求会去请求数据库服务查询数据,然后将数据存储到其他缓存服务器,在请求不降低的情况下,导致越来越多的缓存服务器宕机,然后整个缓存集群不可用,最终导致整个系统的宕机。
常规的解决方案:
可以通过监控nginx日志对用户请求进行时间窗计数、建立多级缓存、服务器本地利用LRU缓存热点key、根据业务预估热点key提前预热等等;
本地缓存 编程直接实现缓存个别场景下,我们只需要简单的缓存数据的功能,而无需关注更多存取、清空策略等深入的特性时,直接编程实现缓存则是最便捷和高效的。
a. 成员变量或局部变量实现简单代码示例如下:
//示例
private List getInfoList() {
return new ArrayList();
}
//示例数据库IO获取
private Object getInfoFromDB() {
return new Object();
}
public void UseLocalCache() {
//一个本地的缓存变量
Map localCacheStoreMap = new HashMap(16);
List infosList = this.getInfoList();
for (Object item : infosList) {
if (localCacheStoreMap.containsKey(item)) {
//缓存命中 使用缓存数据
// todo
System.out.println("命中缓存");
} else { // 缓存未命中 IO获取数据,结果存入缓存
System.out.println("命中未缓存");
Object valueObject = this.getInfoFromDB();
localCacheStoreMap.put(valueObject.toString(), valueObject);
}
}
}
以局部变量map结构缓存部分业务数据,减少频繁的重复数据库I/O操作。缺点仅限于类的自身作用域内,类间无法共享缓存。
b. 静态变量实现最常用的单例实现静态资源缓存,代码示例如下:
/**
* 集群相关的帮助类
*
*/
public class ClusterHelper {
private static final Logger logger = Loggers.getLogger(ClusterHelper.class.getSimpleName());
private static volatile Map clustersInfoMap;
/**
* 获取集群信息粒度到实例节点
*/
public static NewClustersInfo getClustersNodeInfo() {
String res = HttpUtil.get(HttpUtil.getPrefixUrl(Config.getString(Commons.ES_SERVICE_ADDRESS_KEY),
Commons.SERVICE_CLUSTER_NODES_API));
if (res == null || "".equals(res)) {
logger.info("ClustersNodeInfo res is null");
return null;
}
return JSON.parseObject(res, NewClustersInfo.class);
}
public static synchronized void initClusters() throws IOException {
NewClustersInfo clustersInfo = getClustersNodeInfo();
if (clustersInfo == null) {
logger.warn("clustersInfo is null");
} else if (clustersInfo.getData() == null || clustersInfo.getData().size() == 0) {
logger.warn("clustersInfo's newCluster is empty.");
} else {
clustersInfoMap = new HashMap(clustersInfo.getData().size());
List clusterList = clustersInfo.getData();
for (NewCluster cluster : clusterList) {
// 去掉服务化中可能存在的脏数据
if (StringUtil.isNullOrEmpty(cluster.getClusterName())
|| StringUtil.isNullOrEmpty(cluster.getIdc())) {
logger.error("cluster or idc can't be empty, cluster:{}, idc:{}",
cluster.getClusterName(), cluster.getIdc());
continue;
}
logger.info("cluster name:{}, idc:{}",
cluster.getClusterName(),
cluster.getIdc());
// Agent中只收集部分机房
if (!Commons.MachineRoom.MRS.containsKey(cluster.getIdc())) {
logger.error("{} of idc is not exist", cluster.getIdc());
continue;
}
String idcEnName = Commons.MachineRoom.MRS.get(cluster.getIdc());
clustersInfoMap.put(idcEnName + Commons.CLUSTER_DELIMITER + cluster.getClusterName(), cluster);
}
}
}
public static NewCluster getNewClusterByClusterName(String key) {
return clustersInfoMap.get(key);
}
/**
* 在指标数量维度 计算出做大的每个分组的节点个数
*
* @return int
*/
public static int getEachNodeNumberMaxSizeOnMetrics() {
return Commons.MAX_METRICS / Commons.ONCE_NODE_METRICS;
}
/**
* 计算出做大的每个分组的索引个数
*
* @return int
*/
public static int getEachIndexNumberMaxSize() {
return Math.min(getEachIndexNumberMaxSizeOnMetrics(), getEachIndexNumberMaxSizeOnUrl());
}
/**
* 在指标数量维度 计算出做大的每个分组的索引个数
*
* @return int
*/
public static int getEachIndexNumberMaxSizeOnMetrics() {
return Commons.MAX_METRICS / Commons.ONCE_INDEX_METRICS;
}
/**
* 在URL长度限制维度 计算出做大的每个分组的索引个数
*
* @return int
*/
public static int getEachIndexNumberMaxSizeOnUrl() {
return Commons.HTTP_URL_MAXLENGTH / Commons.AVG_INDEX_NAME_LENGTH;
}
}
在做ES多集群管理的时候,由于节点的变化不是很频繁,可以使用通过静态变量一次获取缓存内存中,减少频繁的I/O读取或网络读取,静态变量实现类间可共享,进程内可共享,缓存的实时性稍差。
这类缓存实现,优点是能直接在heap区内读写,最快也最方便;缺点同样是受heap区域影响,缓存的数据量非常有限,同时缓存时间受GC影响。主要满足单机场景下的小数据量缓存需求,同时对缓存数据的变更无需太敏感感知,如上一般配置管理、基础静态数据等场景。
Ehcache待补
Guava Cache待补
Caffeine待补
分布式缓存 memcached缓存待补
Redis缓存待补
参考 原文地址1 原文地址2 原文地址3 原文地址4 原文地址5 原文地址6 原文地址7