缓存雪崩
原因:
大量的缓存在一瞬间失效,导致了大量的查询瞬间落到了数据库上,会有可能导致数据库宕机,
解决方案:
1、加锁排队
使用mutex互斥锁,redis的setnx去set一个mutex key,当操作返回成功时,再进行加载数据库的数据并设置缓存,否则,就重试整个get方法。
2、数据预热
系统上线之后,将相关的缓存数据直接加载到缓存系统。这样可以避免用户请求数据的时候直接请求数据库,然后再将数据缓存的问题。
我们通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动出发加载缓存不同的key。
3、双层缓存策略
C1为原始缓存,C2为拷贝缓存。C1失效时,去访问C2。C1缓存失效时间设置为短期,C2设置为长期
4、定时更新缓存策略
实效性要求不高的缓存,容器启动初始化加载,采用定时任务更新或移除缓存
5、设置不同的过期时间,让缓存时间点尽量均匀
缓存击穿
原因:
用户要读取的数据,缓存中没有(热点key在失效的瞬间),同时并发用户特别多的时候,数据库的压力会瞬间倍增
解决方案:
1、使用mutex互斥锁
让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据。
单机环境:用synchronized或者lock处理
分布式环境:使用分布式锁,memcache的add,redis的setnx,zookeeper的添加节点
2、设置key永不过期
3、缓存屏障
使用countDownLatch和atomicInteger.compareAndSet()方法实现轻量级锁
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);
}
}
}
缓存穿透
原因:
用户要读取的数据,缓存中没有,同时并发用户特别多的时候,数据库的压力会瞬间倍增
解决方案:
1、缓存空对象
在一条查询返回的数据为空的时候,我们把这个空数据进行缓存,缓存时间一般不超过五分钟
2、布隆过滤器
占用内存空间很小,位存储;性能特别高,使用key的hash判断key存不存在 将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
缓存降级
原因:
就是缓存失效的时候,或者缓存服务挂了,我们不走数据库,直接去访问内存部分的数据缓存或者默认数据。
例如首页中的数据,在走了缓存降级策略之后,可能会对业务有一定的影响。这是有损的操作,尽可能的不使用缓存降级。
缓存预热
如果不对数据进行预热,那么redis的初始状态为空,系统上线初期对于高并发的流量都会访问到数据库,对数据库造成最直接的压力。
预热方案
1、数据量不大的时候,工程启动的时候进行加载缓存动作
2、数据量大的时候,设置一个定时任务(或者脚本),进行缓存的刷新
3、数据量太大的时候,优先保证热点数据进行提前加载到缓存