对于缓存穿透,雪崩相信不少小伙伴都有听过,无论是工做中仍是面试都热点问题,本文重点带你们分析这些问题,给位看官请往下看!java
同时用XMind画了一张导图记录Redis的学习笔记和一些面试解析(源文件对部分节点有详细备注和参考资料,欢迎关注个人公众号:阿风的架构笔记 后台发送【导图】拿下载连接, 已经完善更新): 面试
为了缓解持久层数据库的压力,在服务器和存储层之间添加了一层缓存;redis
一个简单的正常请求: 当客户端发起请求时,服务器响应处理,会先从redis缓存层查询客户端须要的请求数据,若是缓存层有缓存的数据,会将数据返回给服务器,服务器再返回给客户端;若是缓存层中没有客户端须要的数据,则会去底层存储层查找,再返回给服务器;算法
缓存穿透就是: 当客户端想要查询一个数据,发现redis缓存层中没有(即缓存没有命中),因而向持久层数据库查询,发现也没有,因而本次查询失败;当用户不少的时候,缓存都没有命中,因而都去请求了持久层数据库,此时会给持久层数据库形成很大的压力,这时候就至关于出现了缓存穿透。数据库
在缓存层加布隆过滤器,通俗简述一下其做用:将数据库中的 id ,经过某方式映射到布隆过滤器,当处理不存在的 id 时,布隆过滤器会将该请求过直接过滤出去,不会到数据库作操做。数组
1)概述: 布隆过滤器是一种数据结构,比较巧妙的几率型数据结构,其实是一个很长的二进制向量和一系列随机映射函数,特色是高效地插入和查询,能够用来告诉你 “某样东西必定不存在或者可能存在”,相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,可是缺点是其返回的结果是几率性的,而不是确切的。缓存
2)返回结果的不确切性: 布隆过滤器是一个 bit 向量或者说 bit 数组:假设有8位服务器
映射数据1: 使用多个不一样的哈希函数生成多个哈希值,并对每一个生成的哈希值指向的 bit 位置为 1,好比三次hash完后,data1 将一、三、6位,置为1;markdown
映射数据2: data2 将二、三、6位,置为1,此时因为hash为随机性,因此6位和 data1 有重复的,便会覆盖 data1 的第6位的1;网络
问题来了!!
6 这个 bit 位因为两个值的哈希函数都返回了这个 bit 位,所以它被覆盖了,当咱们若是想查询 data3这个值是否存在,假设哈希函数返回了 一、五、6三个值,结果咱们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,所以咱们能够很肯定地说 data3 这个值不存在。而当咱们须要查询 data1 这个值是否存在的话,那么哈希函数必然会返回 一、三、6,而后咱们检查发现这三个 bit 位上的值均为 1,那么咱们是否能够说 data1 存在了么?答案是不能够,只能是 data1 这个值可能存在!由于随着增长的值愈来愈多,被置为 1 的 bit 位也会愈来愈多,这样某个值 data4 即便没有被存储过,可是万一哈希函数返回的三个 bit 位都被其余位置位了 1 ,那么程序仍是会判断 data4 这个值存在。
因此: 布隆过滤器的长度会直接影响误报率,布隆过滤器越长且误报率越小。
3)简单剖析布隆过滤器源码
导入guava的包:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
复制代码
源码: BloomFilter一共四个create方法,最终都是走向第四个方法;
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions) {
return create(funnel, (long) expectedInsertions);
}
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}
public static <T> BloomFilter<T> create( Funnel<? super T> funnel, long expectedInsertions, double fpp) {
return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
}
static <T> BloomFilter<T> create( Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
......
}
复制代码
参数类型: funnel:数据类型;expectedInsertions:指望插入的值的个数;fpp:错误率(默认值为0.03);strategy:哈希算法。
总结: 错误率越大,所需空间和时间越小;反之错误率越小,所需空间和时间越大!
在日常高并发的系统中,大量的请求同时查询一个key时,此时这个key正好失效了,就会致使大量的请求都打到数据库上面去。这种现象咱们称为缓存击穿
这种解决方案思路比较简单,就是只让一个线程构建缓存,其余线程等待构建缓存的线程执行完,从新从缓存获取数据就能够了。若是是单机,能够用synchronized或者lock来处理,若是是分布式环境能够用分布式锁就能够了(分布式锁,能够用memcache的add, redis的setnx, zookeeper的添加节点操做)。
在value内部设置1个超时值(timeout1), timeout1比实际的redis timeout(timeout2)小。当从cache读取到timeout1发现它已通过期时候,立刻延长timeout1并从新设置到cache。而后再从数据库加载数据并设置到cache中
从redis上看,确实没有设置过时时间,这就保证了,不会出现热点key过时问题,也就是“物理”不过时。
从功能上看,若是不过时,那不就成静态的了吗?因此咱们把过时时间存在key对应的value里,若是发现要过时了,经过一个后台的异步线程进行缓存的构建,也就是“逻辑”过时
class MyCache{
private ConcurrentHashMap<String, String> map;
private CountDownLatch countDownLatch;
private AtomicInteger atomicInteger;
public MyCache(ConcurrentHashMap<String, String> map, CountDownLatch countDownLatch, AtomicInteger atomicInteger) {
this.map = map;
this.countDownLatch = countDownLatch;
this.atomicInteger = atomicInteger;
}
public String get(String key){
String value = map.get(key);
if (value != null){
System.out.println(Thread.currentThread().getName()+"\t 线程获取value值 value="+value);
return value;
}
// 若是没获取到值
// 首先尝试获取token,而后去查询db,初始化化缓存;
// 若是没有获取到token,超时等待
if (atomicInteger.compareAndSet(0,1)){
System.out.println(Thread.currentThread().getName()+"\t 线程获取token");
return null;
}
// 其余线程超时等待
try {
System.out.println(Thread.currentThread().getName()+"\t 线程没有获取token,等待中。。。");
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 初始化缓存成功,等待线程被唤醒
// 等待线程等待超时,自动唤醒
System.out.println(Thread.currentThread().getName()+"\t 线程被唤醒,获取value ="+map.get("key"));
return map.get(key);
}
public void put(String key, String value){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
// 更新状态
atomicInteger.compareAndSet(1, 2);
// 通知其余线程
countDownLatch.countDown();
System.out.println();
System.out.println(Thread.currentThread().getName()+"\t 线程初始化缓存成功!value ="+map.get("key"));
}
}
class MyThread implements Runnable{
private MyCache myCache;
public MyThread(MyCache myCache) {
this.myCache = myCache;
}
@Override
public void run() {
String value = myCache.get("key");
if (value == null){
myCache.put("key","value");
}
}
}
public class CountDownLatchDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache(new ConcurrentHashMap<>(), new CountDownLatch(1), new AtomicInteger(0));
MyThread myThread = new MyThread(myCache);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.execute(myThread);
}
}
}
复制代码
缓存击穿就是单个高热数据过时的瞬间,数据访问量较大,未命中redis后,发起了大量对同一数据的数据库访问,致使对数据库服务器形成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个key的过时监控难度较高,配合雪崩处理策略便可。
缓存雪崩是指: 某一时间段,缓存集中过时失效,即缓存层出现了错误,不能正常工做了;因而全部的请求都会达到存储层,存储层的调用量会暴增,形成 “雪崩”;
**好比:**双十二临近12点,抢购商品,此时会设置商品在缓存区,设置过时时间为1小时,当到了1点时,缓存过时,全部的请求会落到存储层,此时数据库可能扛不住压力,天然 “挂掉”。
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉以后其余的还能够继续工做,其实就是搭建的集群。
这个解决方案的思想是,在缓存失效后,经过加锁或者队列来控制读数据库写缓存的线程数量。好比对某个key只容许一个线程查询数据和写缓存,其余线程等待。
数据预热的含义就是在正式部署以前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不一样的key,设置不一样的过时时间,让缓存失效的时间点尽可能均匀。
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就能够避免在用户请求的时候,先查询数据库,而后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。如图所示:
若是不进行预热, 那么 Redis 初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库形成流量的压力。
前置准备工做:
平常例行统计数据访问记录,统计访问频度较高的热点数据
利用LRU数据删除策略,构建数据留存队列
例如:storm与kafka配合
复制代码
准备工做:
实施:
缓存预热就是系统启动前,提早将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,而后再将数据缓存的问题!用户直接查询事先被预热的缓存数据
降级的状况,就是缓存失效或者缓存服务挂掉的状况下,咱们也不去访问数据库。咱们直接访问内存部分数据缓存或者直接返回默认数据。
举例来讲:
对于应用的首页,通常是访问量很是大的地方,首页里面每每包含了部分推荐商品的展现信息。这些推荐商品都会放到缓存中进行存储,同时咱们为了不缓存的异常状况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。以下图所示:
降级通常是有损的操做,因此尽可能减小降级对于业务的影响程度。
若是你以为这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙: