聊聊对于缓存预热、缓存穿透、缓存雪崩、缓存击穿、缓存更新、缓存降级的定义理解

缓存穿透

定义

当查询Redis中没有的数据时,该查询会下沉到数据库层,同时数据库层也没有该数据,当这种情况大量出现或被恶意攻击时,接口的访问全部透过Redis访问数据库,而数据库中也没有这些数据,我们称这种现象为"缓存穿透"。缓存穿透会穿透Redis的保护,提升底层数据库的负载压力,同时这类穿透查询没有数据返回也造成了网络和计算资源的浪费。

解决方案:

  • 1 在接口访问层对用户做校验,如接口传参、登陆状态、n秒内访问接口的次数;
  • 2 利用布隆过滤器,将数据库层有的数据key存储在位数组中,以判断访问的key在底层数据库中是否存在;核心思想是布隆过滤器,在redis里也有bitmap位图的类似实现,布隆过滤器过滤器不能实现动态删除,有时间可以研究下布谷鸟过滤器,是布隆过滤器增强版本。布隆过滤器有误判率,虽然不能完全避免数据穿透的现象,但已经可以将99.99%的穿透查询给屏蔽在Redis层了,极大的降低了底层数据库的压力,减少了资源浪费。

基于布隆过滤器,我们可以先将数据库中数据的key存储在布隆过滤器的位数组中,每次客户端查询数据时先访问Redis:

  • 如果Redis内不存在该数据,则通过布隆过滤器判断数据是否在底层数据库内;
  • 如果布隆过滤器告诉我们该key在底层库内不存在,则直接返回null给客户端即可,避免了查询底层数据库的动作;
  • 如果布隆过滤器告诉我们该key极有可能在底层数据库内存在,那么将查询下推到底层数据库即可;

缓存击穿

定义

缓存击穿和缓存穿透从名词上可能很难区分开来,它们的区别是:穿透表示底层数据库没有数据且缓存内也没有数据,击穿表示底层数据库有数据而缓存内没有数据。当热点数据key从缓存内失效时,大量访问同时请求这个数据,就会将查询下沉到数据库层,此时数据库层的负载压力会骤增,我们称这种现象为"缓存击穿"。

解决方案

  • 延长热点key的过期时间或者设置永不过期,如排行榜,首页等一定会有高并发的接口;
  • 利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至Redis内,避免其他大量请求同时穿过Redis访问底层数据库;

缓存雪崩

定义

缓存雪崩是缓存击穿的"大面积"版,缓存击穿是数据库缓存到Redis内的热点数据失效导致大量并发查询穿过redis直接击打到底层数据库,而缓存雪崩是指Redis中大量的key几乎同时过期,然后大量并发查询穿过redis击打到底层数据库上,此时数据库层的负载压力会骤增,我们称这种现象为"缓存雪崩"。

事实上缓存雪崩相比于缓存击穿更容易发生,对于大多数公司来讲,同时超大并发量访问同一个过时key的场景的确太少见了,而大量key同时过期,大量用户访问这些key的几率相比缓存击穿来说明显更大。

解决方案

  • 在可接受的时间范围内随机设置key的过期时间,分散key的过期时间,以防止大量的key在同一时刻过期;
  • 对于一定要在固定时间让key失效的场景(例如每日12点准时更新所有最新排名),可以在固定的失效时间时在接口服务端设置随机延时,将请求的时间打散,让一部分查询先将数据缓存起来;
  • 延长热点key的过期时间或者设置永不过期,这一点和缓存击穿中的方案一样;

缓存预热

  • 如字面意思,当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至Redis内再提供出去使用,这种操作就成为"缓存预热"。
  • 缓存预热的实现方式有很多,比较通用的方式是写个批任务,在启动项目时或定时去触发将底层数据库内的热点数据加载到缓存内。

缓存降级

  • 缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,即使是有损部分其他服务,仍然需要保证主服务可用。可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。
  • 降级的目的是保证核心服务可用,即使是有损的。如去年双十一的时候淘宝购物车无法修改地址只能使用默认地址,这个服务就是被降级了,这里阿里保证了订单可以正常提交和付款,但修改地址的服务可以在服务器压力降低,并发量相对减少的时候再恢复。
  • 降级可以根据实时的监控数据进行自动降级也可以配置开关人工降级。是否需要降级,哪些服务需要降级,在什么情况下再降级,取决于大家对于系统功能的取舍。

缓存更新

缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。

redis 典型缓存架构设计问题及性能优化总结:

缓存穿透

查询一个根本不存在的数据,缓存层和存储层都不会命中。通常出于容错的考虑,如果从存储层查不到数据,则不写入缓存层。

原因:

  • 自身业务代码或数据有问题
  • 恶意攻击等造成大量空命中

解决方案1:缓存空对象

解决方案2:布隆过滤器

当布隆过滤哭喊 说某个值存在时,这个值可能不存在。当说它不存在时,那就肯定不存在。

对于不存在的数据布隆过滤器一般都能过滤掉,不再让请求再往后端发送。

布隆过滤器就是一个大型的位数组和几个不一样的无偏hash 函数,所谓无偏就是能够把元素的hash 值算得比较均匀。

这种方法适用于数据命中不高、数据相对稳定、实时性低的应用场景,通常是数据集较大,代码维护较为复杂,但是缓存空间占用较少。

可以用redisson实现布隆过滤器,引入依赖:

 1 <dependency>
2 <groupId>org.redisson</groupId>
3 <artifactId>redisson</artifactId>
4 <version>3.6.5</version>
5 </dependency>

示例代码:

1 package com.redisson;
2
3 import org.redisson.Redisson;
4 import org.redisson.api.RBloomFilter;
5 import org.redisson.api.RedissonClient;
6 import org.redisson.config.Config;
7
8 public class RedissonBloomFilter {
9
10 public static void main(String[] args) {
11 Config config = new Config();
12 config.useSingleServer().setAddress("redis://localhost:6379");
13 //构造Redisson
14 RedissonClient redisson = Redisson.create(config);
15
16 RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
17 //初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
18 bloomFilter.tryInit(100000000L,0.03);
19 //将zhuge插入到布隆过滤器中
20 bloomFilter.add("zhuge");
21
22 //判断下面号码是否在布隆过滤器中
23 System.out.println(bloomFilter.contains("guojia"));//false
24 System.out.println(bloomFilter.contains("baiqi"));//false
25 System.out.println(bloomFilter.contains("zhuge"));//true
26 }
27 }

使用布隆过滤器需要把所有数据提前放入布隆过滤器,并且在增加数据时也要往布隆过滤器里放,布隆过滤器 缓存过滤伪代码:

1  //初始化布隆过滤器
2 RBloomFilter<String> bloomFilter=redisson.getBloomFilter("nameList");
3 //初始化布隆过滤器:预计元素为100000000L,误差率为3%
4 bloomFilter.tryInit(100000000L,0.03);
5
6 //把所有数据存入布隆过滤器
7 void init(){
8 for (String key: keys) {
9 bloomFilter.put(key);
10 }
11 }
12
13 String get(String key) {
14 // 从布隆过滤器这一级缓存判断下key是否存在
15 Boolean exist = bloomFilter.contains(key);
16 if(!exist){
17 return "";
18 }
19 // 从缓存中获取数据
20 String cacheValue = cache.get(key);
21 // 缓存为空
22 if (StringUtils.isBlank(cacheValue)) {
23 // 从存储中获取
24 String storageValue = storage.get(key);
25 cache.set(key, storageValue);
26 // 如果存储数据为空, 需要设置一个过期时间(300秒)
27 if (storageValue == null) {
28 cache.expire(key, 60 * 5);
29 }
30 return storageValue;
31 } else {
32 // 缓存非空
33 return cacheValue;
34 }
35 }

注意:布隆过滤器不能删除数据,如果要删除得重新初始化数据。

缓存击穿

大量缓存同时失效导致请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大挂掉。最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。

int expireTime - new Random().nextInt(300) + 300;

缓存血崩

如果缓存架构设计得不好,大量请求访问bigkey,导致缓存能支撑的并发急剧下降,大量请求都会打到存储层,造成存储层也会级联宕机的情况。

解决问题:

  • 1 保证缓存层服务高可用性,比如使用redis Sentinel 或 redis Cluster
  • 2 依赖隔离组件为后端限流熔断并降级。比如使用 Sentinel 或 Hystrix 限流降级组件。

我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据,如商品属性,用户信息等,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据,如商品库存,仍然允许查询缓存,如果缓存缺失,可以继续通过数据库读取。

  • 3 做好数据容灾。提前演练,并做一些预案。

热点数据缓存优化

使用“缓存+过期时间”的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。

但是这种策略存在的问题,对应用却是致命的。

  • 当前key 是一个热点key, 如热门活动,并发量非常大
  • 重建缓存不能在短时间完成,可能是一个复杂计算,例如繁杂的SQL,多次IO, 多个依赖等,在缓存操作新选的瞬间,有大量纯种来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

解决这个问题,就是要避免大量纯种同时重建缓存。

可以利用互斥锁,此方法只允许一个纯种重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据。


String get(String key) { // 从redis 中获取数据
String value = redis.get(key); // 如果value 为空,则重构缓存
if(null == value){
// 只允许一个线程重建缓存,使用nx, 并设置过期时间ex
String mutexKey = "mutext:key:" + key; if(redis.set(mutexKey,"1","ex 180",nx)){
// 从数据库中取数据
value = db.get(key);
// 设置过期时间
redis.setex(key,timeout,value);
// 删除key_mutex
redis.delete(mutexKey);
} else {
Thread.sleep(50); get(key);
} }
return value; }

缓存数据库读写不一致

  • 1 双写不一致
  • 2 读写不一致

解决方案:

  • 1 对于并发几率很小的数据,如个人维度的订单数据,用户数据等,这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
  • 2 就算并发很高,如果业务上能容忍短时间的缓存数据不一致,如商品名称,商品分类菜单等,缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
  • 3 如果不能容忍缓存数据不一致,可以通过读写锁保证并发读写或写写的时间按顺序排好队,读读的时间相当于无锁。
  • 4 也可以用阿里开源的canal 通过监听数据库的binlog 日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

小结:一般针对是读多写少的情况加入缓存提高性能,如果写多读多的情况又不能容忍缓存数据不一致,那就没必要加缓存了,可以直接操作数据库。如果数据库抗不住压力,还可以把缓存作为数据读写的主存储,异步将数据同步到数据库,数据库只是作为数据的备份。

加入缓存的数据库应该是对实时性、一致性要求不是很高的数据,切记不要为了用缓存,同时又要保证绝对的一致性做大量的过度设计和控制,增加系统的复杂性。

开发规范

key 名设计

  • 1 建议:可读性和可管理性

以业务名或数据库名库前缀,防止key 冲突,用冒号分隔。如 业务名:表名:id

  • 2 建议:简洁性

保证语义的前提下,控制key 长度,当key 较多时,内存占用也不容忽视。

  • 3 强制:不要包含特殊字符

反例:空格,换行,单双引号以及其他转义字符

value 设计

  • 1 强制:拒绝bigkey ,防止网卡流量,慢查询

在redis 中,一个字符串最大512M, 一个二级数据结构可以存储大约40亿(2^32 -1)个元素,但是实际中如果有下面两种情况,我们认为它是bigkey。

  • 1 字符串类型:它的big 体现在单个value 值很大,一般认为超过10KB 就是bigkey。
  • 2 非字符串类型:hash, list,set, zset,它们的big 体现在元素个数太多。

一般来说,String 类型控制在10kb 以内, hash, list, set, zset 元素个数不要超过5000。非字符串的bigkey,不要使用del 删除,使用hscan,sscan,zscan 方式浙进式删除,同时要注意防止bigkey 过期时间自动删除问题。(如一个200w 的zset 设置一个小时过期,会触发 del 操作,造成阻塞)

bigkey 性能优化

1 bigkey 的产生

一般来说,bigkey 的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的。

  • 1 社交类:大V 粉丝列表,如果设计不当,必是bigkey
  • 2 统计类:如按天存储某项功能或者网站的用户集合,除非没人用,否则必是bigkey
  • 3 缓存类:将数据从数据库中load 出来序列化放在redis 中,但是要注意1-是不是有必要把所有字段都缓存;2-有没有相关关联的数据,为图方便而产生关联数据, 产生bigkey.

2 如何优化

1 拆

big list : list1, list2, ... , listN

big hash :可以将数据分段存储,比如一个大的key, 假设存了100w 的用户数据,可以拆分成200个key, 每个key 下面5000个用户数据。

2 推荐:选择适合的数据类型

举例:

// 正例:
hmset user:1 name tom age 20 favor swimming // 反例:
set user 1 : name tom
set user 1 : age 20
set user 1 : favor swimming

3 推荐:控制key 的生命周期

建议使用expire 设置过期时间,同时过期时间要随机,防止集中过期。

总结篇3:redis 典型缓存架构设计问题及性能优化的更多相关文章

  1. Redis 高可用架构设计(转载)

    转载自:https://mp.weixin.qq.com/s?__biz=MzA3NDcyMTQyNQ==&mid=2649263292&idx=1&sn=b170390684 ...

  2. Java进阶专题(十八) 系统缓存架构设计 (下)

    前言 上章节介绍了Redis相关知识,了解了Redis的高可用,高性能的原因.很多人认为提到缓存,就局限于Redis,其实缓存的应用不仅仅在于Redis的使用,比如还有Nginx缓存,缓存队列等等.这 ...

  3. Java生鲜电商平台-SpringCloud微服务架构中网络请求性能优化与源码解析

    Java生鲜电商平台-SpringCloud微服务架构中网络请求性能优化与源码解析 说明:Java生鲜电商平台中,由于服务进行了拆分,很多的业务服务导致了请求的网络延迟与性能消耗,对应的这些问题,我们 ...

  4. 亿级流量场景下,大型缓存架构设计实现【1】---redis篇

    *****************开篇介绍**************** -------------------------------------------------------------- ...

  5. Java进阶专题(十七) 系统缓存架构设计 (上)

    前言 ​ 我们将先从Redis.Nginx+Lua等技术点出发,了解缓存应用的场景.通过使用缓存相关技术,解决高并发的业务场景案例,来深入理解一套成熟的企业级缓存架构如何设计的.本文Redis部分总结 ...

  6. Redis 多级缓存架构和数据库与缓存双写不一致问题

    采用三级缓存:nginx本地缓存+redis分布式缓存+tomcat堆缓存的多级缓存架构 时效性要求非常高的数据:库存 一般来说,显示的库存,都是时效性要求会相对高一些,因为随着商品的不断的交易,库存 ...

  7. Redis秒杀系统架构设计-微信抢红包

    导读 前二天我写了一篇,Redis高级项目实战(点我直达),SpringBoot整合Redis附源码(点我直达),今天我们来做一下Redis秒杀系统的设计.当然啦,Redis基础知识还不过关的,先去加 ...

  8. Java 架构师+高并发+性能优化+Spring boot大型分布式项目实战

    视频课程内容包含: 高级 Java 架构师包含:Spring boot.Spring cloud.Dubbo.Redis.ActiveMQ.Nginx.Mycat.Spring.MongoDB.Zer ...

  9. HBase设计与开发性能优化(转)

    本文主要是从HBase应用程序设计与开发的角度,总结几种常用的性能优化方法.有关HBase系统配置级别的优化,这里涉及的不多,这部分可以参考:淘宝Ken Wu同学的博客. 1. 表的设计 1.1 Pr ...

  10. 高并发下的缓存架构设计演进及redis常见的缓存应用异象解决方案

    待总结 缓存穿透 缓存击穿 缓存雪崩等

随机推荐

  1. Java项目静态资源映射的几种方式

    一.Springboot 1.webjars方式 我们之前使用Maven构建一个Web项目时,在main目录下会存在一个webapp的目录,我们以前都是将所有的页面或静态资源导在这个目录下,但现在使用 ...

  2. CentOS7离线安装devtoolset-9并编译redis6.0.5

    首先参照https://www.cnblogs.com/wdw984/p/13330074.html,来进行如何安装Centos和离线下载rpm包. 离线下载jemalloc,上传到CentOS的/d ...

  3. Kafka消费端抛出异常Offset commit cannot be completed since the consumer is not part of an active group for auto partition assignment; it is likely that the consumer was kicked out of the group的解决方案

    总结/朱季谦 在一次测试Kafka通过consumer.subscribe()指定偏移量Offset消费过程中,因为设置参数不当,出现了一个异常提示-- [2024-01-04 16:06:32.55 ...

  4. [oeasy]python0074[专业选修]字节序_byte_order_struct_pack_大端序_小端序

    进制转化 回忆上次内容 上次 总结了 计算字符串值的函数 eval   四种进制的转化函数 bin oct int hex     函数名 前缀 目标字符串所用进制 bin 0b 二进制 oct 0o ...

  5. Django--StreamingHttpResponse下载文件

    from django.shortcuts import render, HttpResponse from django.http import StreamingHttpResponse impo ...

  6. CF30D King's Problem? 题解

    CF30D 题意 有 \(n+1\) 个点,其中的 \(n\) 个点在数轴上.求以点 \(k\) 为起点走过所有点的最短距离,允许重复. 思路 有两种情况: \(k\) 在数轴上(如图1). \(k\ ...

  7. Excel快速下拉填充序列至10000行

    问题:想要下拉输入的数据递增得到1.2.3--10000,但是手动下拉太累 解决: 1.如在A1单元格输入1,在A2单元格输入2 2.选中A2单元格,在上方名称框中填写A2:A1000,回车,此时将选 ...

  8. 2023/4/19 SCRUM个人博客

    1.我昨天的任务 初步了解了pandas库,对series和dataframe有了初步的学习使用 2.遇到了什么困难 对PYQT5的概念没有定义,准备进行学习 3.我今天的任务 学习了PYQT5的部分 ...

  9. 写写Redis十大类型bitmap的常用命令

    其实这些命令官方上都有,而且可读性很强,还有汉化组翻译的http://redis.cn/commands.html,不过光是练习还是容易忘,写一写博客记录一下 bitmap 位图,是由0和1状态表现的 ...

  10. 【NodeJS】操作MySQL

    1.在连接的数据库中准备测试操作的表: CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` ...