缓存穿透是指在使用缓存技术时,恶意或无效的请求无法从缓存中获取到数据,从而直接落到底层存储系统(如数据库)上,导致频繁地查询底层存储系统,增加系统负载并降低性能。
缓存通常用于存储经常被请求的数据,以提高系统的访问速度。但是,当恶意用户故意发送无效或不存在的请求时,缓存无法命中,导致每次请求都直接访问底层存储系统。如果这种情况持续发生,会对底层存储系统造成严重的压力,并使系统无法承受高负载。
缓解方式
布隆过滤器(Bloom Filter)
布隆过滤器(Bloom Filter)是一种概率型数据结构,用于快速判断某个元素是否属于一个集合,并具有高效的存储和查询性能。它通过使用位数组和多个哈希函数来实现。
布隆过滤器的核心是一个长度为 m 的位数组(bitmap)和 k 个独立的哈希函数。初始时,位数组中的所有位都被置为0。
当要将一个元素插入到布隆过滤器中时,该元素会依次经过 k 个哈希函数的运算,每个哈希函数都会计算出一个位数组的索引位置,将对应位数组位置置为1(即标记)。例如,哈希函数1计算得到索引位置为7,哈希函数2计算得到索引位置为12,那么位数组的第7和第12个位置都会被置为1。
当需要判断一个元素是否属于布隆过滤器时,同样会经过 k 个哈希函数的运算,检查对应索引位置的位数组值是否都为1。如果发现有任意一个位数组位置为0,则可以判定元素不属于布隆过滤器;如果所有位数组位置都为1,则只能说元素可能存在于布隆过滤器中,但并非绝对准确,存在一定的误判率。
布隆过滤器的误判率(false positive)取决于位数组的长度 m 和哈希函数的个数 k,同时也与插入的元素数量和布隆过滤器的设计有关。通过调整位数组长度和哈希函数个数,可以控制误判率的发生概率。
布隆过滤器的主要优点是占用空间小且查询效率高,在处理大规模数据集时,可以有效地降低底层存储系统的访问频率。然而,布隆过滤器也有一些限制,如不支持删除操作、存在一定的误判率和无法获取具体的误判元素等。
布隆过滤器在实际应用中常用于缓存系统、网络爬虫、数据库查询优化以及信息安全等领域,用于过滤掉明显不存在的数据,减少不必要的查询或处理开销,提高系统性能。
安装使用
在 Redis 中,可以使用 RedisBloom 模块来实现布隆过滤器的功能。RedisBloom 是一个开源的 Redis 模块,提供了多种布隆过滤器相关的功能。
要在 Redis 上使用布隆过滤器,需要先安装 RedisBloom 模块。可以通过以下步骤进行安装:
-
下载 RedisBloom 源代码:
git clone https://github.com/RedisBloom/RedisBloom.git
-
编译 RedisBloom 模块:
cd RedisBloom make
-
将编译生成的 RedisBloom.so 文件复制到 Redis 的模块目录:
cp redisbloom.so /path/to/redis/modules/
-
打开 Redis 配置文件 redis.conf,并在 "Modules" 部分添加以下配置:
loadmodule /path/to/redis/modules/redisbloom.so
-
重启 Redis 使配置生效。
安装完毕后,就可以使用 RedisBloom 提供的布隆过滤器功能了。以下是一些常用的 RedisBloom 命令:
-
BF.ADD:将元素插入到布隆过滤器中。
BF.ADD <key> <item>
-
BF.EXISTS:判断元素是否存在于布隆过滤器中。
BF.EXISTS <key> <item>
-
BF.MADD:批量插入多个元素到布隆过滤器中。
BF.MADD <key> <item1> [<item2> ... <itemN>]
-
BF.MEXISTS:批量判断多个元素是否存在于布隆过滤器中。
BF.MEXISTS <key> <item1> [<item2> ... <itemN>]
-
BF.INFO:获取布隆过滤器的基本信息,如误判率、元素数量等。
BF.INFO <key>
在上述命令中,<key>
代表布隆过滤器的键名,<item>
代表要插入或查询的元素。可以使用不同的键名创建多个独立的布隆过滤器。
需要注意的是,RedisBloom 的布隆过滤器在创建时需要指定预期的元素数量和误判率参数。通过调整这些参数,可以在满足需求的情况下控制内存占用和误判率。
布谷鸟过滤器(Cuckoo Filter)
布谷鸟过滤器(Cuckoo Filter)是一种基于哈希的概率数据结构,用于快速判断一个元素是否在集合中。它是布隆过滤器的一种变体,相比于传统的布隆过滤器,布谷鸟过滤器有较低的空间消耗并提供了更高的查询性能。
布谷鸟过滤器的原理如下:
-
布谷鸟过滤器由一个哈希表和一组哈希函数构成。
-
对于每个插入的元素,使用哈希函数对其进行多次哈希操作,然后将哈希结果存储在哈希表中。
-
当检查一个元素是否存在时,同样使用相同的哈希函数对其进行哈希操作,并查询哈希表中的对应位置。
-
如果所有哈希函数得到的位置都包含了此元素,那么插入时这个元素可能在集合中;如果有一个或多个位置为空,那么插入时这个元素一定不在集合中。
布谷鸟过滤器的优点是它不仅可以判断元素是否存在,还可以删除存在的元素。
安装使用
在 Redis 上使用布谷鸟过滤器,可以使用 RedisBloom 模块中的 CF(Cuckoo Filter)命令。以下是一些常用的 RedisBloom CF 命令:
-
CF.ADD:将元素插入到布谷鸟过滤器中。
CF.ADD <key> <item>
-
CF.EXISTS:判断元素是否存在于布谷鸟过滤器中。
CF.EXISTS <key> <item>
-
CF.DEL:从布谷鸟过滤器中删除指定元素。
CF.DEL <key> <item>
-
CF.INSERT:批量插入多个元素到布谷鸟过滤器中。
CF.INSERT <key> <item1> [<item2> ... <itemN>]
-
CF.EXISTS:批量判断多个元素是否存在于布谷鸟过滤器中。
CF.EXISTS <key> <item1> [<item2> ... <itemN>]
-
CF.INFO:获取布谷鸟过滤器的基本信息,如过滤器容量、元素数量等。
CF.INFO <key>
在上述命令中,<key>
代表布谷鸟过滤器的键名,<item>
代表要插入、查询或删除的元素。可以使用不同的键名创建多个独立的布谷鸟过滤器。
需要注意的是,布谷鸟过滤器在创建时需要指定容量参数。通过调整容量参数,可以在满足需求的情况下控制内存占用和查询性能。
与布隆对比
布隆过滤器(Bloom Filter)和布谷鸟过滤器(Cuckoo Filter)都是概率型数据结构,用于判断一个元素是否属于一个集合。它们之间存在一些区别:
-
存储结构:
-
布隆过滤器: 布隆过滤器通常由一个位数组和若干个哈希函数组成,位数组中的每个位表示一个元素的存在与否。
-
布谷鸟过滤器: 布谷鸟过滤器由一个哈希表和一组哈希函数构成,哈希表中存储了元素的哈希值。
-
-
内存消耗:
-
布隆过滤器: 布隆过滤器通过位数组来表示元素的存在与否,因此在存储空间上相对紧凑。
-
布谷鸟过滤器: 布谷鸟过滤器相比于布隆过滤器有较低的空间消耗,但一般会稍微多占用一些内存。
-
-
查找性能:
-
布隆过滤器: 布隆过滤器具有高效的查询性能,可以在常数时间内判断一个元素是否存在于集合中。但布隆过滤器在查询时存在一定的误判率(可能会误判某个元素存在)。
-
布谷鸟过滤器: 布谷鸟过滤器在查询性能上通常优于布隆过滤器,可以在常数时间内完成查询操作。而且布谷鸟过滤器对于存在于集合中的元素一定不会产生误判。
-
-
删除操作:
-
布隆过滤器: 布隆过滤器不支持直接删除元素,因为删除一个元素会影响其他元素的判断结果。如果需要删除元素,通常需要使用其他的技巧,或者重新构建一个新的布隆过滤器。
-
布谷鸟过滤器: 布谷鸟过滤器支持元素的删除操作,可以直接从过滤器中删除一个存在的元素。
-
总体而言,布隆过滤器适用于对存储空间有限制且对查询性能要求较高的场景,而布谷鸟过滤器则在一些空间相对宽裕的场景下提供了更好的性能和功能。选择使用哪种过滤器应根据具体的应用需求来决定。
缓存空对象(Cache Null Objects)
当缓存无法命中时,可以在缓存中存储一个特殊的空对象或占位符,表示该请求所对应的数据为空。这样,当再次发生相同的无效请求时,可以直接从缓存中获取到空对象,而不必访问底层存储系统。
实现
-
设定一个特殊的值,例如"NULL"或"EMPTY",用于表示空对象。
-
在获取对象时,先从Redis中查询对应的键值。
-
如果键存在且对应的值不等于特殊值,说明对象存在,直接返回该值。
-
如果键不存在或对应的值等于特殊值,说明对象为空,返回空结果。
-
当要缓存一个空对象时,将特殊值作为值存储到Redis中对应的键。
Example
require 'predis/autoload.php';
// 连接 Redis
$client = new Predis\Client();
// 获取对象
function get_object($key) {global $client;$value = $client->get($key);if ($value === null || $value === 'NULL') {return null; // 返回空对象} else {return $value; // 返回对象值}
}
// 缓存对象
function cache_object($key, $value) {global $client;if ($value === null) {$client->set($key, 'NULL');} else {$client->set($key, $value);}
}
// 示例使用
$object_key = 'my_object';
// 获取对象
$cached_object = get_object($object_key);
if ($cached_object === null) {echo "对象不存在或为空\n";
} else {echo "对象值: $cached_object\n";
}
// 缓存空对象
cache_object($object_key, null);
请求参数校验(Request Parameter Validation)
在应用层面对请求参数进行严格的校验,过滤掉无效或非法的请求。通过验证请求参数的有效性,可以在早期阶段抛弃掉无效请求,从而减轻底层存储系统的负载.
在 Redis 中,可以使用 Lua 脚本来实现请求参数的校验。使用 Lua 脚本可以在 Redis 服务器端执行一系列的操作,并返回结果给客户端,这样可以在单个原子操作中完成参数校验。
Example
lua-- 脚本参数:1-主键,2-请求参数 local key = KEYS[1] local requestParam = ARGV[1] -- 获取存储在 Redis 中的参数值 local storedParam = redis.call('GET', key) -- 进行参数校验 if requestParam == storedParam then-- 参数校验通过,返回成功结果return "OK" else-- 参数校验失败,返回错误结果return "ERROR" end
在 PHP 中,可以使用 Predis 库来执行 Lua 脚本。以下是一个示例代码:
phprequire 'predis/autoload.php';
// 连接 Redis
$client = new Predis\Client();
// 执行 Lua 脚本进行参数校验
function execute_validation_script($key, $requestParam) {global $client;
// 定义 Lua 脚本$script = "local key = KEYS[1]local requestParam = ARGV[1]
local storedParam = redis.call('GET', key)
if requestParam == storedParam thenreturn 'OK'elsereturn 'ERROR'end";
// 执行脚本$result = $client->eval($script, 1, $key, $requestParam);
return $result;
}
// 示例使用
$param_key = 'param_key';
$request_param = 'example_value';
// 执行参数校验脚本
$result = execute_validation_script($param_key, $request_param);
// 处理结果
if ($result === 'OK') {echo "参数校验通过\n";
} else {echo "参数校验失败\n";
}
缓存预热(Cache Pre-warming)
在系统启动或低负载期间,预先加载常用数据到缓存中,提前填充缓存。这样可以避免在高负载时期发生大量的缓存穿透,因为常用数据已经预先缓存,可以直接从缓存中获取,而不必访问底层存储系统。
实现
-
确定需要预热的数据:首先确定需要预热的数据集,可以是一些常用的热点数据、经常查询的数据,或者是应用程序启动时必须的数据。
-
编写预热脚本:编写一个脚本来加载数据到Redis缓存中。根据数据量的大小和加载过程的复杂程度,可以选择使用编程语言的Redis客户端(如PHP的Predis库)或直接使用Redis的命令行工具(redis-cli)。
-
预热数据到Redis缓存:在应用程序启动之前,调用预热脚本来将数据加载到Redis缓存中。可以通过循环遍历数据集并逐个添加到缓存中,或者使用Redis的批量操作命令(如MSET)来更高效地加载数据。
-
启动应用程序:在数据加载完成后,启动应用程序。此时应用程序可以直接从Redis缓存中获取数据,而无需从其他数据源(如数据库)中查询数据,提高了响应速度和性能。
require 'predis/autoload.php';
// 连接 Redis
$client = new Predis\Client();
// 预热数据到Redis缓存
function preload_data() {global $client;
// 遍历数据集,以键值对的形式添加到Redis缓存中$data = ['key1' => 'value1','key2' => 'value2',// 添加更多的键值对...];
foreach ($data as $key => $value) {$client->set($key, $value);}
echo "数据预热完成\n";
}
// 执行预热操作
preload_data();
// 在应用程序中使用缓存数据
$result = $client->get('key1');
if ($result === null) {echo "缓存中不存在该数据\n";
} else {echo "获取到缓存数据: $result\n";
}