介绍布隆过滤器之前,先介绍一下哈希函数,我们在Java中的HashMap,HashSet也接触过hashcode()这个函数。
哈希函数指将哈希表中元素的关键键值通过一定的函数关系映射为元素存储位置的函数。
布隆过滤器实际上是一个非常长的二进制向量(bitmap)和一系列随机哈希函数。
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom Filter(BF)是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。
它是一个判断元素是否存在集合的快速的概率算法。Bloom Filter有可能会出现错误判断,但不会漏掉判断。也就是Bloom Filter判断元素不再集合,那肯定不在。如果判断元素存在集合中,有一定的概率判断错误。因此,Bloom Filter”不适合那些“零错误的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter比其他常见的算法(如hash,折半查找)极大节省了空间。
常见的补救办法是建立一个小的白名单,存储那些可能被误判的元素。但是如果元素数量太少,使用散列表足矣。
我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面,这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
布隆过滤器可以用于检索一个元素是否在一个集合中,常用于解决如下问题
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。
检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。

步骤:

不再需要spring-boot-starter-data-redis依赖,但是都添加也不会报错
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.5</version>
</dependency>
https://www.jianshu.com/p/00ddb0187468
https://www.jianshu.com/p/00ddb0187468
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @Author: huangyibo
* @Date: 2022/7/25 17:21
* @Description:
*/
@Component
public class BloomFilterUtil {
@Resource
private RedissonClient redissonClient;
/**
* 创建布隆过滤器
*
* @param filterName 过滤器名称
* @param expectedInsertions 预测插入数量
* @param falsePositiveRate 误判率
*/
public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falsePositiveRate) {
RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(filterName);
bloomFilter.tryInit(expectedInsertions, falsePositiveRate);
return bloomFilter;
}
}
其它层正常编写即可,与之前并无差别,此处不再展示
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @Author: huangyibo
* @Date: 2022/7/25 18:44
* @Description:
*/
@Service
public class UserServiceImpl {
// 预期插入数量
static long expectedInsertions = 200L;
// 误判率
static double falseProbability = 0.01;
// 非法请求所返回的JSON
static String illegalJson = "[\"com.company.springboot.entity.User\",{\"id\":null,\"userName\":\"null\",\"sex\":null,\"age\":null}]";
private RBloomFilter<Long> bloomFilter = null;
@Resource
private BloomFilterUtil bloomFilterUtil;
@Resource
private RedissonClient redissonClient;
@Resource
private UserMapper userMapper;
@PostConstruct // 项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法
public void init() {
// 启动项目时初始化bloomFilter
List<User> userList = this.list();
bloomFilter = bloomFilterUtil.create("idWhiteList", expectedInsertions, falseProbability);
for (User user : userList) {
bloomFilter.add(user.getId());
}
}
@Cacheable(cacheNames = "user", key = "#id", unless = "#result==null")
public User findById(Long id) {
// bloomFilter中不存在该key,为非法访问
if (!bloomFilter.contains(id)) {
System.out.println("所要查询的数据既不在缓存中,也不在数据库中,为非法key");
/**
* 设置unless = "#result==null"并在非法访问的时候返回null的目的是不将该次查询返回的null使用
* RedissonConfig-->RedisCacheManager-->RedisCacheConfiguration-->entryTtl设置的过期时间存入缓存。
*
* 因为那段时间太长了,在那段时间内可能该非法key又添加到bloomFilter,比如之前不存在id为1234567的用户,
* 在那段时间可能刚好id为1234567的用户完成注册,使该key成为合法key。
*
* 所以我们需要在缓存中添加一个可容忍的短期过期的null或者是其它自定义的值,使得短时间内直接读取缓存中的该值。
*
* 因为Spring Cache本身无法缓存null,因此选择设置为一个其中所有值均为null的JSON,
*/
redissonClient.getBucket("user::" + id, new StringCodec()).set(illegalJson, new Random().nextInt(200) + 300, TimeUnit.SECONDS);
return null;
}
// 不是非法访问,可以访问数据库
System.out.println("数据库中得到数据*****");
return userMapper.selectById(id);
}
// 先执行方法体中的代码,成功执行之后删除缓存
@CacheEvict(cacheNames = "user", key = "#id")
public boolean delete(Long id) {
// 删除数据库中具有的数据,就算此key从此之后不再出现,也不能从布隆过滤器删除
return userMapper.deleteById(id) == 1;
}
// 如果缓存中先前存在,则更新缓存;如果不存在,则将方法的返回值存入缓存
@CachePut(cacheNames = "user", key = "#user.id")
public User update(User user) {
userMapper.updateById(user);
// 新生成key的加入布隆过滤器,此key从此合法,因为该更新方法并不更新id,所以也不会产生新的合法的key
bloomFilter.add(user.getId());
return user;
}
@CachePut(cacheNames = "user", key = "#user.id")
public User insert(User user) {
userMapper.insert(user);
// 新生成key的加入布隆过滤器,此key从此合法
bloomFilter.add(user.getId());
return user;
}
}
注意:
redisson利用redis存储,布隆过滤器生成数组,但是长度限制为 4 294 967 296 ,但是根据布隆过滤器的原理来看,生成的数组长度是没有限制的,判断应该是redis String类型最大是512M所导致的限制。
加入我们这次使用redis & BloomFilter 的核心依赖包
<!--使用Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--借助guava的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
spring:
redis:
database: 3
host: 127.0.0.1
port: 6379
password: 12345
jedis.pool.max-idle: 100
jedis.pool.max-wait: -1ms
jedis.pool.min-idle: 2
timeout: 2000ms
如果是一般的使用redis存字符串的话,使用StringRedisTemplate,就不需要配置序列化。
但是咱们这里使用的是RedisTemplate<String, Object> redisTemplate ,存储的是对象,所以为了防止存入的对象值在查看的时候不显示乱码,就需要配置相关的序列化(其实我们存的bit结构数据,布隆过滤器存值分分钟都是百万级别的,会因为数据量太大redis客户端也没办法显示,不过不影响使用)。
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.hash.Funnel;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
/**
* @Author: huangyibo
* @Date: 2022/7/25 19:00
* @Description:
*/
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.create(connectionFactory);
return redisCacheManager;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//序列化设置 ,这样计算是正常显示的数据,也能正常存储和获取
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(factory);
return stringRedisTemplate;
}
//初始化布隆过滤器,放入到spring容器里面
@Bean
public BloomFilterHelper<String> initBloomFilterHelper() {
return new BloomFilterHelper<>((Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8), 1000000, 0.01);
}
}
import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;
/**
* @Author: huangyibo
* @Date: 2022/7/25 18:59
* @Description:
*/
public class BloomFilterHelper<T> {
private int numHashFunctions;
private int bitSize;
private Funnel<T> funnel;
public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
Preconditions.checkArgument(funnel != null, "funnel不能为空");
this.funnel = funnel;
// 计算bit数组长度
bitSize = optimalNumOfBits(expectedInsertions, fpp);
// 计算hash方法执行次数
numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
}
public int[] murmurHashOffset(T value) {
int[] offset = new int[numHashFunctions];
long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
offset[i - 1] = nextHash % bitSize;
}
return offset;
}
/**
* 计算bit数组长度
*/
private int optimalNumOfBits(long n, double p) {
if (p == 0) {
// 设定最小期望长度
p = Double.MIN_VALUE;
}
int sizeOfBitArray = (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
return sizeOfBitArray;
}
/**
* 计算hash方法执行次数
*/
private int optimalNumOfHashFunctions(long n, long m) {
int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
return countOfHash;
}
}
然后是具体的布隆过滤器配合redis使用的 方法类
import com.google.common.base.Preconditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* @Author: huangyibo
* @Date: 2022/7/25 19:02
* @Description:
*/
@Component
public class RedisBloomFilter<T> {
@Autowired
private RedisTemplate<String, T> redisTemplate;
/**
* 根据给定的布隆过滤器添加值
*/
public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
System.out.println("key : " + key + " " + "value : " + i);
redisTemplate.opsForValue().setBit(key, i, true);
}
}
/**
* 根据给定的布隆过滤器判断值是否存在
*/
public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
System.out.println("key : " + key + " " + "value : " + i);
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
return true;
}
}
local bloomName = KEYS[1]
local value = KEYS[2]
local result = redis.call('BF.ADD',bloomName,value)
return result
local bloomName = KEYS[1]
local value = KEYS[2]
local result = redis.call('BF.EXISTS',bloomName,value)
return result
@Service
public class RedisBloomFilterService {
@Autowired
private RedisTemplate redisTemplate;
//我们依旧用刚刚的那个过滤器
public static final String BLOOMFILTER_NAME = "test-bloom-filter";
/**
* 向布隆过滤器添加元素
* @param str
* @return
*/
public Boolean bloomAdd(String str) {
DefaultRedisScript<Boolean> LuaScript = new DefaultRedisScript<Boolean>();
LuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("bf_add.lua")));
LuaScript.setResultType(Boolean.class);
//封装传递脚本参数
List<String> params = new ArrayList<String>();
params.add(BLOOMFILTER_NAME);
params.add(str);
return (Boolean) redisTemplate.execute(LuaScript, params);
}
/**
* 检验元素是否可能存在于布隆过滤器中 * @param id * @return
*/
public Boolean bloomExist(String str) {
DefaultRedisScript<Boolean> LuaScript = new DefaultRedisScript<Boolean>();
LuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("bf_exist.lua")));
LuaScript.setResultType(Boolean.class);
//封装传递脚本参数
ArrayList<String> params = new ArrayList<String>();
params.add(BLOOMFILTER_NAME);
params.add(String.valueOf(str));
return (Boolean) redisTemplate.execute(LuaScript, params);
}
}
参考:
https://blog.csdn.net/dreaming9420/article/details/124153422
https://developer.aliyun.com/article/951745
https://www.cnblogs.com/woshixiangshang/p/11412474.html
我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel
我一直很高兴地使用DelayedJob习惯用法:foo.send_later(:bar)这会调用DelayedJob进程中对象foo的方法bar。我一直在使用DaemonSpawn在我的服务器上启动DelayedJob进程。但是...如果foo抛出异常,Hoptoad不会捕获它。这是任何这些包中的错误...还是我需要更改某些配置...或者我是否需要在DS或DJ中插入一些异常处理来调用Hoptoad通知程序?回应下面的第一条评论。classDelayedJobWorker 最佳答案 尝试monkeypatchingDelayed::W
是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s
前置步骤我们都操作完了,这篇开始介绍jenkins的集成。话不多说,看操作1、登录进入jenkins后会让你选择安装插件,选择第一个默认的就行。安装完成后设置账号密码,重新登录。2、配置JDK和Git都需要执行路径,所以需要先把执行路径找到,先进入服务器的docker容器,2.1JDK的路径root@69eef9ee86cf:/usr/bin#echo$JAVA_HOME/usr/local/openjdk-82.2Git的路径root@69eef9ee86cf:/#whichgit/usr/bin/git3、先配置JDK和Git。点击:ManageJenkins>>GlobalToolCon
我有一个名为Post的类,我需要能够适应以下场景:如果用户选择了一个类别,则只显示该类别的帖子如果用户选择了一种类型,则只显示该类型的帖子如果用户选择了一个类别和类型,则只显示该类别中该类型的帖子如果用户没有选择任何内容,则显示所有帖子我想知道我的Controller是否不可避免地会因大量条件语句而显得粗糙...这是我解决此问题的错误方法-有谁知道我如何才能做到这一点?classPostsController 最佳答案 您最好遵循“胖模型,瘦Controller”的惯例,这意味着您应该将这种逻辑放在模型本身中。Post类应该能够报告
我正在我的Rails项目中安装Grape以构建RESTfulAPI。现在一些端点的操作需要身份验证,而另一些则不需要身份验证。例如,我有users端点,看起来像这样:moduleBackendmoduleV1classUsers现在如您所见,除了password/forget之外的所有操作都需要用户登录/验证。创建一个新的端点也没有意义,比如passwords并且只是删除password/forget从逻辑上讲,这个端点应该与用户资源。问题是Grapebefore过滤器没有像except,only这样的选项,我可以在其中说对某些操作应用过滤器。您通常如何干净利落地处理这种情况?
我仍然收到标题中的“错误”消息,但不知道如何解决。在ApplicationController中,classApplicationController在routes.rb#match'set_activity_account/:id/:value'=>'users#account_activity',:as=>:set_activity_account--thisdoesn'tworkaswell..resources:usersdomemberdoget:action_a,:action_bendcollectiondoget'account_activity'endend和User
对于用户模型,我有一个过滤器来检查用户的预订状态,该状态由整数值(0、1或2)表示。UserActiveAdmin索引页上的过滤器是通过以下代码实现的:filter:booking_status,as::select然而,这会导致下拉选项为0、1或2。当管理员用户从下拉列表中选择它们时,我更愿意自己将它们命名为“未完成”、“待定”和“已确认”之类的名称。有没有办法在不改变booking_status在模型中的表示方式的情况下做到这一点? 最佳答案 假设booking_status是模型中的枚举字段,您可以使用:过滤器:booking
三分钟集成Tap防沉迷SDK(Unity版)一、SDK介绍基于国家对上线所有游戏必须增加防沉迷功能的政策下,TapTap推出防沉迷SDK,供游戏开发者进行接入;允许未成年用户在周五、六、日以及法定节假日晚上8:00-9:00进行游戏,防沉谜时间段进入游戏会弹窗进行提示!开发环境要求:Unity2019.4或更高版本iOS10或更高版本Android5.0(APIlevel21)或更高版本🔗Unity集成Demo参考链接🔗UnityTapSDK功能体验APK下载链接二、集成前准备1.创建应用进入开发者后台,按照提示开始创建应用;2.开通服务在使用TDS实名认证和防沉迷服务之前,需要在上面创建的应
我被这个难住了。到目前为止教程中的一切都进行得很顺利,但是当我将这段代码添加到我的/spec/requests/users_spec.rb文件中时,事情开始变得糟糕:describe"success"doit"shouldmakeanewuser"dolambdadovisitsignup_pathfill_in"Name",:with=>"ExampleUser"fill_in"Email",:with=>"ryan@example.com"fill_in"Password",:with=>"foobar"fill_in"Confirmation",:with=>"foobar"cl