Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。本文将简单介绍缓存穿透、缓存雪崩和缓存击穿这三者之间的区别以及这三类问题的解决方法。
缓存穿透
缓存穿透指的是访问redis中一个不存在的key的时候,导致缓存无法命中,每次请求都要穿透到数据库中进行查询,导使数据库压力过大,甚至挂掉。
解决方案:
1)布隆过滤器拦截。有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。
2)缓存空对象。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空,仍然把这个空结果进行缓存,这意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除,一般最长不超过五分钟。
缓存击穿(缓存并发)
缓存击穿,是指当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案:
1)设置热点数据永不过期。
“永远不过期”包含两层意思:从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
2)加互斥锁。
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
本地锁:与分布式锁类似,我们通过本地锁的方式来限制只有一个线程去数据库中查询数据,而其他线程只需等待,等前面的线程查询到数据后再访问缓存。但是,这种方法只能限制一个服务节点只有一个线程去数据库中查询,如果一个服务有多个节点,则还会有多个数据库查询操作,也就是说在节点数量较多的情况下并没有完全解决缓存并发的问题。
public static final ReentrantLock reenLock = new ReentrantLock(); public static String getData(String key) throws InterruptedException { String result = getDataFromRedis(key); if(result == null) { if(reenLock.tryLock()) { result = getDataFromMysql(key); if(result != null) { setDataToCache(key, result); } reenLock.unlock(); }else { Thread.sleep(100); result = getData(key); } } return result; }
缓存雪崩
缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致所有的查询同一时刻都落在了数据库上,造成数据库压力过大。
解决方案:
1)不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
2)在缓存失效后,通过加分布式锁或者分布式队列的方式来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
3)做一些预案设定,比如二级缓存,或者叫双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
本文已通过「原本」原创作品认证,转载请注明文章出处及链接。