八、缓存穿透
一、简介
请求去查询一条记录,先 redis 后 mysql 发现都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设。
危害:
第一次来查询后,一般我们有回写 redis 机制。第二次来查的时候 redis 就有了,偶尔出现穿透现象一般情况无关紧要。
要防止恶意攻击,用 redis 和 mysql 都不存在的数据,不断查询。
二、解决方案1:空对象缓存或者缺省值
黑客会对你的系统进行攻击,拿一个不存在的 id 去查询数据,会产生大量的请求到数据库去查询,可能会导致你的数据库由于压力过大而宕掉。
id相同打你系统。第一次打到mysql,空对象缓存后第二次就返回null了,避免mysql被攻击,不用再到数据库中去走一圈了。
id不同打你系统。由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)。
三、解决方案2:Google布隆过滤器Guava解决缓存穿透
Guava适合应用在单机项目。
1、pom文件
<!--guava Google 开源的 Guava 中自带的布隆过滤器--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency>
2、HelloWorld级的demo
@Test public void bloomFilter() { // 创建布隆过滤器对象 BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100); // 判断指定元素是否存在 System.out.println(filter.mightContain(1)); System.out.println(filter.mightContain(2)); // 将元素添加进布隆过滤器 filter.put(1); filter.put(2); System.out.println(filter.mightContain(1)); System.out.println(filter.mightContain(2)); }
3、100w数据的误判率的demo
public class BloomfilterDemo { public static final int _1W = 10000; //布隆过滤器里预计要插入多少数据 public static int size = 100 * _1W; //误判率 public static double fpp = 0.03; public static void main(String[] args) { // 构建布隆过滤器 BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp); // 1 先往布隆过滤器里面插入100万的样本数据 for (int i = 0; i < size; i++) { bloomFilter.put(i); } // 故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里 List<Integer> list = new ArrayList<>(10 * _1W); for (int i = size + 1; i < size + 100000; i++) { if (bloomFilter.mightContain(i)) { System.out.println(i + "\t" + "被误判了."); list.add(i); } } System.out.println("误判的数量:" + list.size()); } }
输出:
...
1099707 被误判了.
1099850 被误判了.
1099882 被误判了.
1099947 被误判了.
误判的数量:3033
4、误判率分析
fpp=0.03,它越小误判的个输也就越少。这样是不是可以设置无限小?
源码底层会根据设置的误判率,不断添加hash函数和布隆过滤器的 bit 位。因此误判率设置的越小,内存开销越大,执行效率也越低,计算量急剧增大。0.03 是默认系数。
5、布隆过滤器常用方案
四、解决方案3:Redis布隆过滤器解决缓存穿透
为了Guava只能单机使用的这个问题,诞生了 Redis 中的布隆过滤器。
1、白名单demo
public class RedissonBloomFilterDemo { public static final int _1W = 10000; //布隆过滤器里预计要插入多少数据 public static int size = 100 * _1W; //误判率,它越小误判的个数也就越少 public static double fpp = 0.03; static RedissonClient redissonClient = null; static RBloomFilter rBloomFilter = null; static { Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.1.101:6379").setDatabase(0); //构造redisson redissonClient = Redisson.create(config); //通过redisson构造rBloomFilter rBloomFilter = redissonClient.getBloomFilter("phoneListBloomFilter", new StringCodec()); rBloomFilter.tryInit(size, fpp); // 测试一、布隆过滤器有+redis有 rBloomFilter.add("10086"); redissonClient.getBucket("10086", new StringCodec()).set("chinamobile10086"); // 测试二、隆过滤器有+redis无 //rBloomFilter.add("10087"); // 测试三、都没有 } public static void main(String[] args) { String phoneListById = getPhoneListById("10086"); System.out.println("------查询出来的结果: " + phoneListById); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } redissonClient.shutdown(); } private static String getPhoneListById(String IDNumber) { String result = null; if (IDNumber == null) { return null; } // 1 先去布隆过滤器里面查询 if (rBloomFilter.contains(IDNumber)) { // 2 布隆过滤器里有,再去redis里面查询 RBucket<String> rBucket = redissonClient.getBucket(IDNumber, new StringCodec()); result = rBucket.get(); if (result != null) { return "i come from redis: " + result; } else { result = getPhoneListByMySQL(IDNumber); if (result == null) { return null; } // 重新将数据更新回redis redissonClient.getBucket(IDNumber, new StringCodec()).set(result); } return "i come from mysql: " + result; } return result; } private static String getPhoneListByMySQL(String IDNumber) { return "chinamobile" + IDNumber; } }