Redis—BloomFilter 预防缓存击穿的一种数据结构
写在前面
几天前,小付在复习redis的时候,再一次看到了BloomFilter,此时脑中突然出现一撮迷人的小胡子,手拿一面盾牌的大叔朝着我喊了句:站在布隆后面的画面~
此时我跳出画面,正儿八经的直视眼前的这个东西,就有了下面的故事…
BloomFilter 是什么
BloomFilter 的概念——百度百科
Bloom filter是由Howard Bloom在1970年提出的二进制向量数据结构,它具有空间和时间效率
,被用来检测一个元素是不是集合中的一个成员
。
简介
如果检测结果为是
,该元素不一定在集合中
;但如果检测结果为否
,该元素一定不在集合中
。因此Bloom filter具有100%的召回率。这样每个检测请求返回有“在集合内(可能错误)”和“不在集合内(绝对不在集合内)”两种情况,可见 Bloom filter 是牺牲了正确率和时间
以节省空间
。
BloomFilter的原理
BloomFilter 的数据结构
在小付的眼里来说,BloomFilter就是一个位图与hash函数巧妙结合用来应对于redis中预防缓存被击穿的情况而设计出来的一种数据结构。它利用时间以及正确率(也就是后面会提到与之对应的误判率)来节省空间的神奇东东。
那它是如何做到如此精妙的呢?它的原理是什么呢?
当一个元素要被加入到集合当中时,通过n次hash函数计算出n个映像位点,并把当前所处位图此点的值置为1。然后依次进行搜索这几个映像位点是否都是1,如果都是1的话,说明该元素可能存在于当前集合之中,如果但凡出现一个0,都说明此元素不处于当前元素中,直接返回false。
说到了BloomFilter是预防缓存穿透的一种数据结构,那么缓存穿透是什么呢?
emmm,说道缓存穿透就又不得不提到导致缓存宕机的三兄弟了——缓存击穿
,缓存穿透
,缓存雪崩
了。
那咱们先来回答问题:
缓存穿透:透过字面理解、穿透、贯穿之意,也就说明了缓存与数据库中都没有数据
,此时用户不断向数据库发送请求,一个完全没有的数据,却要花数据库不少的时间进行处理,如果这个请求达到高峰,导致数据库查询压力过大,那么此时数据库就可能会直接被打掉线。
缓存击穿:击穿、说白了就是击穿掉了缓存,让缓存形同虚设
,也就好比其实开发中,数据库中存在这个数据,但是缓存中没有这个数据的时候
,或者是最为一个热点数据,当热点数据的key在缓存中即将过期时,高并发进行访问请求,这无疑就是穿破缓存、直接打在你肉上的意思
,严重情况下一样可以打死数据库。
缓存雪崩:说道雪崩就不得不提到一句话,“雪崩来临时,没有一片雪花是无辜的”
,这里咱们就来举例说明缓存雪崩:雪花就是用户请求数据,而大部分热点数据请求就比作狂风暴雪来临时,这就好理解了塞,当这些热点数据忍不住了要进行请求时(热点数据即将同一时期过期),突然一下落下
(一下全部过期),欧后~发生了雪崩(缓存大面积失效
,所有请求直接打在数据库脸上,问你疼不疼一样),要是我是数据库的话我直接si
给你看,而你还不信邪重启就能解决问题,这时候大量数据又来了,你再给我si
一次。
咱们言归正传,小伙子刚才讲的不错诶,来说说BloomFilter实现
啊?BloomFilter的实现 ,看来还是逃不过手撕BloomFilter了~
实现BloomFilter就不得不提及hash函数的选取执行次数和bitmap的数组大小。
当然我们在真实使用的时候,肯定是知道当前要存取的数据为n,期望其误判率(就是bitmap中均为1时,该元素可能存在于此集合出现的差错),然后计算其最佳BITMAP数组大小m[位图最佳大小] = (-n[插入数据量] * ln(p[误判率]))/(ln2)^2
,与hash函数的选取k = m[位图的最佳大小] /n[插入的数据链] * (ln2)
通过BloomFilter类查看源码,知其然并且知其所以然:
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions, double fpp) {
return create(funnel, (long)expectedInsertions, fpp);
}
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions, double fpp) {
return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
}
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions) {
return create(funnel, (long)expectedInsertions);
}
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
return create(funnel, expectedInsertions, 0.03D);
}
看到如上的四个BloomFilter中的create方法,总结归纳一下,你会发现形参中有共同之处:
-
funnel: 和泛型挂钩 数据类型
-
expectedInsertions 期望插入的数据量
-
fpp 误判率
-
strategy:采取哪种hash算法来计算
然后源码中:又给出了如图的计算代码
@VisibleForTesting
static int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int)Math.round((double)m / (double)n * Math.log(2.0D)));
}
@VisibleForTesting
static long optimalNumOfBits(long n, double p) {
if (p == 0.0D) {
p = 4.9E-324D;
}
return (long)((double)(-n) * Math.log(p) / (Math.log(2.0D) * Math.log(2.0D)));
}
计算最佳的bitmap数组大小以及计算最佳的hash函数的使用次数。
以上都弄得差不多了,咱们就差一个 使用何种hash算法来进行bitmap数组映射了
所以我又跑源码中找了找:果不其然 ,全都有
在一个叫做BloomFilterStrategies
的枚举类型中他给我们提供了很多的hash算法,博主这里采取的时最简单的hash算法。
具体如何进行计算的就需要小伙伴们自行去研究啦~
代码实现
/**
* 布隆过滤器的核心类
*
* @author Alascanfu
*/
public class BloomFilterUtil<T> {
private int numHashFunction;
private int bitSize;
@SuppressWarnings("all")
private Funnel funnel;
@SuppressWarnings("all")
public BloomFilterUtil(long expectInsertions) {
funnel = Funnels.stringFunnel(Charset.defaultCharset());
bitSize = optimalNumOfBitSize(expectInsertions, 0.03);
numHashFunction = optimalNumOfHashFunction(bitSize, expectInsertions);
}
@SuppressWarnings("all")
public BloomFilterUtil(long expectInsertions, double falsePositive, Funnel funnel) {
this.funnel = funnel;
bitSize = optimalNumOfBitSize(expectInsertions, falsePositive);
numHashFunction = optimalNumOfHashFunction(bitSize, expectInsertions);
}
/**
* 源码分析中可以看到这个哈希算法 又
* strategy 哈希算法
* @param value
* @return
*/
@SuppressWarnings("all")
public int[] murmurHashOffset(T value) {
int[] offset = new int[numHashFunction];
long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunction; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
offset[i - 1] = nextHash % bitSize;
}
return offset;
}
/**
* 获取最佳的位图数组大小
* @param expectInsertions 期望插入多少数据
* @param falsePositive 误判率
* @return 位图数组大小
* 经过计算 公式 m[位图最佳大小] = (-n[插入数据量] * ln(p[误判率]))/(ln2)^2
*/
public int optimalNumOfBitSize(long expectInsertions, double falsePositive) {
return (int) (((-expectInsertions) * Math.log(falsePositive)) / (Math.log(2) * Math.log(2)));
}
/**
* 获取最佳执行哈希函数的次数
* @param expectInsertions 期望插入多少数据
* @param bitSize 位图数组大小
* @return 哈希函数执行的最佳次数
*/
public int optimalNumOfHashFunction(long bitSize, long expectInsertions) {
return (int) Math.max(1, (double) bitSize/expectInsertions * Math.log(2));
}
}
这里就完成好了一个简易的BloomFilter了,实现都实现完了,今天我觉得我也差不多了。
最后再给大家写了一个比较简单的封装工具类作为BloomFilter的缓存盾牌,确实印证了那句话,站在布隆后面~
因为就是比较简单的增删改查操作,以及值的判定是否存在于内存当中~
读者们可以放心食用,进行测试与学习~
/**
* redis操作布隆过滤器
*
* @param <T>
* @author Alascanfu
*/
public class RedisBloomFilter<T> {
@Autowired
private RedisTemplate redisTemplate;
/**
* 删除缓存的KEY
*
* @param key KEY
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 根据给定的布隆过滤器添加值,在添加一个元素的时候使用,批量添加的性能差
*
* @param bloomFilterHelper 布隆过滤器对象
* @param key KEY
* @param value 值
* @param <T> 泛型,可以传入任何类型的value
*/
public <T> void add(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
redisTemplate.opsForValue().setBit(key, i, true);
}
}
/**
* 根据给定的布隆过滤器添加值,在添加一批元素的时候使用,批量添加的性能好,使用pipeline方式(如果是集群下,请使用优化后RedisPipeline的操作)
*
* @param bloomFilterHelper 布隆过滤器对象
* @param key KEY
* @param valueList 值,列表
* @param <T> 泛型,可以传入任何类型的value
*/
public <T> void addList(BloomFilterHelper<T> bloomFilterHelper, String key, List<T> valueList) {
redisTemplate.executePipelined(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
connection.openPipeline();
for (T value : valueList) {
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
connection.setBit(key.getBytes(), i, true);
}
}
return null;
}
});
}
/**
* 根据给定的布隆过滤器判断值是否存在
*
* @param bloomFilterHelper 布隆过滤器对象
* @param key KEY
* @param value 值
* @param <T> 泛型,可以传入任何类型的value
* @return 是否存在
*/
public <T> boolean contains(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
return true;
}
}
大功告成! 博主后续会更新redis的相关部分哦 ~ 因为最近在复习这一板块
感谢
非常感谢
熬丙三太子
博文《Redis-避免缓存穿透的利器之BloomFilter》:链接地址。
给了作者很多的启发与帮助 以及 他对redis不菲的见解
同时 也感谢 正在阅读此篇博文的你
学无止境 每天学一点 每天收获一点
博主在这里 祝愿 诸君能 事业有成 学有所得
写在最后
最后本文共计3k字
希望读者们慢慢食用
加油!致敬成长道路上的你和我~
如果可以 请记得 动动小手 一键三连