IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 缓存管理经验分享 -> 正文阅读

[大数据]缓存管理经验分享

缓存穿透/击穿/缓存雪崩

概述

目前主流的数据库无法承载高并发的读取,因此后端应用大量使用分布式缓存来应对高并发的读流量,如果因为缓存穿透/击穿/雪崩原因导致大量流量打到数据库上,可能导致数据库挂掉。

缓存穿透

访问一个缓存和数据库都不存在的 key,此时会直接打到数据库上,并且因为查不到数据,也不会写入缓存,所以下次同样会打到数据库,请求每次都会走到数据库,流量大时可能会击垮数据库。

解决方案

【1】空值缓存:查询数据库发现没有数据,给对应的 key存入一个空值缓存,代码数据库中没有数据,应用查询到空值就直接返回,避免了穿透到DB。
【2】布隆过滤器:①、空值缓存已经能解决问题,但是再数据量非常大的情况下,我们还得考虑空间利用的高效。
②、布隆过滤器的数据结构,它有一个初始值为0的bit数组以及N个 hash函数构成,它可以快速判断或标记当前值是否存在,标记的主要步骤为三个:
■ 使用 N个 hash函数计算出N个 hash值。
■ N个hash值与 bit数组取模得到 N个映射的下标。
■ 将 bit数组中 N个下标对应的值置为1。
③、当我们需要查询一个值时,重复上述步骤1,2 得到 N个下标,当这 N个下标上对应的值均为1,则说明该值已被标记,否则未被标记。
④、非法值校验:对于一些请求参数,我们能够判断出是否合法,如果不合法直接在入口处拦截,自然不需要穿透到DB。

缓存击穿

存在一个高并发的热点 key,在缓存过期的一瞬间,有大量的读请求,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬间数据库请求量大,压力骤增,可能导致数据库挂掉。

解决方案

【1】:①、当大量数据读取缓存并失败时,其实只需要一个线程/机器去访问数据库并填充数据,其他线程/机器只需要等待缓存填充完成后读取缓存就行。
②、我们可以通过互斥锁使得只有一个线程/机器能够访问数据库并填充缓存,线程间的同步比较容易,因为是在同一个进程内,使用 java提供的同步机制,如果 ReentranLock即可。
③、机器间的同步,由于涉及到分布式环境,因此需要涉及到分布式锁进行同步。简单的分布式锁可通过Redis或者DB实现,更高级的可以通过ZK这样的分布式协调服务来实现。
【2】热点数据不过期:既然缓存击穿是由热点Key过期导致的,那么我们可以不为热点数据设置过期时间,而是由专门的后台Job应用进行定时的刷新。

缓存雪崩

大量的 key设置了相同的过期时间,或者因为 Redis实例宕机,在同一时刻缓存全部失效,造成请求全部穿透到DB,瞬间DB请求量大,压力骤增,可能导致DB被打卦。

解决方案

【1】过期时间打散:避免大量 key 同时过期,可以在设置过期时间时,在基础过期时间上加一个随机值来打散过期时间,通过打散过期时间,避免同一时间大量缓存过期。
【2】不过期:不为数据设置过期时间或者设置比较长的过期时间,由专门的 job应用定时刷新缓存。
【3】Redis 高可用:通过主从复制,故障转移等高可用机制避免 Redis集群不可用导致的缓存雪崩。
【4】数据库的限流与熔断:对数据的读添加限流与熔断机制,目的主要是为了在无可避免的缓存雪崩时保护数据库,使得之后的快速恢复缓存。
【5】数据库解耦:应用于数据库解耦,只读 Redis,由专门的 Job应用主动填充缓存。

缓存预热

概述

目前我们所使用的缓存大致可以分为两种,一种是分布式缓存例如 redis,另外一种就是本地緩存,有很多组件。
我们都知道一般使用缓存的数据都是热数据。所以如果存在缓存击穿,或者缓存失效容易引起程序雪崩。因此缓存的初始化和填充预热就是很必要的。

填充緩存的必要性

1、采用被动填充,就会使得机器拉入集群以后,生产流量大量涌入,线程阻塞等待缓存数据返回,线程池打满,另外存在大量的反序列化,CPU打满的情况。导致服务不可用。
2、解决方案:采用主动填充在系统启动时预热缓存数据。
3、采用被动填充,在整个集群加载的数据不一致,可能会导致因为有些机器长时间没加载,导致加载的时候和其他机器数据不一致的问题。

填充缓存时会遇到的问题

问题1:采用主动填充在系统启动时会造成大批量的机器(几百台机器*N 个Key)同时请求数据库填充数据,导致数据库被打爆。
解决方案:采用多级缓存,在发布时去 Redis中获取数据。如果 Redis没有数据,使用分布式锁去数据库中加载数据到Redis。
问题2:LocalCache 在填充的过程中,数据源数据膨胀,导致OOM
解决方案:需要熔断器,在填充缓存前检测数据总量是否大于设置的阈值,如果大于阈值发出警告并且不填充缓存。
问题3:分布式缓冲某节点失效,造成流量打到其他节点上,形成雪崩。
解决方案:采用一致性 hash,填充数据副本到相邻的两个节点。

Redis Key

大 Key,多Key的场景

1、简单的 key存储的 value很大。
2、hash,set,zset,list 中存储过多的元素(以万为单位)。
3、一个集群存储了上亿的Key,Key本身过多也带来了更多的空间占用。

大 Key的危害

1、内存占用大,空间不均匀。
2、操作耗时时,容易阻塞。
3、每次存取网络流量大,容易网络阻塞。

解决方案

分拆

1、单个 key存储的 value很大:需要整体存取:分拆成多个 key mget。 部分存取:使用 hash值分拆,或者存入 redis hash中的 field。
2、value 存储过多的元素:分桶:将大 hash,set,list 等按 field 的 hash 值模除进行分桶,分拆成多个集合。分区:对于时间有效性的可以加上时间后缀拆分。
3、key 过多:转为 hash结构存储。

删除

大 key 线上删除要使用 unlink,del 会阻塞(自然过期也会出现阻塞)

其他

1、redis hash 不能 expire field, redis 只能过期顶级 key。
2、mget:需要同时获取多个 key 的值时请使用 mget 而不是循环 get 多次
减少 redis 操作,每次请求都要消耗时间,比如 del 操作不需要先判断 exists,get 的值存一个 local 变量,不要对一个 key 重复 get。
3、根据业务场景,使用 redis 的不同的数据结构:list, hash, set, sorted set, bitmap。
4、热点 key 问题,可以将 key 加上后缀拆分到不同机器上。
附:阿里 Redis键值设计规范

大 Key改造

大 JSON

1、业务需求需要保存查询 sql 对应的结果集 k: sql hashcode → v: 查询结果 result 存入 redis,以及 k: sql hashcode → v:(时间、sql 语句、查询引擎),在本地存进 Map 序列化成 Json,由一个固定 key(SQL_CACHE)放入 redis,过期时间 7 天。
2、缓存设置:一个新查询进来,保存 sqlkey 以及 result,取出 SQL_CACHE 对应的 json,反序列化成 map,put 新的 kv,再序列化重写回 redis(此处会有事务问题,同时请求 set 会导致一个值丢失)。
3、相应的缓存删除更新逻辑,需要在监听 QMQ 表重刷或者 QConfig 切换表引擎的情况下,取出 SQL_CACHE 对应的 json,遍历此 map,对应的每个 sql 解析表名,匹配则做相应的删除更新操作。
4、没有多久 SQL_CACHE 这个 key 就变成了大 key 报警了。

redis hash

1、第一次改造就针对这个大 key 把原本的 map 直接存成 redis hash,避免反复序列化以及大 key 网络传输成本。
2、是此处有个错误使用:为了兼容原来的流程,改造的时候只是简单的替换,导致了故障:遍历 hash 的 keyset 然后依次 hget。
改造前

public List<CacheVo> getCacheList() {
    Map<String, CacheVo> map = getCacheMap();
    List<CacheVo> list = new ArrayList<>();
    Set<String> keys = map.keySet();
    for (String key : keys) {
      list.add(map.get(key));
    }
    return list;
  }

  private Map<String, CacheVo> getCacheMap() {
    Map<String, CacheVo> map = new HashMap<>();
    if (!provider.exists(CACHE_LIST_HASH)) {
      LogUtils.warn("Cache", "cache list is not exists");
      return map;
    }
    Set<String> fields = provider.hkeys(CACHE_LIST_HASH);
    for (String field : fields) {
            // 此处重新遍历了整个redis hash,但是上层最后只返回了value
      String value = provider.hget(CACHE_LIST_HASH, field);
      map.put(field, JackJsonUtils.parse(value, CacheVo.class));
    }
    return map;
  }

解决 bug后,由于我们只需要 valueList,直接使用 hvals

public List<CacheVo> getCacheList() {
    if (!provider.exists(getCacheListHash())) {
      LogUtils.warn("Cache", "cache list is not exists");
      return Collections.emptyList();
    }
    return provider.hvals(getCacheListHash()).stream().map(v -> JackJsonUtils.parse(v, CacheVo.class)).collect(Collectors.toList());
  }

3、某一次请求调用了 7000 次 redis 操作导致了 timeout 发现了该异常。
4、此处还有个编码有问题:是否走缓存的布尔变量应该优先判断,利用 java 判断短路机制跳出不去请求 redis exists。

// 此处是先判断在不在缓存里,再判断要不要读缓存
if (cacheManageService.isExists(sql) && readCache) {

//应该改成先判断要不要读缓存,利用布尔短路直接不用判断在不在缓存里,避免每次都要调用redis
if (readCache && cacheManageService.isExists(sql)) {

细分前缀 k/v,set 管理 keys

1、由于我们的 sql 自带时间属性,我们将 sql 和引擎一起做了 md5 or SHA1,java hashcode 碰撞概率大所以不采用,减少 key 的长度这里截取了前 8 位,TTL 一天,sql→ result 的缓存。这样每天或者切换引擎将自动生成新的 key,避免需要去删除或者更新缓存。
2、根据我们的按表删除逻辑,使用 redis 的 set,保存同一个表下面的所有的 sqlkey,以 tableName + 时间作为 set 的 key,TTL 一天,注意此处的时间要注意时区的问题。同时利用 set 的幂等操作,只需要调用 sset 即可,set 的大多数操作也是 O1 的复杂度。
3、最后用了一个固定前缀+时间作为 key 的 set 来管理当天的所有 tableKey,这样子的三层结构,把所有的 key 全部打散,避免了出现大 key 问题,同时调用链也很清晰,避免了不必要的遍历操作。注意是此处我们调研后只保留了删除逻辑。

避免数据膨胀

前言

内存作为一种总量有限的资源,一旦耗尽,立马会造成非常严重的后果,例如持续性 fullgc、oom 导致进程被杀死等问题。结果基本上是服务不可用。 对于绝大部分系统,我们会在内存中存放各种数据在加速系统的响应。 对于内存中存放的数据,我们一定要有一种感知:它们随时可能变大而撑爆了内存。 因此,我们一定要小心谨慎,确保数据量在受控范围内。

内存占用的计算

估计内存的占用是非常有必要的,如果我们能估计出单个对象的大小以及对象的大致数目,也就能够大致估计出整体的内存占用情况,从而做到心里有数。 下面介绍一下 java 中各类数据类型的内存布局。通过分析布局,我们可以精确的计算出单个类型的对象的大小。

基础数据类型

1、java 中一共有 8 种基础数据类型,每种各自占用的字节数如下:
在这里插入图片描述2、基础数据类型占用的字节数极少,是 java 中非常轻量的类型,应该尽量多的使用。
3、我们讨论的是 64 位 cpu、内存<=32G、开启了指针压缩机器的内存占用。下同。

对象类型

对象类型是复合类型,它有基础数据类型组成。 对象类型的内存占用分为 4 块:mark word、klass pointer、数据字段、padding。
1、mark word 的大小为 8 字节。
2、klass pointer 大小为 4 字节。
3、数据字段的大小为所有字段的类型对应的大小的和。
4、对象的内存占用必须为 8 的整数倍,不足需要用 padding 来补齐。
可以看到,对象的内存占用比较大,即使是一个空对象(没有任何数据字段),也有高达 16 字节的内存消耗。 对象类型由于比较重,使用它的时候要特别注意内存占用情况。

数组

数组实际上也是一种特殊的对象,此外,它还有个特有的字段:数组长度。 数组占用的内存分为 5 块:mark word、klass pointer、数组长度字段、数组元素、padding。
1、mark word 的大小为 8 字节。
2、klass pointer 大小为 4 字节。
3、数组长度字段的大小为 4 字节,
4、数组元素的大小为:数组长度*元素类型大小。
5、数组的内存占用必须为 8 的整数倍,不足需要用 padding 来补齐。

内存占用分析工具

1、如果要精准的计算内存占用情况,还是需要借助工具的分析,这类工具目前并不太多。
2、现在能用的是 jol-core,这是一个 java 官方推出了 jar 包,地址为 GitHub - openjdk/jol
3、jol-core 有些性能问题以及使用不方便的地方,携程内部有一个改动版:object-layout · develop · intlflightcommon / common-framework · GitLab (ctripcorp.com)

防止内存爆炸

使用原生类型替换 string

根据前面的分析,能轻易计算出一个 String 的内存占用是 50+个字节(根据 String 的长度而变化,但至少是 50)。 在机票的业务场景中,会大量出现短长度 string(长度只有 2/3 个字符,例如航司 2 字码,机场/城市三字码等),这样的数据使用内存占用高达 50 的 String 来表示存在巨大的浪费。 使用 CodeUtil,可以把 String 编码成原生的数据类型,从而节约大量的内存占用(通常也能提升性能)。 目前这种技术在国际机票大系统引擎、agg 等比较消耗内存的系统中大量使用。

使用对象池

一些业务场景中,往往存在大量数据完全相同的对象,如果能保证相同的对象在内存中只有一份,那么也能节省大量的内存占用。 例如,在引擎中,每一个 Fare 对象都有个字段,一共占用了 14 个字节,如下:
在这里插入图片描述
由于部分 Fare 的这 5 个字段完全一致,所以可以把这 5 个字段抽取到一个单独的对象中,再由 Fare 通过持有对象的引用来使用这些字段,如下:
在这里插入图片描述抽取的公共部分
在这里插入图片描述

限制容器最大容量

1、单个对象占用的内存通常不会太多,内存爆炸的情况几乎都表现为极大数量(上百万甚至上千万)的对象总和占据了大量的内存。
2、这些对象一般是存放在容器中的。大部分情况下,我们会使用 HashMap 在作为对象的容器(少部分会使用 Set 或者 List)。
3、HashMap 有一个缺陷:它没有容量上限。如果不断 put 元素,它会持续扩容以便容纳更多的元素。占用的内存空间会越来越大。
4、如果程序有 bug,或者仅仅是业务数据膨胀,HashMap 不断扩张,就有可能造成内存爆炸。
5、一种好的替代 HashMap 的方法是使用 Guava Cache,它是一种 LRU cache,支持限制元素的最大数量,从而把内存占用控制在合理范围内。

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-01-14 02:03:13  更:2022-01-14 02:04:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 13:22:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码