简介
以下是我根据网上找的文章加以自己的理解和侧重点整理归纳的
缓存击穿
指的是客户的请求在缓存中没查到,数据库中存在。一般情况是一些热点key失效时间到期,在瞬间的并发量很大,所以对数据库造成的压力很大。
解决方案
- 永不失效:这里有两重含义,第一重含义就是字面上的意思,不设置失效时间,自然不会出现这种问题。第二重含义是指在为一个key设置缓存时间之后,启动另一个线程去监听当其快要到达失效时间的时候,重新延长失效时间。从而实现永不过期的效果。其中官方推荐的更加高效的redis工具:redisson,已经实现了自动续期的功能。
- 互斥锁:互斥锁的逻辑是在请求发现缓存失效的时候加锁来保证只有一个线程可以去查询数据库,查询数据库之后在缓存起来,供给其他线程进行查询。互斥锁的方案简单有效,但是如果中间获取处理的数据的逻辑复杂,运行时间长,那很有可能造成死锁和阻塞。
- 简略写法:
public String get(String key) {
// 从Redis中获取数据
String value = redis.get(key);
// 如果value为空,则开始重构缓存
if(value == null) {
// 只允许一个线程重构缓存,使用nx,并设置过期时间ex
String mutexKey = "mutext:key:" + key;
if(redis.set(mutexKey, value, "NX", "EX", seconds)) {
// 从数据源获取数据
value = db.get(key);
// 回写Redis,并设置过期时间
redis.setex(key, timeout, value);
// 删除key_mutex
redis.delete(mutex_key);
}
// 其他线程休息50毫秒后重试
else {
Thread.sleep(50);
get(key);
}
}
return value;
}
- 单元测试示例:
@Test
public void testLock() {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(null, null, "线程" + i) {
public void run() {
String key = "test";
String value = redisUtil.get(key);
if (!Strings.isBlank(value)) {
System.out.println(Thread.currentThread().getName() + "查到缓存数据,value" + value);
return;
}
System.out.println(Thread.currentThread().getName() + "查询不到缓存数据");
try {
//加锁
if (redisUtil.lock("lock", "1", 10, "")) {
System.out.println(Thread.currentThread().getName() + "查询数据库");
//查询数据库
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "存入缓存");
redisUtil.set(key, "100");
redisUtil.expire(key, 10);
//解锁
redisUtil.unlock("lock", "1");
} else {
System.out.println(Thread.currentThread().getName() + "进入等待");
Thread.sleep(2000);
this.run();
}
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行完毕");
}
- 输出结果
线程4查询不到缓存数据
线程1查询不到缓存数据
线程0查询不到缓存数据
线程2查询不到缓存数据
线程3查询不到缓存数据
线程0进入等待
线程2进入等待
线程1进入等待
线程3进入等待
线程4查询数据库
线程4存入缓存
线程2查到缓存数据,value100
线程3查到缓存数据,value100
线程0查到缓存数据,value100
线程1查到缓存数据,value100
线程执行完毕
- 说明 从中可以发现,5个线程同时请求,只有线程4请求到了数据库,并从中获取值,反写redis,其他线程都进入等待,等待线程4反写缓存后,从缓存中获取了值。(特殊说明:上述单元测试需要用到CountDownLatch,因为如果不用,单元测试的时候主线程结束后,子线程也就是结束了。程序可能直接中断掉,没法再唤醒了。)
缓存穿透
指的是访问的请求在缓存中没查到,在数据库中也不存在。简单的说一直请求系统中不存在的数据,就是缓存穿透。而因为缓存中不存在数据,所有假设大量的这种请求访问过来,会给数据库造成巨大压力。
解决方案
- 在缓存中缓存下来这个查询不到的数据,直接缓存数据为null.这种方法可以有效防御缓存穿透,但是会存在数据库中的数据和缓存数据不一致的问题,假设访问的数据原来不存在,但是过了一段时间后又新增存在的了,那这种时候如果缓存未过期,会有一段时间查询返回的还是null。
- 使用布隆过滤器,布隆过滤器采用将所有的数据通过hash存储到一个位数组上,当请求经过布隆过滤器的时候,会经过hash判断是否存在这个位数组上,如果布隆过滤器返回不存在,则说明真的不存在,直接返回null。如果布隆过滤器返回存在。此时再走原来的逻辑,先查缓存,缓存没有再查数据库。
- 1和2的区别:第一种方案虽然方便,但是如果访问的请求中的key是不同的,则还是把压力给到了数据库。而且存储大量的value为null的没用的数据在缓存中,也会对缓存造成压力。所以针对那种key是重复的,此方案比较适合。第二种方案,实现比较复杂,维护成本高。但是适合针对大量不同key的恶意请求场景。两种各有优劣,具体看使用场景适合哪一种方案。
缓存雪崩
从上述可知,缓存的的作用其中很重要的一点是保护了数据库。如果缓存因为某些情况导致无法起作用,那么大量请求冲击数据库就是缓存雪崩 缓存雪崩的原因一般是两点:
- 缓存自身失效,比如机器宕机,无法使用
- 缓存数据失效,比如有一批热点数据同时失效,瞬间大量请求到来,直接请求数据库
解决方案
- 针对缓存宕机的情况,可以采用部署集群的方式,既然单台机器容易出故障宕机,那么多部署几台即可。将缓存层设计成高可用的可以有效避免这种情况。
- 针对缓存因为一批热点数据同时失效的情况,一般是因为失效时间在同一个时间的原因,那么可以通过错开失效时间避免大量缓存同时失效的情况。可以在设置失效时间的时候加上随机数来达到缓存失效时间不相同的目的。
- 使用Hystrix,通过其提供的熔断、降级、限流的功能来达到处理缓存雪崩的目的。