标题:Redis的布隆过滤器与缓存穿透防护
理解问题:什么是缓存穿透?
理解 缓存穿透的定义。当应用系统请求一个在缓存(如Redis)和数据库中都不存在的数据时,这个请求会直接“穿透”缓存层,每次都打到数据库。如果这种请求量很大,会对数据库造成巨大压力,甚至导致服务崩溃。
想象 这个场景:系统缓存了热门商品信息,但恶意用户或程序不断用不存在的商品ID(如 999999)发起查询。缓存查不到,所有请求全部转发至数据库,数据库反复查询后发现数据不存在,返回空结果,且这个结果也不会被缓存(因为缓存空值有额外开销和时效问题)。
认识 其核心是:缓存对“不存在”的数据束手无策。
解决方案:布隆过滤器(Bloom Filter)
引入 一个高效的概率型数据结构——布隆过滤器。它能快速判断一个元素是否“可能存在于”一个集合中,或“肯定不存在”于集合中。
理解 其工作原理:
- 初始化:创建一个全为0的位数组(比如长度为m),并确定k个不同的哈希函数。
- 添加元素:当向过滤器中添加一个元素时,使用这k个哈希函数分别对该元素进行计算,得到k个索引位置,并将位数组中这些位置的值全部置为1。
- 查询元素:当查询一个元素是否存在时,同样使用这k个哈希函数计算索引,并检查位数组中这些位置是否全部为1。
- 如果有任何一个位置是0,则该元素肯定不存在。
- 如果所有位置都是1,则该元素可能存在(因为有“误判”的可能,即不同的元素经过哈希计算后可能恰好都映射到了相同的1位上)。
应用 到缓存穿透防护:在查询数据库之前,先用布隆过滤器判断这个数据ID是否可能存在。如果过滤器判断“肯定不存在”,则直接返回空结果,阻止请求到达数据库。
实战:在Redis中使用布隆过滤器
Redis通过模块的形式提供了布隆过滤器功能,最常见的模块是 RedisBloom。
第一步:确保加载了RedisBloom模块。
检查 你的Redis服务是否已加载该模块。在 redis-cli 中执行以下命令:
redis-cli> MODULE LIST
如果输出中包含 bf,则表示模块已加载。否则,你需要先安装并配置Redis加载此模块。
第二步:创建布隆过滤器。
使用 BF.RESERVE 命令创建一个新的过滤器。你需要预设两个关键参数:
错误率(error rate):允许的误判概率,例如0.001(0.1%)。容量(capacity):预计存储的元素数量。
# 创建一个错误率为0.001,预计容量为100万的商品ID过滤器
redis-cli> BF.RESERVE product:bloom_filter 0.001 1000000
第三步:向过滤器中添加元素。
使用 BF.ADD 命令添加单个元素,或 BF.MADD 命令添加多个元素。
# 添加单个商品ID
redis-cli> BF.ADD product:bloom_filter "100001"
# 批量添加多个商品ID
redis-cli> BF.MADD product:bloom_filter "100002" "100003" "100004"
第四步:在查询缓存前,用过滤器进行检查。
编写 业务逻辑。在典型的查询流程中集成布隆过滤器检查。下面用伪代码描述:
def get_product_info(product_id):
# 1. 先查询缓存
cache_key = f"product:{product_id}"
cached_data = redis.get(cache_key)
if cached_data:
return cached_data
# 2. 查询布隆过滤器,判断ID是否可能存在于数据库
# 使用 BF.EXISTS 命令检查
is_probably_exist = redis.execute_command("BF.EXISTS", "product:bloom_filter", product_id)
if not is_probably_exist:
# 3. 布隆过滤器判断“肯定不存在”,直接返回,避免查询数据库
return None
# 4. 布隆过滤器判断“可能存在”,才去查询数据库
db_data = database.query(f"SELECT * FROM products WHERE id = {product_id}")
if db_data:
# 5. 将数据存入缓存,并设置过期时间
redis.setex(cache_key, 3600, db_data)
return db_data
关键考量与最佳实践
理解 布隆过滤器的核心特性:它只能告诉你“可能存在”或“肯定不存在”。使用它必须接受一定的“假阳性”(False Positive)误判,即把不存在的元素误判为存在。这种情况下,请求还是会到达数据库,但相比缓存穿透,概率已大大降低。
计算 误判率与空间、哈希函数数量的关系。以下公式描述了在最优情况下(给定容量n,位数组大小m,哈希函数数量k),误判率 $p$ 的近似值:
$$
p \approx (1 - e^{-kn/m})^k
$$
其中,$e$ 是自然对数的底。为了达到预期的错误率,在设计过滤器时,需要根据预计元素数量 n 和目标错误率 p,来估算需要的位数组大小 m 和哈希函数数量 k。BF.RESERVE 命令会自动完成这些计算。
规划 过滤器的生命周期。
- 元素只增不删:标准的布隆过滤器不支持删除元素。如果业务数据会物理删除,你需要定期用新数据重建过滤器(例如,每天凌晨用全量有效的商品ID集合重建一个新的
product:bloom_filter_v2,然后进行热切换)。 - 容量预估:务必准确预估数据规模(
capacity)。如果实际元素数量远超预设容量,误判率会急剧上升,失去防护意义。
选择 合适的错误率。
- 错误率设置得越低(如
0.00001),需要的存储空间越大,哈希函数也越多,查询性能会略有下降。 - 对于缓存穿透防护,通常
0.001(0.1%) 到0.01(1%) 是一个常见的权衡范围。这意味着每1000到100次误判,只有一次会放行请求到数据库,已能有效抵御大规模的恶意攻击。
进阶:使用Redisson客户端(Java示例)
使用 更高级的客户端库可以简化操作。以Java的Redisson为例,它提供了与业务对象更自然的集成方式。
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("product:bloom_filter");
// 初始化:预计元素数量100万,误判率0.001
bloomFilter.tryInit(1000000L, 0.001);
// 添加数据
bloomFilter.add("100001");
// 在业务逻辑中检查
String productId = "999999";
if (bloomFilter.contains(productId)) {
// 可能存在,继续后续流程(查缓存/DB)
} else {
// 肯定不存在,直接返回
return null;
}
注意:Redisson的 BloomFilter 接口底层依然是对Redis命令的封装,其核心原理与直接使用 BF.* 命令一致。使用客户端库主要提升了开发的便捷性和代码可读性。

暂无评论,快来抢沙发吧!