redis内存溢出

Redis缓存雪崩、缓存穿透、热点Key解决方案和分析

缓存穿透

缓存系统,按照KEY去查询VALUE,当KEY对应的VALUE一定不存在的时候并对KEY并发请求量很大的时候,就会对后端造成很大的压力。

(查询一个必然不存在的数据。比如文章表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。)

由于缓存不命中,每次都要查询持久层。从而失去缓存的意义。

 

解决方法:

1、缓存层缓存空值。 
–缓存太多空值,占用更多空间。(优化:给个空值过期时间) 
–存储层更新代码了,缓存层还是空值。(优化:后台设置时主动删除空值,并缓存把值进去)

2、将数据库中所有的查询条件,放到布隆过滤器中。当一个查询请求来临的时候,先经过布隆过滤器进行检查,如果请求存在这个条件中,那么继续执行,如果不在,直接丢弃。

 

备注:

    比如数据库中有10000个条件,那么布隆过滤器的容量size设置的要稍微比10000大一些,比如12000.

    对于误判率的设置,根据实际项目,以及硬件设施来具体决定。但是一定不能设置为0,并且误判率设置的越小,哈希函数跟数组长度都会更多跟更长,那么对硬件,内存中间的要求就会相应的高。

  private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.0001); 

    有了size跟误判率,那么布隆过滤器就会产生相应的哈希函数跟数组。

    综上:我们可以利用布隆过滤器,将redis缓存击穿控制在一个可容忍的范围内。

 


缓存雪崩(缓存失效)

        如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。

        缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储

    解决方法:

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  2. 可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
  3. 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
  4. 做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

 

 

热点key

      (1) 这个key是一个热点key(例如一个重要的新闻,一个热门的八卦新闻等等),所以这种key访问量可能非常大。

      (2) 缓存的构建是需要一定时间的。(可能是一个复杂计算,例如复杂的sql、多次IO、多个依赖(各种接口)等等)

       于是就会出现一个致命问题:在缓存失效的瞬间,有大量线程来构建缓存(见下图),造成后端负载加大,甚至可能会让系统崩溃 。

    解决方法:

1. 使用互斥锁(mutex key):这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了

2. "提前"使用互斥锁(mutex key):在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。

3. "永远不过期":

 这里的“永远不过期”包含两层意思:

    (1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

    (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

4. 资源保护:可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。

四种方案对比:

      作为一个并发量较大的互联网应用,我们的目标有3个:

      1. 加快用户访问速度,提高用户体验。

      2. 降低后端负载,保证系统平稳。

      3. 保证数据“尽可能”及时更新(要不要完全一致,取决于业务,而不是技术。)

      所以第二节中提到的四种方法,可以做如下比较,还是那就话:没有最好,只有最合适。 

解决方案优点缺点
简单分布式锁(Tim yang)

 1. 思路简单

2. 保证一致性

1. 代码复杂度增大

2. 存在死锁的风险

3. 存在线程池阻塞的风险

加另外一个过期时间(Tim yang) 1. 保证一致性同上 
不过期(本文)

1. 异步构建缓存,不会阻塞线程池

1. 不保证一致性。

2. 代码复杂度增大(每个value都要维护一个timekey)。

3. 占用一定的内存空间(每个value都要维护一个timekey)。

资源隔离组件hystrix(本文)

1. hystrix技术成熟,有效保证后端。

2. hystrix监控强大。

 

 

1. 部分访问存在降级策略。 


总结

 

   1.  热点key + 过期时间 + 复杂的构建缓存过程 => mutex key问题

   2. 构建缓存一个线程做就可以了。

   3. 四种解决方案:没有最佳只有最合适。

分析和相关解决

 

 

什么是缓存穿透

    一般的缓存系统,都是按照key值去缓存查询,如果不存在对应的value,就应该去DB中查找 。这个时候,如果请求的并发量很大,就会对后端的DB系统造成很大的压力。这就叫做缓存穿透。关键词:缓存value为空;并发量很大去访问DB。

 

 

造成的原因

1.业务自身代码或数据出现问题;2.一些恶意攻击、爬虫造成大量空的命中,此时会对数据库造成很大压力。

解决方法

1.设置布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,
从避免了对底层存储系统的查询压力。
2. 如果一个查询返回的数据为空,不管是数据不存在还是系统故障,我们仍然把这个结果进行缓存,但是它的过期时间会很短
最长不超过5分钟。


二、雪崩

1.什么是雪崩

因为缓存层承载了大量的请求,有效的保护了存储 层,但是如果缓存由于某些原因,整体不能够提供服务,于是所有的请求,就会到达存储层,存储层的调用量就会暴增,造成存储层也会挂掉的情况。缓存雪崩的英文解释是奔逃的野牛,指的是缓存层当掉之后,并发流量会像奔腾的野牛一样,大量后端存储。

存在这种问题的一个场景是:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,大量数据会去直接访问DB,此时给DB很大的压力。

 

 

2.解决方法

(1)设置redis集群和DB集群的高可用,如果redis出现宕机情况,可以立即由别的机器顶替上来。这样可以防止一部分的风险。

(2)使用互斥锁

在缓存失效后,通过加锁或者队列来控制读和写数据库的线程数量。比如:对某个key只允许一个线程查询数据和写缓存,其他线程等待。单机的话,可以使用synchronized或者lock来解决,如果是分布式环境,可以是用redis的setnx命令来解决。

(3)不同的key,可以设置不同的过期时间,让缓存失效的时间点不一致,尽量达到平均分布。

(4)永远不过期

redis中设置永久不过期,这样就保证了,不会出现热点问题,也就是物理上不过期。

(5)资源保护

使用netflix的hystrix,可以做各种资源的线程池隔离,从而保护主线程池。

3.使用

四种方案,没有最佳只有最合适, 根据自己项目情况使用不同的解决策略。
 

懂的越多,不会的也就越多,知识之路是不断进取的

把redis作为缓存使用已经是司空见惯,但是使用redis后也可能会碰到一系列的问题,尤其是数据量很大的时候,经典的几个问题如下:

(一)缓存和数据库间数据一致性问题
分布式环境下(单机就不用说了)非常容易出现缓存和数据库间的数据一致性问题,针对这一点的话,只能说,如果你的项目对缓存的要求是强一致性的,那么请不要使用缓存。我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括 合适的缓存更新策略,更新数据库后要及时更新缓存、缓存失败时增加重试机制,例如MQ模式的消息队列。

(二)缓存击穿问题
缓存击穿表示恶意用户模拟请求很多缓存中不存在的数据,由于缓存中都没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。这个我们在实际项目就遇到了,有些抢购活动、秒杀活动的接口API被大量的恶意用户刷,导致短时间内数据库c超时了,好在数据库是读写分离,同时也有进行接口限流,hold住了。

解决方案的话:

方案1、使用互斥锁排队

业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁,单机的话用普通的锁(synchronized、Lock)就够了。

复制代码

 1 public String getWithLock(String key, Jedis jedis, String lockKey, String uniqueId, long expireTime) {
 2     // 通过key获取value
 3     String value = redisService.get(key);
 4     if (StringUtil.isEmpty(value)) {
 5         // 分布式锁,详细可以参考https://blog.csdn.net/fanrenxiang/article/details/79803037
 6         //封装的tryDistributedLock包括setnx和expire两个功能,在低版本的redis中不支持
 7         try {
 8             boolean locked = redisService.tryDistributedLock(jedis, lockKey, uniqueId, expireTime);
 9             if (locked) {
10                 value = userService.getById(key);
11                 redisService.set(key, value);
12                 redisService.del(lockKey);
13                 return value;
14             } else {
15                 // 其它线程进来了没获取到锁便等待50ms后重试
16                 Thread.sleep(50);
17                 getWithLock(key, jedis, lockKey, uniqueId, expireTime);
18             }
19         } catch (Exception e) {
20             log.error("getWithLock exception=" + e);
21             return value;
22         } finally {
23             redisService.releaseDistributedLock(jedis, lockKey, uniqueId);
24         }
25     }
26     return value;
27 }

复制代码

 

这样做思路比较清晰,也从一定程度上减轻数据库压力,但是锁机制使得逻辑的复杂度增加,吞吐量也降低了,有点治标不治本。

方案2、接口限流与熔断、降级

重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制。

方案3、布隆过滤器

bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,下面先来简单的实现下看看效果,我这里用guava实现的布隆过滤器:

复制代码

 1 <dependencies> 
 2 <dependency> 
 3 <groupId>com.google.guava</groupId> 
 4 <artifactId>guava</artifactId> 
 5 <version>23.0</version> 
 6 </dependency> 
 7 </dependencies> 
 8 public class BloomFilterTest {
 9 
10 private static final int capacity = 1000000;
11 private static final int key = 999998;
12 
13 private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
14 
15 static {
16 for (int i = 0; i < capacity; i++) {
17 bloomFilter.put(i);
18 }
19 }
20 
21 public static void main(String[] args) {
22 /*返回计算机最精确的时间,单位微妙*/
23 long start = System.nanoTime();
24 
25 if (bloomFilter.mightContain(key)) {
26 System.out.println("成功过滤到" + key);
27 }
28 long end = System.nanoTime();
29 System.out.println("布隆过滤器消耗时间:" + (end - start));
30 int sum = 0;
31 for (int i = capacity + 20000; i < capacity + 30000; i++) {
32 if (bloomFilter.mightContain(i)) {
33 sum = sum + 1;
34 }
35 }
36 System.out.println("错判率为:" + sum);
37 }
38 }

复制代码

成功过滤到999998
布隆过滤器消耗时间:215518
错判率为:318
可以看到,100w个数据中只消耗了约0.2毫秒就匹配到了key,速度足够快。然后模拟了1w个不存在于布隆过滤器中的key,匹配错误率为318/10000,也就是说,出错率大概为3%,跟踪下BloomFilter的源码发现默认的容错率就是0.03:

1 public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) {
2 return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
3 }

 

我们可调用BloomFilter的这个方法显式的指定误判率:

 

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);
我们断点跟踪下,误判率为0.02和默认的0.03时候的区别:

 

 

对比两个出错率可以发现,误判率为0.02时数组大小为8142363,0.03时为7298440,误判率降低了0.01,BloomFilter维护的数组大小也减少了843923,可见BloomFilter默认的误判率0.03是设计者权衡系统性能后得出的值。要注意的是,布隆过滤器不支持删除操作。用在这边解决缓存穿透问题就是:

复制代码

 1 public String getByKey(String key) {
 2 // 通过key获取value
 3 String value = redisService.get(key);
 4 if (StringUtil.isEmpty(value)) {
 5 if (bloomFilter.mightContain(key)) {
 6 value = userService.getById(key);
 7 redisService.set(key, value);
 8 return value;
 9 } else {
10 return null;
11 }
12 }
13 return value;
14 }

复制代码

 


(三)缓存雪崩问题
缓存在同一时间内大量键过期(失效),接着来的一大波请求瞬间都落在了数据库中导致连接异常。

解决方案:

方案1、也是像解决缓存穿透一样加锁排队,实现同上;

方案2、建立备份缓存,缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有读B,并且更新A缓存和B缓存;

方案3、设置缓存超时时间的时候加上一个随机的时间长度,比如这个缓存key的超时时间是固定的5分钟加上随机的2分钟,酱紫可从一定程度上避免雪崩问题;

复制代码

 1 public String getByKey(String keyA,String keyB) {
 2 String value = redisService.get(keyA);
 3 if (StringUtil.isEmpty(value)) {
 4 value = redisService.get(keyB);
 5 String newValue = getFromDbById();
 6 redisService.set(keyA,newValue,31, TimeUnit.DAYS);
 7 redisService.set(keyB,newValue);
 8 }
 9 return value;
10 }

复制代码

 

(四)缓存并发问题
这里的并发指的是多个redis的client同时set key引起的并发问题。其实redis自身就是单线程操作,多个client并发操作,按照先到先执行的原则,先到的先执行,其余的阻塞。当然,另外的解决方案是把redis.set操作放在队列中使其串行化,必须的一个一个执行,具体的代码就不上了,当然加锁也是可以的,至于为什么不用redis中的事务,留给各位看官自己思考探究。

一、缓存

 


Redis做缓存是最常见的应用场景。客户端请求在缓存层命中就直接返回,如果miss就去读取存储层,存储层读取到就写入缓存层,然后再返回到客户端。
优点:
加速读写
降低后端负载
缺点:
数据的不一致性
代码维护成本
运维成本

二、缓存穿透优化

 


然而缓存可能会遇到这种问题:请求cache拿不到数据,就会去存储层拿,都拿不到时,返回空值(可能会返回大量空值)。或者代码有问题,拿不到数据。就会一直请求数据。导致后端打崩。

优化方法:
1、缓存层缓存空值。
–缓存太多空值,占用更多空间。(优化:给个空值过期时间)
–存储层更新代码了,缓存层还是空值。(优化:后台设置时主动删除空值,并缓存把值进去)

三、缓存雪崩优化

 


redis挂了,客户端直接请求到数据库里面。数据库负载非常高。甚至数据库拖挂了。

优化方法:
1、保持缓存层服务器的高可用。
–监控、集群、哨兵。当一个集群里面有一台服务器有问题,让哨兵踢出去。
2、依赖隔离组件为后端限流并降级。
比如推荐服务中,如果个性化推荐服务不可用,可以降级为热点数据。
3、提前演练。
演练 缓存层crash后,应用以及后端的负载情况以及可能出现的问题。
对此做一些预案设定。

四、热点key 重建优化:

 


A、B、C、D同时请求一个资源,不存在时都要去请求存储层,有可能会拖挂。

优化方法:
1、互斥锁:
只允许一个请求重建缓存。
其他请求等待缓存重建执行完,重新从缓存获取数据即可。

 


2、用户过期
–“物理”不过期
–逻辑设置过期时间(根据上一次更新时间,构建一个队列,主动去更新)

 

 

 

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis 是一个开源的键值存储数据库,它在内存中操作数据,以提供极快的读写速度。如果 Redis内存使用超过了其自身的物理内存限制,可能会导致严重的性能问题甚至系统崩溃。这种情况被称为"内存溢出"或"Out of Memory (OOM)"。 当 Redis 内存溢出时,可能发生的现象包括: 1. **缓存失效**:Redis 无法在内存中容纳新的数据,可能导致缓存中的数据被丢弃,影响服务的响应速度。 2. **客户端连接断开**:为了释放内存资源,Redis 可能会主动断开客户端的连接,或者在达到某些设置的限制时,拒绝新连接。 3. **性能下降**:随着内存占用越来越多,Redis 的处理能力会显著降低,查询和操作的响应时间延长。 4. **系统级警告**:操作系统通常会有相应的告警机制,显示内存使用过高,提醒管理员介入。 5. **强制重启**:在严重情况下,Redis 或者宿主机可能会因为内存耗尽而强制重启,导致数据丢失或服务中断。 为避免 Redis 内存溢出,你可以采取以下措施: - **定期监控内存使用**:使用 Redis 的内置监控工具或第三方工具监控 Redis内存使用情况。 - **配置合适的内存限制**:调整 Redis 的 maxmemory、maxmemory-policy 等参数,以适应实际需求和硬件资源。 - **设置合理的内存分解策略**:例如,使用 volatile-lru 或 volatile-ttl 来自动删除旧数据,减少内存占用。 - **使用持久化机制**:定期将内存中的数据写入磁盘,以便在重启时恢复数据。 - **合理扩展硬件**:增加物理内存,或者优化服务器资源分配。 如果你正在处理这样的问题,具体解决方法可能需要根据你的 Redis 配置和实际情况来定制。如果有进一步的问题,请告诉我。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值