Redis: 缓存过期、缓存雪崩、缓存穿透、缓存击穿(热点)、缓存并发(热点)、多级缓存、布隆过滤器

2019年08月18日 16:34:24 hanchao5272 阅读数 1026更多

分类专栏: Redis 分布式
 
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。

1.缓存过期

缓存过期:在使用缓存时,可以通过TTL(Time To Live)设置失效时间,当TTL为0时,缓存失效。

为什么要设置缓存的过期时间呢?

一、为了节省内存

例如,在缓存中存放了近3年的10亿条博文数据,但是经常被访问的可能只有10万条,其他的可能几个月才访问一次。

那么,就没有必要让所有的博文数据长期存在于缓存中。

设置一个过期时间比方说7天,超过7天未被访问的博文数据将会自动失效,如此节省大量内存。

二、时效性信息

有些信息具有时效性,设置过期时间非常合适。例如:游戏中的发言间隔为10秒钟,可以通过缓存实现。

三、用于分布式锁

参考博客:Redis: 分布式锁的官方算法RedLock以及Java版本实现库Redisson

四、其他需求

2.缓存雪崩

缓存雪崩:某一时间段内,缓存服务器挂掉,或者大量缓存失效,导致大量请求直接访问数据库,给数据库造成极大压力,甚至宕机,严重时引起整个系统的崩溃。

解决办法:

  1. 数据库访问加锁
  2. 随机过期时间
  3. 定时刷新缓存
  4. 缓存刷新标记
  5. 多级缓存

2.1.数据库访问加锁

因为短时间内大量请求访问数据库,导致后续影响,那么限制数据库的访问量不就行了吗?

限制数据库访问量的方法有很多,对数据库的访问进行加锁就是一种最直接的方式。

下面分别给出的伪代码:

    /**
* 用于加锁的对象
*/
private static final byte[] LOCK_OBJ = new byte[0]; /**
* 获取商品信息
*/
public String getGoodsByLock(String key) {
//获取缓存值
String value = RedisService.get(key); // 如果缓存有值,就直接取出来即可
if (value != null) {
return value;
} else {
//对数据库的访问进行加锁限制
synchronized (LOCK_OBJ) {
value = RedisService.get(key);
if (value != null) {
return value;
} else {
//访问数据库
value = MySqlService.select(key);
//缓存刷新
RedisService.set(key, value, 10);
}
}
return value;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

分析:加锁会产生线程阻塞,导致用户长时间进行等待,体验不好,只适合并发量小的场景。

2.2.随机过期时间

缓存雪崩的主要原因是,短时间内大量缓存失效造成的,那么避免大量缓存同时失效不就行了吗?

避免大量缓存失效的最直接方法就是给缓存设置不同的过期时间。例如,原定失效时间30分钟,修改为失效时间在30~35分钟之内随机。

下面给出一种获取随机失效时间的简单实现作为参考:

    /**
* 获取随机失效时间
*
* @param originExpire 原定失效时间
* @param randomScope 最大随机范围
* @return 随机失效时间
*/
public static Long getRandomExpire(Long originExpire, Long randomScope) {
return originExpire + RandomUtils.nextLong(0, randomScope);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

**分析:**随机过期时间,虽然实现简单,但是并不能完全避免大量缓存的同时过期。

例如:大量缓存的过期时间设置在30~35分钟,但是无论如何随机,这些缓存经过40分钟后,都会过期。

造成如此结果的原因可能有很多,例如:过期时间设计不合理等。

2.3.定时刷新缓存

避免大量缓存失效的另一种策略就是:开发额外的服务,定时刷新缓存。

这样做,虽然能够保证缓存的失效,但是有个弊端:缓存可能多种多样,每种缓存都需要开发对应的定时刷新服务,相当麻烦。

2.4.缓存刷新标记

缓存失效标记,其实也是一种缓存刷新策略,只不过它更加通用化,无需针对每种缓存进行定制开发。

**思路:**不仅存储缓存数据,而且存储是否需要刷新的标记。

缓存刷新标记:

  • 标记数据是否应该被刷新,如果存在则表示数据无需刷新,反之则表示需要刷新。
  • 缓存刷新标记的过期时间要比缓存本身的过期时间要短,这样才能起到提前刷新的效果。可以设置为1:2,或者1:1.5

下面给出伪代码:

    /**
* 线程池:用于异步刷新缓存
*/
private static ExecutorService executorService = Executors.newCachedThreadPool();
/**
* 缓存刷新标记后缀
*/
public static final String REFRESH_SUFFIX = "_r"; /**
* 获取缓存刷新标记的key
*/
public String getRefreshKey(String key) {
return key + REFRESH_SUFFIX;
} /**
* 判断无需刷新: 刷新标记存在,则表示不需要刷新
*/
public boolean notNeedRefresh(String key) {
return RedisService.containsKey(key + REFRESH_SUFFIX);
} /**
* 获取商品信息
*/
public String getGoods(String key) {
//获取缓存值
String value = RedisService.get(key); //过期时间
Long expire = 10L; //如果无需刷新,则直接返回缓存值
if (notNeedRefresh(key)) {
//理论上:如果缓存刷新标记存在,则缓存必存在,所以可以直接返回
return value;
} else {
//如果需要刷新,则重置缓存刷新标记的过期时间
RedisService.set(getRefreshKey(key), "1", expire / 2); //如果缓存有值,就直接返回即可
if (value != null) {
//因为有值,所以可以异步刷新缓存
executorService.submit(() -> {
//访问数据库
String newValue = MySqlService.select(key);
//缓存刷新
RedisService.set(key, newValue, expire);
}); return value;
} else {
//因为无值,所以还是要同步刷新缓存
value = MySqlService.select(key);
//缓存刷新
RedisService.set(key, value, expire); return value;
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

分析:刷新标记本身也存在大量失效的可能。

2.5.多级缓存

所谓多级缓存,就是设置多个层级的缓存。

例如:

  • 本地缓存 + 分布式缓存构成二级缓存,本地缓存作为第一级缓存,分布式缓存作为第二级缓存。
  • 本地缓存可以通过多种技术实现,如:Ehcache、Caffeine等。
  • 分布式缓存一般采用Redis实现。
  • 由于本地缓存会占用JVM的heap空间,所以本地缓存中存放少量关键信息,其他的缓存信息存放在分布式缓存中。

下面是一个二级缓存示例的伪代码:

    /**
* 是否使用一级缓存
*/
@Setter
private boolean useFirstCache; /**
* 查询商品信息
*/
public String getGoods(String key) {
String value; //如果使用一级缓存,则首先从一级缓存中获取数据
if (useFirstCache) {
value = LocalCacheService.get(key);
if (value != null) {
return value;
}
} //如果一级缓存中无值,则查询二级缓存
value = RedisCacheService.get(key);
if (value != null) {
return value;
} else {
//如果二级缓存中也无值,则查询数据库
value = MySqlService.select(key);
//缓存刷新
RedisCacheService.set(key, value, 10);
return value;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

3.缓存穿透

缓存穿透:大量请求查询本就不存在的数据,由于这些数据在缓存中肯定不存在,所以会直接绕过缓存,直接访问数据库,给数据库造成极大压力,甚至宕机,严重时引起整个系统的崩溃。

举例:有些黑客恶意攻击网站,制造大量请求访问不存在的缓存,直接搞垮网站。

解决办法:

  1. 空值缓存
  2. 布隆过滤器

3.1.空值缓存

空值缓存:查询数据库为空时,仍然把设置成一种默认值进行缓存,这样后续请求继续请求这个key时,知道值不存在就不会去数据库查询了。

下面给出示例伪代码:

    /**
* 缓存空值
*/
public static final String NULL_CACHE = "_"; /**
* 获取商品信息
*/
public String getGoodsByLock(String key) {
//获取缓存值
String value = RedisCacheService.get(key); //如果缓存有值
if (value != null) {
//如果缓存的是空值,则直接返回空,无需查询数据库
if (NULL_CACHE.equals(value)) {
return null;
} else {
return value;
}
} else {
//访问数据库
value = MySqlService.select(key);
//如果数据库有值,则直接返回
if (value != null) {
//缓存刷新
RedisCacheService.set(key, value, 10);
return value;
} else {
//如果数据库无值,则设置空值缓存
RedisCacheService.set(key, NULL_CACHE, 5);
return null;
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

缺点:

  • 有可能设置空值缓存之后数据又有值了,这时如果无正确的刷新策略,会导致数据不一致,所以空值失效时间不要设置太长,例如5分钟即可。
  • 空值缓存虽然能够避免缓存穿透,但是如果存在大量请求不存在,则会储存大量空值缓存,消耗较多内存。

3.2.布隆过滤器

什么是布隆过滤器?

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的bit数组和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

简单理解布隆过滤器

  1. 首先,我们定义一个bit数组,每个元素只占1byte。

  1. 然后,在存放每个元素时,分表对其进行若干次(例如3次)哈希函数计算,将每个哈希结果对应的bit数组元素置为1。

  2. 最后,判断一个元素是否在bit数组中,只需对其同样进行若干次(例如3次)哈希函数计算,如果计算结果对应的bit数组元素都为1,则可以判断:这个元素可能存在与bit数组中;如果有任一个哈希结果对应的元素不为1,则可以判断:这个元素必定不存在于bit数组中。

关于布隆过滤器的实现有多种,常用的有guava包和redis。

guava版本的布隆过滤器

这里给出guava版本布隆过滤器的简单使用:

        //定义布隆过滤器的期望填充数量
Integer expectedInsertions = 100;
//定义布隆过滤器:默认情况下,使用5个哈希函数已保证3%的误差率。
BloomFilter<Long> userIdFilter = BloomFilter.create(Funnels.longFunnel(),expectedInsertions); //填充布隆过滤器
//获取全部用户ID List<Long> idList = MySqlService.getAllId();
List<Long> idList = Lists.newArrayList(521L,1314L,9527L,3721L);
if (CollectionUtils.isNotEmpty(idList)){
idList.forEach(userIdFilter::put);
} //通过布隆过滤器判断数据是否存在
log.info("521是否存在:{}",userIdFilter.mightContain(521L));
log.info("125是否存在:{}",userIdFilter.mightContain(125L));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

运行结果:

 INFO traceId: pers.hanchao.basiccodeguideline.redis.bloom.BloomFilterDemo:33 - 521是否存在:true
INFO traceId: pers.hanchao.basiccodeguideline.redis.bloom.BloomFilterDemo:34 - 125是否存在:false
  • 1
  • 2

**缺点:**是一种本地布隆过滤器,基于JVM内存,会占用heap空间,重启失效,不适用与分布式场景,不适用与大批量数据。

Redis版本的布隆过滤器

基于Redis的布隆过滤器实现,目前本人也并未深入了解,这里暂时就不班门弄斧了,各位可自行了解。

4.缓存热点并发

缓存热点并发: 大量请求查询一个热点Key,此key过期的瞬间来不及更新,导致大量请求直接访问数据库,给数据库造成极大压力,甚至宕机,严重时引起整个系统的崩溃。

解决办法:

  1. 缓存重建加锁
  2. 热点key不过期:重建缓存期间,数据不一致。
  3. 多级缓存。

4.1.缓存重建加锁

章节2.1.数据库访问加锁的思路类似,伪代码如下:

    /**
* 用于加锁的对象
*/
private static final byte[] LOCK_OBJ = new byte[0]; /**
* 通过某种手段(如配置中心等)判断一个值是热点key。这里为了示例直接硬编码
*/
private Set<String> hotKeySet = Sets.newHashSet("521", "1314"); /**
* 获取商品信息
*/
public String getGoodsByLock(String key) {
//获取缓存值
String value = RedisCacheService.get(key); // 如果缓存有值,就直接取出来即可
if (value != null) {
return value;
} else {
//如果是热点key,则对缓存重建过程进行加锁
if (hotKeySet.contains(key)) {
//对缓存重建过程进行加锁限制
synchronized (LOCK_OBJ) {
value = RedisCacheService.get(key);
if (value != null) {
return value;
} else {
//访问数据库
value = MySqlService.select(key);
//缓存刷新
RedisCacheService.set(key, value, 10);
}
}
} else {
//如果是普通Key,无需对缓存重建加锁
value = MySqlService.select(key);
//缓存刷新
RedisCacheService.set(key, value, 10);
} return value;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

虽然两者的代码类似,但是出发点不一样两者的不同:

  • 数据库访问加锁:针对的是所有的缓存。
  • 缓存重建加锁:针对的是热点Key。

同样的,加锁会产生线程阻塞,导致用户长时间进行等待,体验不好,只适合并发量小的场景。

4.2.热点key不过期

热点Key不过期很好理解,就是通过某种手段(查库、配置中心等等)确定某个key是热点key,则在建立缓存时,不设置过期时间。

这种方式虽然从根本上杜绝了失效的可能,但是也有其不足之处:

  • 就算缓存不过期,也会因数据变化而进行缓存重建,缓存重构期间,可能会产生数据不一致的问题。

4.3.多级缓存

参考:章节2.5.多级缓存

关注点:将热点Key存放在一级缓存。

5.缓存击穿

缓存击穿:大量请求查询一个热点Key,由于一个Key在分布式缓存中的节点是固定的,所以这个节点短时间内承受极大压力,可能会挂掉,引起整个缓存集群的挂掉,导致大量请求直接访问数据库,给数据库造成极大压力,甚至宕机,严重时引起整个系统的崩溃。

**举例:**现实生活中发生的一些重大新闻,会导致大量用户访问微博,导致微博直接挂掉。这些新闻可能就是缓存中的几条数据。

解决办法:

  1. 多读多写
  2. 多级缓存

5.1.多读多写

多读多写:关键在于把全部流向一个缓存节点的压力进行分担。

实施简述:

  • 确定存在一个key为热点key。
  • 分布式缓存的节点数为N。
  • 通过某种算法将这个key转换成一组key:key1,key2…keyN,并且确保这些keyi分表落到不同的缓存node上。
  • 当请求访问这个key时,通过轮训或者随机的方式,访问keyi即可获取value值。

缺点

  • 需要提供合适的算法保证拆分后的key落在不同的缓存节点上。
  • 如果缓存节点数量发生了变化,原有算法是否继续可用?
  • 如果缓存内容发送变化,如何保证所有keyi的强一致性?
  • 整体来说,这个方案过重

5.2.多级缓存

参考:章节2.5.多级缓存

关注点:由于服务节点存在多个,本地缓存能够做到分布式缓存不易做到的事情:通过负载均衡,分散热点key的压力。

Redis: 缓存过期、缓存雪崩、缓存穿透、缓存击穿(热点)、缓存并发(热点)、多级缓存、布隆过滤器的更多相关文章

  1. Java高并发--CPU多级缓存与Java内存模型

    Java高并发--CPU多级缓存与Java内存模型 主要是学习慕课网实战视频<Java并发编程入门与高并发面试>的笔记 CPU多级缓存 为什么需要CPU缓存:CPU的频率太快,以至于主存跟 ...

  2. 8.了解什么是 redis 的雪崩、穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透?

    作者:中华石杉 面试题 了解什么是 redis 的雪崩.穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透? 面试官心理分析 其实这是问到缓存必问的,因为缓 ...

  3. Redis 雪崩、穿透和击穿

    https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-caching-avalanche-and ...

  4. redis缓存雪崩、穿透、击穿概念及解决办法

    缓存雪崩 对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机.缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据 ...

  5. Redis 缓存雪崩、穿透、击穿

    缓存雪崩 定义: 同一时间所有 key 大面积失效,比如网站首页的数据基本上都是同一批次去缓存的. 解决方法: ① 存的时候设定随机的失效时间. ② 服务做熔断处理(异常或着慢查询 Hystrix 限 ...

  6. Redis 雪崩、穿透、击穿、并发、缓存讲解以及解决方案

    1.缓存雪崩 数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机. 比如一个雪崩的简单过程 1.redis集群大面积故障 2.缓存 ...

  7. 什么是 redis 的雪崩、穿透和击穿?

    缓存雪崩 对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机.缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据 ...

  8. 有赞透明多级缓存解决方案(TMC)设计思路

    引子 TMC 是什么 TMC,即"透明多级缓存(Transparent Multilevel Cache)",是有赞 PaaS 团队给公司内应用提供的整体缓存解决方案. TMC 在 ...

  9. Redis解读(4):Redis中HyperLongLog、布隆过滤器、限流、Geo、及Scan等进阶应用

    Redis中的HyperLogLog 一般我们评估一个网站的访问量,有几个主要的参数: pv,Page View,网页的浏览量 uv,User View,访问的用户 一般来说,pv 或者 uv 的统计 ...

  10. 细谈布隆过滤器及Redis实现

    ​ 何为布隆过滤器? 本质上是一种数据结构,是1970年由布隆提出的.它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数).可以用于检索一个元素是否在一个集合中. 数据结构: 布隆过 ...

随机推荐

  1. HAOI2010软件安装

    首先tarjan缩点应该能看出来,然后我用topsort跑了个DAG上的一维dp,结果WA的很惨. 其实用DAG应该也能做,但是DAG强调整体顺序,而对一些局部问题,例如两个儿子怎么分配,是否给当前节 ...

  2. git一键push至github脚本

    ######################################################################### # File Name: push.sh # Aut ...

  3. centos 6和centos7关闭防火墙的方法

    centos 6 关闭命令:  service iptables stop 永久关闭防火墙:chkconfig iptables off 两个命令同时运行,运行完成后查看防火墙关闭状态         ...

  4. fmri格式相关简介————转自网络

    转自莫毕业 目前,脑成像数据主要有DTI.fmri.3D三种模态.这些数据在分析前都要进行格式转换,不同公司的扫描仪存储格式也不尽相同.脑成像处理软件也很多,不同软件使用的格式也不一样,所以数据转换是 ...

  5. 按下home键,重新打开,应用重启

    其实不是重启,只是重新打开了luncher的那个activity.只要通过判断把它finish,就会显示按下home键前的页面. 解决方法: 在重启的页面中加入一下代码,注意加在setContentV ...

  6. 学习前端第二天心得体会(初步了解HTML5的部分API以及画布Canvas)

    一.HTML5部分API 1.选择器querySelector和querySelectorAll 1.1.querySelector:返回文档中匹配指定的CSS选择器的第一元素.  document. ...

  7. qt liunx 安装命令

    qt SDK : apt-get install qt-sdkqt 安装 : apt-get install qtcreator文档安装 : cmake kdelibs5-data subversio ...

  8. OpenStack 对接 Ceph 环境可以创建卷但不能挂载卷的问题

    问题 环境:Nova.Cinder.Glance 都对接了 Ceph RBD 后端存储. 以往的操作包括上传镜像.创建卷.挂载卷都是一切正常的,但突然出现了无法挂载卷的问题,而且还是因为 Ceph 客 ...

  9. java的几种上传文件方法

    这时:commonsmultipartresolver 的源码,可以研究一下 http://www.verysource.com/code/2337329_1/commonsmultipartreso ...

  10. androidstudio导入新项目build tools不符合问题解决

    问题描述:从网上或者其他地方拷贝来完整代码导入androidstudio的时候,gradle过程显示build tools不符合 问题分析:你安装的SDK版本可能与其他人不一样,那么build的工具也 ...