缓存失效之布隆过滤|青训营笔记
这是我参与「第五届青训营」伴学笔记创作活动的第21天
在后面会依次倒叙回顾之前的学习课程,便于复习~
一、重点内容
- 缓存失效
- 布隆过滤算法
二、缓存失效
都说redis好用,但亦有缺陷,今天学习的是关于缓存失效的两种典型现象~
-
缓存击穿
缓存击穿是指,针对某个热key,突然在缓存中失效,然后这些请求到热点数据的请求会都请求到数据库。 缓存击穿一般是热key 在 Redis 中过期了导致的。 最直接的方法就是,对于热 key ,就不设置过期时间,那么如何发现热key呢,目前字节内部是,采用中间件来进行发现,只要请求次数大于500,就可以识别为热key。
-
缓存穿透
缓存穿透指的是,数据既不在 Redis 中,也不在数据库中。每次请求 Redis 发现没有对应的 key之后,再去请求数据库,发现数据库也没有。 那么这时, Redis 就相当于一个摆设,没有具体的作用了。如恶意攻击系统,故意使用空值或者其他不存在的值进行频繁请求,那么也会对数据库造成比较大的压力。为了避免缓存穿透,可以: 1、缓存空值或缺省值
2、采用布隆过滤算法,提前判断是否有此数据。
三、布隆过滤
布隆过滤器实际上就是把 key 通过三次不同的哈希,计算出三个哈希值,然后在哈希表中把对应哈希值位置置为1。
当有新的请求过来时,先判断这个 key 经过N次哈希后,对应的哈希值位置是否为1,只要有一个不为1,就说明此 key 之前没有缓存过。
实际上,布隆过滤器也是有缺陷的,它不能完全保证请求过来的 key ,通过布隆过滤器的校验,就一定有这个数据。 但是,只要没有通过布隆过滤器的校验,那么这个 key 就一定不存在。 其实这样就已经可以过滤掉大部分不存在的 key 请求了。 正如以上提到的布隆过滤器缺陷,如果布隆过滤器的哈希槽过短,很有可能导致大部分的位置都为 1 ,那么此时,布隆过滤器就失去了它的意义。 所以,当我们发现布隆过滤器大部分位置都为1了,应该要扩宽哈希槽。
在实际业务中,我们对于请求的参数应该要先进行校验,请求的参数应该要在规定范围内。实际上,在工程应用中,主要也是依赖于参数的校验,过滤掉很多无效请求。
如果布隆过滤器失效了,或者说误判了,应该考虑reset或者扩展哈希槽。下面针对不同场景进行考虑,如果是考生注册与登录系统,具体做法是判断当前考生是否注册,通过布隆过滤器,判断出:
- 已注册,不一定已注册,需要进入数据库或者redis里面查询一次,如果出现误判,则需要进行扩展哈希槽,或者申请更大的哈希数组(位图),这时也需要rehash,建议是直接reset后再进行重建。
- 未注册,一定是未注册,直接去数据库CRUD。
还有一种用途是只查找,不插入,说白了就像是一层防护栏,一般用于恶意的用户登录,这也是用的最多的场景,一般是将用户的个人信息进行hash,然后存入位图中,用于检索当前的用户。
三、具体实现
#include <string>
#include <vector>
#include <type_traits>
using namespace std;
class BitMap
{
public:
//位图的内存大小和数据范围有关
BitMap(size_t range)
:_bit(range / 32 + 1)
{}
void set(const size_t num)
{
//计算数组中的下标
int idx = num / 32;
//计算num在对应下标整数中的下标位置
int bitIdx = num % 32;
//将对应的比特位置1
_bit[idx] |= 1 << bitIdx;
}
bool find(const size_t num)
{
int idx = num / 32;
int bitIdx = num % 32;
return (_bit[idx] >> bitIdx) & 1;
}
void reset()
{
_bit.assign(_bit.size(),0);//清除内容,但不回收空间,这里不能用 _bit.swap(vector<int>());
}
private:
vector<int> _bit;
};
template<class T>
struct HashFun1
{
//将字符串的每个字符通过计算得到一个hash值
//仅支持数字与字符串
size_t operator()(const T& val)
{
size_t hash = 0;
string str;
if( is_same<int,T>::value){
str = to_string(val);
}else{
str = val;
}
for (const auto& ch : str)
{
hash = hash * 131 + ch;
}
return hash;
}
};
template<class T>
struct HashFun2
{
size_t operator()(const T& val)
{
size_t hash = 0;
string str;
if(is_same<int,T>::value){
str = to_string(val);
}else{
str = val;
}
for (const auto& ch : str)
{
hash = hash * 65599 + ch;
}
return hash;
}
};
template<class T>
struct HashFun3
{
size_t operator()(const T& val)
{
size_t hash = 0;
string str;
if(is_same<int,T>::value){
str = to_string(val);
}else{
str = val;
}
for (const auto& ch : str)
{
hash = hash * 1313131 + ch;
}
return hash;
}
};
// TODO: 定义一个布隆过滤器类
// TODO:模板类,包含过滤器的元素类型、哈希函数类型
template<class T,class HashFun1,class HashFun2,class HashFun3>
class BloomFilter
{
public:
BloomFilter(const size_t num)
:_bit(5 * num)
, _bitTotCount(5 * num),
_bitCnt(0)
{}
void set(const T& val)
{
HashFun1 h1;
HashFun2 h2;
HashFun3 h3;
int idx1 = h1(val) % _bitTotCount;
int idx2 = h2(val) % _bitTotCount;
int idx3 = h3(val) % _bitTotCount;
_bit.set(idx1);
_bit.set(idx2);
_bit.set(idx3);
_bitCnt+=3;
}
bool find(const T& val)
{
HashFun1 h1;
HashFun2 h2;
HashFun3 h3;
int idx1 = h1(val) % _bitTotCount;
int idx2 = h2(val) % _bitTotCount;
int idx3 = h3(val) % _bitTotCount;
if (!_bit.find(idx1))
return false;
if (!_bit.find(idx2))
return false;
if (!_bit.find(idx3))
return false;
return true;//可能存在
}
// 误判率达到50%即可进行位图reset
bool FP(){
if (_bitCnt>_bitTotCount/2) return true;
return false;
}
void reset(){
_bit.reset();
_bitCnt = 0;
}
private:
BitMap _bit;//位图
size_t _bitTotCount;//位图的大小
size_t _bitCnt;//当前位图的元素数目
};