在之前我们提及过布隆过滤器这个概念,回顾一下当时是在了解缓存穿透的时接触到了这个概念。在没有引入布隆过滤器这个概念时,我们可以通过其他的一些途径解决缓存穿透这个问题。例如我们现在需要获取用户数据,参数中包含用户ID,若某个请求中传递了一个我们数据库中不存在的用户ID,那么这个请求无论请求多少次都会穿透缓存命中DB,当然我们可以通过缓存空值去尽量避免这个问题。但是我们还可以通过一种思路去解决,我们在代码中维护一个内存数据结构(List/Set/Map),在项目启动时将数据库中存在的用户ID维护到一个集合中,则请求时可通过判断请求用户ID是否存在于当前集合中来避免穿透。 其实这里面涉及都了两个概念:
黑客流量攻击:恶意频繁访问不存在的数据,导致程序不断访问DB数据库。黑客安全阻截:当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。我们来比较下上面三种集合遍历检验元素的性能:
List.contain(key):遍历数据,进行equals()比较,性能低Set.contain(key) :利用Hashcode比较,性能较高Map.get(key):利用Hashcode比较,但数据结构相对复杂,性能适中当我们的用户数量仅仅只有一万、十万的时候,我们使用上面几种集合去进行一个遍历检验性能可能还是不错的。但是当我们的用户量达到百万千万甚至更多的时候,我们就不得不考虑一下这几种数据结构对性能和空间占有量带来的一些影响。这里我们就要引入布隆过滤器这个概念了。
布隆过滤器(Bloom Filter):是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 布隆过滤器是一种数据结构,可以用来判断某个Key是否一定不存在或者可能存在于容器当中。并且因为其特殊的数据结构构成,使得查找效率很高并且占用空间更小。它是通过多个哈希函数生成多个哈希值,并对每个生成的哈希值指向的BIT位置为1。 上图所就是布隆过滤器的实现原理,布隆过滤器有一个问题就是存在误算率,我们来看一下如何体现。假如现在布隆过滤器中已有一部分数据存入,并通过Hash计算将1、4、5、6、7、9、10、12块置为1,此时添加一个不存在的Key到布隆过滤器中,该Key通过Hash计算得到1、5、9、12,此时校验1、5、9、12槽中都为1,所以该Key在布隆过滤器中检验会通过,实际上是由于和其他多个Key计算的Hash值完全重叠,这就是布隆过滤器的误算率。这里我们来总结一下布隆过滤器的优缺点:
优点:相比于其它数据结构,布隆过滤器在空间和时间方面都具备巨大优势。布隆过滤器存储空间和操作时间都是常数级。并且散列函数之间不存在关系,方便由硬件并行实现。而且它不需要存储元素本身,在对保密要求非常严格的场合更具备优势。缺点:布隆过滤器的缺点也十分明显,由于实现原理所造成的误算率就是比较明显的一个缺点。随着布隆过滤器中元素数量增加,误算率也会随之增加。若元素数量太少,则使用散列表就足够了。上面我们举例的只是布隆过滤器的一个小小的场景应用,它还能应用于很多其他场景。例如:网页爬虫URL去重、反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱、垃圾短信电话等等很多其他场景。
对于布隆过滤器这种数据结构其实有很多支持,其中Google布隆过滤器就是比较知名的一种,这里我们通过一个简单的会员抽奖功能去了解Google布隆过滤器及其在代码中如何使用。 首先我们准备了一些用户数据,各位可以自己简单准备一点。 这里我们还是在之前的项目上进行编写,首先引入Google提供的依赖包。
pom.xml
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>20.0</version> </dependency>这里我们再去编写一个用于项目启动时初始化加载用户数据以及提供布隆过滤器校验的Service以及提供API入口的Controller。
BloomFilterService.java
package com.springboot.service; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import com.springboot.dao.BlogUserMapper; import com.springboot.repository.entity.BlogUser; import com.springboot.repository.entity.BlogUserExample; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.annotation.PostConstruct; import java.util.List; /** * @author hzk * @date 2019/7/19 */ @Service public class BloomFilterService { @Autowired private BlogUserMapper blogUserMapper; private BloomFilter<Integer> bloomFilter; /** * 程序启动时初始化布隆过滤器 */ @PostConstruct public void initBloomFilter(){ List<BlogUser> blogUsers = blogUserMapper.selectByExample(new BlogUserExample()); if(!CollectionUtils.isEmpty(blogUsers)){ bloomFilter = BloomFilter.create(Funnels.integerFunnel(),blogUsers.size()); for (BlogUser blogUser : blogUsers) { bloomFilter.put(blogUser.getId()); } } } /** * 判断用户ID是否真实存在 * @param id * @return */ public boolean existUser(Integer id){ if(null != id){ return bloomFilter.mightContain(id); } return false; } }BloomFilterController.java
package com.springboot.controller; import com.springboot.service.BloomFilterService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.websocket.server.PathParam; /** * @author hzk * @date 2019/7/19 */ @RestController @RequestMapping("/api/bloom") public class BloomFilterController { @Autowired private BloomFilterService bloomFilterService; @RequestMapping("/existUser/{id}") public boolean existUser(@PathVariable("id") Integer id){ return bloomFilterService.existUser(id); } }启动服务,验证布隆过滤器达到了我们预期的效果。其实整个使用都十分简单,大家应该也很清楚就了解到了布隆过滤器的一个基本使用。
Redis从4.0开始引入了Module的概念,这里我们通过加载Module去嵌入布隆过滤器。 这里我们首先需要安装git,通过git去拉取布隆过滤器模块。如果没有没有安装的可以通过以下命令安装,或者在安装过的环境下拉取之后上传到服务器上。
1、安装git,直接使用yum安装即可: yum -y install git 2、创建git用户,git用户可以正常通过ssh使用git,但无法登录shell,因为我们为git用户指定的git-shell每次一登录就自动退出。 useradd -m -d /home/git -s /usr/bin/git-shell git 3、初始化git仓库 mkdir -p /data/git cd /data/git git init --bare project1.git chown git.git project1.git -R 4、创建免密钥 cd /home/git mkdir .ssh chmod 700 .ssh touch .ssh/authorized_keys chmod 600 .ssh/authorized_keys chown git.git .ssh -R安装好git后通过git clone git://github.com/RedisLabsModules/rebloom拉取布隆过滤器模块,这里我放在之前Redis目录下。通过make进行编译。
cd /usr/local/redis/redis-4.0.6/rebloom make这里我们为了方便,启一个最简单的单机Redis,修改配置文件redis.conf添加loadmodule /usr/local/redis/redis-4.0.6/rebloom/redisbloom.so。 启动Redis服务,使用客户端连接验证布隆过滤器。
127.0.0.1:6379> BF.ADD userBloom kobe (integer) 1 127.0.0.1:6379> BF.ADD userBloom kobe (integer) 0 127.0.0.1:6379> BF.ADD userBloom james (integer) 1 127.0.0.1:6379> BF.EXISTS userBloom james (integer) 1 127.0.0.1:6379> BF.EXISTS userBloom paul (integer) 0我们通过Redis官方提供的方法发现无法直接去操作布隆过滤器,这是由于布隆过滤器是我们额外引入进入的模块。这里我们可以通过github上的开源框架JRedisBloom【RedisBloom/JRedisBloom】去操作Redis布隆过滤器。上面也提供了各种创建使用的例子,大家看一下很快就可以上手。但是仔细看一遍我们可以发现这个开源框架提供了单机、集群的操作方式,却没有提供哨兵模式下的集成操作方式,如果各位使用的是单机或者集群的方式则可以直接引入JRedisBloom。这里我们为了兼容哨兵模式,使用之前Lua脚本的方式去做一个所有模式公用的操作方式。
这里我们先编写好两个Lua脚本。
bf_add.lua
local bloomName = KEYS[1] local value = KEYS[2] local result = redis.call('BF.ADD',bloomName,value) return resultbf_exist.lua
local bloomName = KEYS[1] local value = KEYS[2] local result = redis.call('BF.EXISTS',bloomName,value) return result接着我们提供出一个用于操作脚本的Service和提供API的Controller。
RedisBloomFilterService.java
package com.springboot.service; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import com.springboot.dao.BlogUserMapper; import com.springboot.repository.entity.BlogUser; import com.springboot.repository.entity.BlogUserExample; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.List; /** * @author hzk * @date 2019/7/19 */ @Service public class RedisBloomFilterService { @Autowired private RedisTemplate redisTemplate; public static final String BLOOMFILTER_NAME = "user_bloom"; /** * 向布隆过滤器中添加元素 * @param id * @return */ public Boolean bloomFilterAdd(Integer id){ DefaultRedisScript<Boolean> LuaScript = new DefaultRedisScript<Boolean>(); LuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("bf_add.lua"))); LuaScript.setResultType(Boolean.class); //封装传递脚本参数 ArrayList<Object> params = new ArrayList<>(); params.add(BLOOMFILTER_NAME); params.add(String.valueOf(id)); return (Boolean) redisTemplate.execute(LuaScript, params); } /** *检验元素是否可能存在于布隆过滤器中 * @param id * @return */ public Boolean bloomFilterExist(Integer id){ DefaultRedisScript<Boolean> LuaScript = new DefaultRedisScript<Boolean>(); LuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("bf_exist.lua"))); LuaScript.setResultType(Boolean.class); //封装传递脚本参数 ArrayList<Object> params = new ArrayList<>(); params.add(BLOOMFILTER_NAME); params.add(String.valueOf(id)); return (Boolean) redisTemplate.execute(LuaScript, params); } }RedisBloomFilterController
package com.springboot.controller; import com.springboot.service.BloomFilterService; import com.springboot.service.RedisBloomFilterService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author hzk * @date 2019/7/19 */ @RestController @RequestMapping("/api/redisbloom") public class RedisBloomFilterController { @Autowired private RedisBloomFilterService redisBloomFilterService; @RequestMapping("/addUser/{id}") public boolean addUser(@PathVariable("id") Integer id){ return redisBloomFilterService.bloomFilterAdd(id); } @RequestMapping("/existUser/{id}") public boolean existUser(@PathVariable("id") Integer id){ return redisBloomFilterService.bloomFilterExist(id); } }启动成功后我们通过接口验证,符合布隆过滤器的验证结果。
这里我们主要接触的就是两种布隆过滤器,一种是Redis提供的布隆过滤器模块,另一种是Google提供的布隆过滤器。这里两种实现都有各自的优缺点,我们来比较一下。 Google布隆过滤器:
优点:基于内存、性能高、整合使用方便快捷、本身不会产生多余的维护成本。缺点:服务重启即失效、无法在分布式场景使用、不适合用于大数据量。Redis布隆过滤器:
优点:可扩展性强、不会对服务造成额外内存消耗、存储信息不会随服务重启而丢失、可用于分布式场景。缺点:基于网络I/O、性能相对内存级别产品较低。关于使用哪种过滤器,主要还是根据实际情况来考虑,若能够保证存储的数据量不大使用内存级别的布隆过滤器更好,面对大数据量的业务或者是分布式架构的业务则采用Redis布隆过滤器更好。
