Redis是当前最流行的NoSQL数据库之一 ,其主要用途是作为缓存,以提高数据查询效率和保护
数据库,因此对于提高系统性能起到了至关重要的作用。当然,也常常会遇到一些异常情况,从而
导致 Redis 失去缓存作用。
常见的异常类型主要有:缓存雪崩、缓存穿透、缓存击穿。
Redis缓存异常问题分别是:1.缓存雪崩。2.缓存预热。3.缓存穿透。4.缓存降级。5.缓存击穿,以
及对应Redis缓存异常问题解决方案。
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。
举例来说, 我们在准备一项抢购的促销运营活动,活动期间将带来大量的商品信息、库存等相关信息的查询。
为了避免商品数据库的压力,将商品数据放入缓存中存储。不巧的是,抢购活动期间,大量的热门商品缓存同时失
效过期了,导致很大的查询流量落到了数据库之上,对于数据库来说造成很大的压力。
mutex互斥锁解决,Redis的SETNX去set一个mutex key,当操作返回成功时,再进行加载数据库的操作并回设缓存,否则,就重试整个get缓存的方法。
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key。
C1为原始缓存,C2为拷贝缓存,C1失效时,可以访问C2,C1缓存失效时间设置为短期,C2设置为长期。
实效性要求不高的缓存,容器启动初始化加载,采用定时任务更新或移除缓存。
让缓存失效的时间点尽量均匀。
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。
如图所示:
如果不进行预热, 那么 Redis 初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
数据量不大的时候,工程启动的时候进行加载缓存动作;
数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
数据量太大的时候,优先保证热点数据进行提前加载到缓存。
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到对应key的value,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库。
简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
优势:占用内存空间很小,位存储;性能特别高,使用key的hash判断key存不存在。
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
降级的情况,就是缓存失效或者缓存服务挂掉的情况下,我们也不去访问数据库。
我们直接访问内存部分数据缓存或者直接返回默认数据。
对于应用的首页,一般是访问量非常大的地方,首页里面往往包含了部分推荐商品的展示信息。这些推荐商品都会放到缓存中进行存储,同时我们为了避免缓存的异常情况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。
降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没
读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
会造成某一时刻数据库请求量过大,压力剧增。
这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。
如果是单机,可以用synchronized或者lock来处理;
如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。
从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。
该方法类似于方法一:
使用countDownLatch和atomicInteger.compareAndSet()方法,实现轻量级锁。
?public 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"));
? ? }
??
?}
??
?public 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);
? ? ? ? }
? ? }
?}