您的位置:首页 > 数据库 > Redis

Redis缓存相关问题(雪崩、穿透、击穿、并发)

2018-12-20 10:38 911 查看

一、缓存雪崩(缓存失效)

概念:

  数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。

解决思路:

  1.缓存的高可用性:
    缓存层设计成高可用,防止缓存大面积故障。即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。
    参考方法:
      ⑴在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
      ⑵在即将发生大并发访问前手动触发更新缓存。
      ⑶不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
      ⑷做二级缓存,或者双缓存策略。

  2.缓存降级:
    可以考虑利用ehcache等本地缓存,但主要还是对源服务访问进行限流、资源隔离(熔断)、降级等。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
    降级的最终目的是保证核心服务可用,即使是有损的。
    在进行降级之前要对系统进行梳理,比如:哪些业务是核心业务(必须保证),哪些业务可以容许暂时不提供服务(比如利用静态页面替换)等。同时可以配合服务器核心指标,来设置整体预案,比如:
      ⑴一般:某些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级。
      ⑵警告:某些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警。
      ⑶错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值之上,此时可以根据情况自动降级或者人工降级。
      ⑷严重错误:比如因为特殊原因数据错误了,或者其他严重错误,此时需要紧急人工降级。

  3.Redis备份和缓存预热:
    ⑴Redis数据备份和恢复。
    ⑵缓存预热。
      缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。
      这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存。用户直接查询的是事先被预热的缓存数据。
      实现方法:
        ①直接写个缓存刷新页面,上线前手工访问。
        ②在项目启动的时候自动进行加载。

  4.提前演练:
    最后,建议还是在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,提前发现问题。

二、缓存穿透

概念:

  查询一个不存在的数据。
  在缓存redis没有命中,则需要从数据库查询。这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决思路:

  1.设置默认返回值:
    如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。设置一个过期时间或者当有值的时候将缓存中的值替换掉即可。

  2.查询前过滤:
    可以给key设置一些格式规则,然后查询之前先过滤掉不符合规则的Key。
    将数据库中所有的查询条件,放到布隆过滤器中。当一个查询请求来临的时候,先经过布隆过滤器进行检查,如果请求存在这个条件中,那么继续执行,如果不在,直接丢弃。
      ⑴布隆过滤器的容量size设置要比数据库总条件数稍微大一些。
      ⑵对于误判率的设置,根据实际项目,以及硬件设施来具体决定。不能设置为0。误判率设置的越小,硬件要求相应越高。

三、缓存击穿

概念:

  一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到数据库,造成瞬时数据库请求量大、压力骤增。
  和缓存雪崩的区别在于这里针对某一个key缓存,缓存雪崩则是针对很多key。

解决思路:

  1.使用互斥锁(mutex key):(最常用)
    在缓存失效的时候(判断拿出来的值为空),不是立即去加载数据库,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再加载数据库并回设缓存;否则,就重试整个get缓存的方法。
    SETNX 是【SET if Not eXists】的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
实现伪代码:

public String get(String key) {
String value = redis.get(key);
// 说明缓存值过期
if (value == null) {
// 设置3min的超时,防止del操作失败的时候,再次访问无法加载数据库
// 代表设置成功
if (redis.setnx(key_mutex, value_mutex, 3 * 60)) {
db_value = db.get(key);
redis.set(key, db_value, expire_secs);
redis.del(key_mutex);
} else {
// 到此代表同一时间其他线程已经加载数据库并回设到缓存了,此时重试获取缓存值即可
sleep(50);
// 重试
get(key);
}
} else {
return value;
}
}

  2.永不过期:
    这里的“永不过期”包含两层意思:
      ⑴从redis上看,不设置过期时间,就保证了,不会出现热点key过期问题,也就是“物理”不过期。
      ⑵把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。
实现伪代码:

String get(String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (timeout <= System.currentTimeMillis()) {
// 异步更新后台异常执行
threadPool.execute(new Runnable() {
public void run() {
if (redis.setnx(key_mutex, value_mutex)) {
// 设置3min的超时,防止del操作失败的时候,再次访问无法加载数据库
db_value = db.get(key);
redis.set(key, db_value, expire_secs);
redis.delete(key_mutex);
}
}
});
}
return value;
}

注意:
  此处只是提供思想,具体代码实现还需要考虑时间同步、加锁解锁角色统一等问题。
  由于key可能随着时间的变化而变化,针对固定的数据进行特殊缓存是不能起到治本作用的,结合LRU算法能够较好的解决这个问题。此外还要LRU-K、Two Queues和Mutil Queues等等可对比拓展。

四、缓存并发

概念:

  这里的并发指的是多个redis的client同时set key引起的并发问题。
  其实redis自身就是单线程操作,多个client并发操作,按照先到先执行的原则,先到的先执行,其余的阻塞,并不存在竞争关系。但是利用jedis等客户端对Redis进行并发访问时会出现问题。

解决思路:

  1.分布式锁+时间戳:
    通过加锁把并行读写改成串行读写,避免资源竞争。通过时间戳保证操作的顺序执行。

  2.消息队列:
    在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写串行化。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: