缓存击穿问题
1、缓存击穿问题及其原因
背景:用户向后端查询数据时先查询缓存是否存在,如果存在直接获取,如果不存在就去找数据库,然而数据库的查找是慢的,多查询时性能不佳。
缓存击穿原因:黑客向后端发送大量缓存中不存在的数据,导致后端查询缓存不到,转而去查询数据库,大量查询堆积在数据库,数据库可能会挂掉。
2、解决方案:
1、使用互斥锁
该方法是比较普遍的做法,即,在根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕,释放锁。若其他线程发现获取锁失败,则睡眠50ms后重试。
至于锁的类型,单机环境用并发包的Lock类型就行,集群环境则使用分布式锁( redis的setnx)集群环境的redis的代码如下所示:
String get(String key) { String value = redis.get(key); if (value == null) { if (redis.setnx(key_mutex, "1")) { // 3 min timeout to avoid mutex holder crash redis.expire(key_mutex, 3 * 60) value = db.get(key); redis.set(key, value); redis.delete(key_mutex); } else { //其他线程休息50毫秒后重试 Thread.sleep(50); get(key); } } }
分布式环境下,使用setnx分布式锁(redis下的)key_mutex加锁,让redis向数据库请求值,其他的线程获取不到互斥锁,所以只能先休息50毫秒在来试,缓解了数据库的负担。
优点:思路简单,能一直保证缓存一致性
缺点:代码复杂,有可能存在死锁风险:如果另外的线程获取的资源正好是当前线程锁定的资源,同时当前线程即将要去访问另外线程正锁定的内容,就出现了死锁。
2、布隆过滤器:
布隆过滤器可以迅速从庞大的集合中查找到是否含有某个元素。
<dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>22.0</version> </dependency> </dependencies
import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class Test { private static int size = 1000000; private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size); public static void main(String[] args) { for (int i = 0; i < size; i++) { bloomFilter.put(i); } long startTime = System.nanoTime(); // 获取开始时间 //判断这一百万个数中是否包含29999这个数 for (int i = 10000; i <= 50000; i *= 2) { if (bloomFilter.mightContain(i)) { System.out.println("命中了"); } } long endTime = System.nanoTime(); // 获取结束时间 System.out.println("程序运行时间: " + (endTime - startTime) + "纳秒"); } }
其误判率越低,所占数组长度就越长。
实例化时在构造函数内的第三个参数添加误判率,必须大于0.0,默认为0.3;
实际在缓存击穿上的应用:
String get(String key) { String value = redis.get(key); if (value == null) { if(!bloomfilter.mightContain(key)){ return null; }else{ value = db.get(key); redis.set(key, value); } } return value; }
优点:思路简单,能保证缓存一致性,性能强;
缺点:代码复杂,需要另外维护一个集合来存放缓存的key,不支持删值操作