解决Redis缓存穿透之布隆过滤器详解
文章目录
- 1. 什么是Bloom Filter(布隆过滤器)
- 1.1 布隆过滤器优点
- 1.2 布隆过滤器缺点
- 1.3 布隆过滤器使用场景
- 1.4 布隆过滤器检索过程
- 1.5 布隆过滤器的算法描述
- 2. 布隆过滤器实际应用
- 2.1 在Java中使用布隆过滤器来进行邮件校验(简单实现)
- 2.2 复杂布隆过滤器实现
- 2.3 Redission 中的分布式布隆过滤器
- 3. 使用布隆过滤器解决Redis缓存穿透
1. 什么是Bloom Filter(布隆过滤器)
一个很长的二进制向量和一系列随机映射函数实现可以用于检索一个元素是否存在一个集合中
1.1 布隆过滤器优点
- 空间效率高,所占用空间小
- 查询时间短
1.2 布隆过滤器缺点
- 元素添加到集合中后,不能被删除
- 存在一定的误判率,数据越多误判率越高
1.3 布隆过滤器使用场景
字处理软件中,需要检查一个英语单词是否拼写正确
在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上
在网络爬虫里,一个网址是否被访问过
yahoo, gmail等邮箱垃圾邮件过滤功能
最常见的也就是面试中提到的缓存穿透是怎么解决的 或多或少都会提到一个布隆过滤器的概念
如图便是一个基本的布隆过滤器
下图是将0 存入到布隆过滤器中
1.4 布隆过滤器检索过程
在线使用布隆过滤器
- 要查询一个元素(测试它是否在集合中),请将其提供给k个哈希函数中的每一个以获取k个数组位置。如果这些位置的任何位为 0,则该元素肯定不在集合中;如果是,那么在插入时所有位都将设置为 1。如果全部为 1,则要么元素在集合中,要么在插入其他元素期间这些位偶然设置为 1,从而导致误报。在简单的布隆过滤器中,无法区分这两种情况,但更高级的技术可以解决这个问题。
1.5 布隆过滤器的算法描述
一个空的布隆过滤器是一个由m位组成的位数组,全部设置为 0。还必须定义k个不同的散列函数,每个散列函数将某个集合元素映射或散列到m个数组位置之一,生成均匀随机分布。通常,k是一个小常数,它取决于所需的错误错误率 ε,而m与k和要添加的元素数量 成正比。
要添加一个元素,请将其提供给每个k个哈希函数以获得k个数组位置。将所有这些位置的位设置为 1。
设计k个不同的独立散列函数的要求对于较大的k来说是令人望而却步的。对于具有广泛输出的良好散列函数,这种散列的不同位域之间应该几乎没有相关性,因此这种类型的散列可用于通过将其输出切成多个位来生成多个“不同”散列函数字段。或者,可以将k个不同的初始值(例如 0、1、…、k - 1)传递给采用初始值的散列函数;或将这些值添加(或附加)到键中。对于较大的m和/或k,可以放宽散列函数之间的独立性,而误报率的增加可以忽略不计。
从这个简单的布隆过滤器中删除一个元素是不可能的,因为没有办法知道它映射到的k位中的哪一个应该被清除。尽管将这些k位中的任何一个设置为零就足以删除该元素,但它也会删除碰巧映射到该位上的任何其他元素。由于简单算法无法确定是否添加了影响要删除元素的位的任何其他元素,因此清除任何位将引入假阴性的可能性。
可以通过使用包含已删除项目的第二个 Bloom 过滤器来模拟从 Bloom 过滤器中一次性删除元素。然而,第二个过滤器中的误报在复合过滤器中变成了误报,这可能是不希望的。在这种方法中,重新添加以前删除的项目是不可能的,因为必须将其从“已删除”过滤器中删除。
从算法描述中可以得出 布隆过滤器存在一定的误判率同样布隆过滤器中的元素是不可以被删除的。
2. 布隆过滤器实际应用
2.1 在Java中使用布隆过滤器来进行邮件校验(简单实现)
import java.util.BitSet;/*** @ClassName Bloom* @Description TODO* @Author ZhangSan_Plus* @Date 2022/3/2 19:13* @Version 1.0**/
public class Bloom {private static final int SIZE = 1 << 24;BitSet bitSet = new BitSet(SIZE);Hash[] blHash = new Hash[8];private static final int seeds[] = new int[]{3, 5, 7, 9, 11, 13, 17, 19};public static void main(String[] args) {String email = "2633655104@qq.com";Bloom bloomDemo = new Bloom();System.out.println(email + "是否在列表中: " + bloomDemo.contains(email));bloomDemo.add(email);System.out.println(email + "是否在列表中: " + bloomDemo.contains(email));email = "2633655104@qq.com";System.out.println(email + "是否在列表中: " + bloomDemo.contains(email));}public Bloom() {for (int i = 0; i < seeds.length; i++) {blHash[i] = new Hash(seeds[i]);}}/*** 添加到Bloom Filter 中** @param str* @return void* @author ZhangSan_Plus* @description //TODO* @date 19:17 2022/3/2**/public void add(String str) {for (Hash hash : blHash) {bitSet.set(hash.getHash(str), true);}}/*** 判断是否存在** @param str* @return boolean* @author ZhangSan_Plus* @description //TODO* @date 19:16 2022/3/2**/public boolean contains(String str) {//假定存在boolean have = true;for (Hash hash : blHash) {have &= bitSet.get(hash.getHash(str));}return have;}class Hash {private int seed = 0;public Hash(int seed) {this.seed = seed;}public int getHash(String string) {int val = 0;int len = string.length();for (int i = 0; i < len; i++) {val = val * seed + string.charAt(i);}return val & (SIZE - 1);}}}
2.2 复杂布隆过滤器实现
package com.xccservice.utils;import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.BitSet;
import java.util.concurrent.atomic.AtomicInteger;public class BloomFilter implements Serializable {private static final long serialVersionUID = -5221305273707291280L;private final int[] seeds;private final int size;private final BitSet notebook;private final MisjudgmentRate rate;private final AtomicInteger useCount = new AtomicInteger(0);private final Double autoClearRate;/*** 默认中等程序的误判率:MisjudgmentRate.MIDDLE 以及不自动清空数据(性能会有少许提升)** @param dataCount 预期处理的数据规模,如预期用于处理1百万数据的查重,这里则填写1000000* @author ZhangSan_Plus*/public BloomFilter(int dataCount) {this(MisjudgmentRate.MIDDLE, dataCount, null);}/*** @param rate 一个枚举类型的误判率* @param dataCount 预期处理的数据规模,如预期用于处理1百万数据的查重,这里则填写1000000* @param autoClearRate 自动清空过滤器内部信息的使用比率,传null则表示不会自动清理,* 当过滤器使用率达到100%时,则无论传入什么数据,都会认为在数据已经存在了* 当希望过滤器使用率达到80%时自动清空重新使用,则传入0.8* @author ZhangSan_Plus*/public BloomFilter(MisjudgmentRate rate, int dataCount, Double autoClearRate) {long bitSize = rate.seeds.length * dataCount;if (bitSize < 0 || bitSize > Integer.MAX_VALUE) {throw new RuntimeException("位数太大溢出了,请降低误判率或者降低数据大小");}this.rate = rate;seeds = rate.seeds;size = (int) bitSize;notebook = new BitSet(size);this.autoClearRate = autoClearRate;}public void add(String data) {checkNeedClear();for (int i = 0; i < seeds.length; i++) {int index = hash(data, seeds[i]);setTrue(index);}}public boolean check(String data) {for (int i = 0; i < seeds.length; i++) {int index = hash(data, seeds[i]);if (!notebook.get(index)) {return false;}}return true;}/*** 如果不存在就进行记录并返回false,如果存在了就返回true** @param data* @return boolean* @author ZhangSan_Plus* @description //TODO* @date 19:22 2022/3/2**/public boolean addIfNotExist(String data) {checkNeedClear();int[] dataIndex = new int[seeds.length];// 先假定存在boolean exist = true;int index;for (int i = 0; i < seeds.length; i++) {dataIndex[i] = index = hash(data, seeds[i]);if (exist) {if (!notebook.get(index)) {// 只要有一个不存在,就可以认为整个字符串都是第一次出现的exist = false;// 补充之前的信息for (int j = 0; j <= i; j++) {setTrue(dataIndex[j]);}}} else {setTrue(index);}}return exist;}private void checkNeedClear() {if (autoClearRate != null) {if (getUseRate() >= autoClearRate) {synchronized (this) {if (getUseRate() >= autoClearRate) {notebook.clear();useCount.set(0);}}}}}public void setTrue(int index) {useCount.incrementAndGet();notebook.set(index, true);}private int hash(String data, int seeds) {char[] value = data.toCharArray();int hash = 0;if (value.length > 0) {for (int i = 0; i < value.length; i++) {hash = i * hash + value[i];}}hash = hash * seeds % size;// 防止溢出变成负数return Math.abs(hash);}public double getUseRate() {return (double) useCount.intValue() / (double) size;}public void saveFilterToFile(String path) {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path))) {oos.writeObject(this);} catch (Exception e) {throw new RuntimeException(e);}}public static BloomFilter readFilterFromFile(String path) {try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))) {return (BloomFilter) ois.readObject();} catch (Exception e) {throw new RuntimeException(e);}}/*** 清空过滤器中的记录信息*/public void clear() {useCount.set(0);notebook.clear();}public MisjudgmentRate getRate() {return rate;}/*** 分配的位数越多,误判率越低但是越占内存* <p>* 4个位误判率大概是0.14689159766308* <p>* 8个位误判率大概是0.02157714146322* <p>* 16个位误判率大概是0.00046557303372* <p>* 32个位误判率大概是0.00000021167340** @author lianghaohui*/public enum MisjudgmentRate {// 这里要选取质数,能很好的降低错误率/*** 每个字符串分配4个位*/VERY_SMALL(new int[]{2, 3, 5, 7}),/*** 每个字符串分配8个位*/SMALL(new int[]{2, 3, 5, 7, 11, 13, 17, 19}), ///*** 每个字符串分配16个位*/MIDDLE(new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53}), ///*** 每个字符串分配32个位*/HIGH(new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,101, 103, 107, 109, 113, 127, 131});private int[] seeds;private MisjudgmentRate(int[] seeds) {this.seeds = seeds;}public int[] getSeeds() {return seeds;}public void setSeeds(int[] seeds) {this.seeds = seeds;}}public static void main(String[] args) {BloomFilter bloomFilter = new BloomFilter(7);System.out.println(bloomFilter.addIfNotExist("1111111111111"));System.out.println(bloomFilter.check("1111111111111"));}
}
2.3 Redission 中的分布式布隆过滤器
不论是在 Guava 中,还是自己的简单实现,都只是本地的布隆过滤器,仅仅存在单个应用中,同步起来十分复杂,而且一旦应用重启,则之前添加的元素均丢失,对于分布式环境,可以利用 Redis 构建分布式布隆过滤器
Redisson 框架提供了布隆过滤器的实现
RBloomFilter<SomeObject> bloomFilter = redisson.getBloomFilter("sample");
// 初始化布隆过滤器,预计统计元素数量为55000000,期望误差率为0.03
bloomFilter.tryInit(55000000L, 0.03);
bloomFilter.add(new SomeObject("field1Value", "field2Value"));
bloomFilter.add(new SomeObject("field5Value", "field8Value"));
bloomFilter.contains(new SomeObject("field1Value", "field8Value"));
简单分析下 Redission 中源码
通用接口
public interface RBloomFilter<T> extends RExpirable {boolean add(T object)boolean contains(T object)boolean tryInit(long expectedInsertions, double falseProbability);
}
接口实现
public class RedissonBloomFilter<T> extends RedissonExpirable implements RBloomFilter<T> {public boolean add(T object) {// 构造多个哈希值long[] hashes = hash(object);while (true) {if (size == 0) {// 配置以哈希表的形式存在 Redis 中// 这里执行 HGETALLreadConfig();}int hashIterations = this.hashIterations;long size = this.size;// 需要设置为 1 的索引long[] indexes = hash(hashes[0], hashes[1], hashIterations, size);// 省略部分代码 ${新建客户端}// 依次执行 set 操作for (int i = 0; i < indexes.length; i++) {bs.setAsync(indexes[i]);}try {List<Boolean> result = (List<Boolean>) executorService.execute();for (Boolean val : result.subList(1, result.size()-1)) {if (!val) {return true;}}return false;} catch (RedisException e) {if (!e.getMessage().contains("Bloom filter config has been changed")) {throw e;}}}}
}
GET 函数这里就不再深入探讨,只是将 add 函数中的 SET 变成 GET 操作
Hash 函数
private long[] hash(Object object) {ByteBuf state = encode(object);try {return Hash.hash128(state);} finally {state.release();}
}
这个函数将 Object 编码后,返回一个 Byte 数组,然后调用 Hash.hash128 计算哈希值,这里的哈希算法是 HighwayHash
public static long[] hash128(ByteBuf objectState) {HighwayHash h = calcHash(objectState);return h.finalize128();
}protected static HighwayHash calcHash(ByteBuf objectState) {HighwayHash h = new HighwayHash(KEY);int i;int length = objectState.readableBytes();int offset = objectState.readerIndex();byte[] data = new byte[32];// 分区计算哈希for (i = 0; i + 32 <= length; i += 32) {objectState.getBytes(offset + i, data);h.updatePacket(data, 0);}if ((length & 31) != 0) {data = new byte[length & 31];objectState.getBytes(offset + i, data);h.updateRemainder(data, 0, length & 31);}return h;
}// 第二个哈希函数,计算最后的索引值
private long[] hash(long hash1, long hash2, int iterations, long size) {long[] indexes = new long[iterations];long hash = hash1;// 多次迭代for (int i = 0; i < iterations; i++) {indexes[i] = (hash & Long.MAX_VALUE) % size;// 根据迭代次数选择哈希值,累加if (i % 2 == 0) {hash += hash2;} else {hash += hash1;}}return indexes;
}
3. 使用布隆过滤器解决Redis缓存穿透
关于缓存穿透问题可以在之前写的博客如何应对缓存问题查看。解决缓存穿透问题可以使用缓存空对象和布隆过滤器两种方法,这里仅讨论布隆过滤器方法。
使用布隆过滤器逻辑如下:
- 根据 key 查询缓存,如果存在对应的值,直接返回;如果不存在则继续执行
- 根据 key 查询缓存在布隆过滤器的值,如果存在值,则说明该 key 不存在对应的值,直接返回空,如果不存在值,继续向下执行
- 查询 DB 对应的值,如果存在,则更新到缓存,并返回该值,如果不存在值,则更新到布隆过滤器中,并返回空
具体流程图如下所示:
public String getByKey(String key) {String value = get(key);if (StringUtils.isEmpty(value)) {logger.info("Redis 没命中 {}", key);if (bloomFilter.mightContain(key)) {logger.info("BloomFilter 命中 {}", key);return value;} else {if (mapDB.containsKey(key)) {logger.info("更新 Key {} 到 Redis", key);String valDB = mapDB.get(key);set(key, valDB);return valDB;} else {logger.info("更新 Key {} 到 BloomFilter", key);bloomFilter.put(key);return value;}}} else {logger.info("Redis 命中 {}", key);return value;}
}
解决Redis缓存穿透之布隆过滤器详解相关推荐
- 清空缓存的命令_布隆过滤器应用——解决Redis缓存穿透问题
1. 布隆过滤器 简要介绍布隆过滤器的概念和特点,详细知识请参考几篇参考文献或其它文章. 1.1 概念 简单点说,布隆过滤器本质是一个位数组. 当一个元素加入过滤器时,使用多个hash函数对元素求值, ...
- java雪崩_【并发编程】java 如何解决redis缓存穿透、缓存雪崩(高性能示例代码)...
[并发编程]java 如何解决redis缓存穿透.缓存雪崩(高性能示例代码) 发布时间:2018-11-22 16:48, 浏览次数:872 , 标签: java redis <>缓存穿透 ...
- 高并发架构系列:Redis缓存和MySQL数据一致性方案详解
需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景,主要 ...
- HBase的布隆过滤器详解
HBase的布隆过滤器详解 1.布隆过滤器的简单介绍 2.布隆过滤器的原理分析 2.1 哈希表存在的问题 2.2 布隆过滤器的原理 2.2.1 原理详解 2.2.2 布隆过滤器失误率的调节 2.2.3 ...
- Bloom Filter布隆过滤器(解决redis缓存穿透)
目录 1.什么是布隆过滤器: 2.用BitSet手写简单的布隆过滤器 3.redis中的缓存穿透 4.Redis中的布隆过滤器 4.1 RedisBloom 4.1.1直接编译进行安装 4.1.2使用 ...
- Redis系列教程(六):Redis缓存和MySQL数据一致性方案详解
需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景,主要 ...
- Redis 预防缓存穿透“神器” — 布隆过滤器
1. 布隆过滤器 1.1 概念 在架构设计时有一种最常见的设计被称为布隆过滤器,它可以有效减少缓存穿透的情况.其主旨是采用一个很长的二进制数组,通过一系列的 Hash 函数来确定该数据是否存在. 布隆 ...
- 解决redis缓存穿透、redis缓存雪崩问题
redis缓存雪崩 如果我们的缓存挂掉了,这意味着我们的全部请求都跑去数据库了. 数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕 ...
- 【redis】布隆过滤器详解
简介 本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),它实际上是一个很长的二进制向量和一系列随机映射函数. 布隆过滤器可以用于检索 ...
最新文章
- Placement new
- 《数学建模:基于R》一一2.1 回归分析
- eventfd和timerfd
- Apache Solr入门教程
- 2021年下信息系统项目管理师报考和考试时间
- html标签之img,input标签
- ​什么问题最让程序员头秃?我们分析了11种语言的11000个问题
- 高通人工智能应用创新大赛收官!9大奖项花落谁家?
- 利用gsoap工具,通过wsdl文件生成webservice的C++工程文件
- st语言 数组的常用方法_三菱ST语言教学(2)——数组的使用
- 一个新手学习python、pys60的感受
- 一个数根号3怎样用计算机计算,根号3等于多少怎么算
- PLC网关是什么 PLC网关是做什么的
- 蓝桥杯---史丰收速算
- kafka的offset理解
- 【学习笔记】Python_Faker,制造测试数据的第三方库,创建姓名、手机、电话、浏览器头、时间、地址等
- java满天星星代码_java实现满天星swingawt
- 我的2016—遇见自己,遇见未来
- css-超出内容省略号
- mysql药品库管理项目简介_MySQL数据库项目化教程简介,目录书摘