缓存穿透与布隆过滤器BloomFilter那些事

原文在这里: 缓存穿透与布隆过滤器BloomFilter那些事
缓存穿透与布隆过滤器BloomFilter那些事_第1张图片
很多小伙伴在面试的时候都会被问到类似这样的问题:如何解决redis的缓存穿透问题?以用户登录为例来说,浏览器传递过来用户名和密码,服务端首先是根据用户名去查cache,如果有直接返回,如果没有再去查DB,如果查到回写到cache并返回,查不到说明用户不存在。中间加一个cache就是为了加快查询的速度,减轻DB的压力,对DB做一层保护。

假如有一个恶意请求,用一个系统不存在的用户名去刷登录接口,cache始终不会命中,这些请求就都会透传到DB中,给DB造成很大的压力,这就是所谓的缓存穿透问题。

一般有两种解决办法,第一种是缓存空对象,但是给缓存加一个很短的有效期,显然并不是一种很好的解决办法,因为有效期之内很可能数据是不一致的。还有种更好的办法是使用布隆过滤器。

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。算法的实现细节就不说了,自行百度之,本文主要讲解如何来使用。

布隆过滤器牛逼的地方在于非常高效同时占空间非常少,它判断一个元素不存在那肯定就是不存在,它判断存在的时候有一定误差,是有可能不存在的。对应于前面说的那个场景,服务端收到请求之后,可以首先用布隆过滤器判断下用户名是否存在,如果不存在就直接返回,就不用再去查DB了,非常完美的解决方案!

1.让Redis服务器(4.0以上)支持布隆过滤器
(1)下载bloom-filter插件,下载地址:
https://github.com/RedisLabsModules/redisbloom/

unzip RedisBloom-master.zip
cd RedisBloom-master
make

在当前目录下生成redisbloom.so。
当然也可以直接使用已经编译好的so,比如:
https://github.com/RedisBloom/RedisBloom/releases/tag/v1.1.1
(2)修改redis的配置文件,加载so

vi redis.conf
loadmodule /usr/local/src/RedisBloom-master/redisbloom.so

(3)重启redis服务器
测试一下:

127.0.0.1:6379> bf.add books java
(integer) 1
127.0.0.1:6379> bf.add books c++
(integer) 1
127.0.0.1:6379> bf.add books php
(integer) 1
127.0.0.1:6379> bf.exists books java
(integer) 1
127.0.0.1:6379> bf.exists books c
(integer) 0
127.0.0.1:6379> bf.reserve users 0.01 1000
OK
127.0.0.1:6379> bf.add users u1
(integer) 1
127.0.0.1:6379> bf.add users u2
(integer) 1
127.0.0.1:6379> bf.add users u3
(integer) 1
127.0.0.1:6379> bf.exists users u3
(integer) 1

bf.reserve创建Filter,语法:[bf.reserve key error_rate initial_size],需要注意的是:布隆过滤器的initial_size估计的过大,所需要的空间就越大,会浪费空间,估计的过小会影响准确率,因此在使用前一定要估算好元素数量,还需要加上一定的冗余空间以避免实际元素高出预估数量造成误差过大。布隆过滤器的error_rate越小,所需要的空间就会越大,对于不需要过于准确的,error_rate设置的稍大一点也无所谓。

2.如何用java客户端来使用布隆过滤器
jedis不支持布隆过滤器,可以使用RedisLabs提供的JReBloom,github地址:https://github.com/RedisLabsModules/redisbloom/。
引入依赖:

<dependency>
      <groupId>redis.clientsgroupId>
      <artifactId>jedisartifactId>
      <version>3.0.0version>
    dependency>
    
    <dependency>
        <groupId>com.redislabsgroupId>
        <artifactId>jrebloomartifactId>
        <version>1.2.0version>
    dependency>

添加配置:


@Configuration
public class RedisConfig {
  @Autowired
  RedisProperties redisProperties;
  @Bean
  public JedisPool jedisPool() {
    JedisPoolConfig poolConfig = new JedisPoolConfig();
    poolConfig.setMaxIdle(redisProperties.getPoolMaxIdle());
    poolConfig.setMaxTotal(redisProperties.getPoolMaxTotal());
    poolConfig.setMaxWaitMillis(redisProperties.getPoolMaxWait() * 1000);
    JedisPool jp = new JedisPool(poolConfig, redisProperties.getHost(), redisProperties.getPort(),
        redisProperties.getTimeout()*1000, redisProperties.getPassword(), 0);
    return jp;
  }
  @Bean
  public Client bloomFilter(JedisPool jp) {
    return new Client(jp);
  }
}

然后就可以直接注入Client进行使用了:

@Service
public class RedisService {
  @Autowired
  Client client;
  public boolean bfCreate(String key, long initCapacity, double errorRate) {
    try {
      client.createFilter(key, initCapacity, errorRate);
      return true;
    }catch(Exception e){
      e.printStackTrace();
      return false;
    }
  }
  public boolean bfAdd(String key, String value) {
    try {
      return client.add(key, value);
    }catch(Exception e){
      e.printStackTrace();
      return false;
    }
  }
  public boolean bfExists(String key, String value) {
    try {
      return client.exists(key, value);
    }catch(Exception e){
      e.printStackTrace();
      return false;
    }
  }
  public boolean delete(String key) {
    try {
      client.delete(key);
      return true;
    }catch(Exception e){
      e.printStackTrace();
      return false;
    }
  }
}

3.在java中直接使用布隆过滤器
Google的guava包提供了bloom filter的实现,我们来看下如何使用。
引入依赖:

<dependency>
        <groupId>com.google.guavagroupId>
        <artifactId>guavaartifactId>
        <version>28.0-jreversion>
    dependency>

测试代码:

@Test
  public void create() {
    BloomFilter<String> bf = BloomFilter.create(
        Funnels.stringFunnel(Charset.defaultCharset()), 10000, 0.01);
    bf.put("hello");
    bf.put("world");
    bf.put("java");
    boolean ret = bf.mightContain("java");
    assertTrue(ret);
    boolean ret2 = bf.mightContain("php");
    assertFalse(ret2);
  }

完整的源码下载:扫描文章开头的二维码加关注查看原文。

你可能感兴趣的:(java)