通常情况下,被访问的数据即使不在缓存当中,请求也是可以访问数据库然后将结果回填到缓存中的。缓存“穿透”是指,访问的 key 并不存在(即业务系统中没有这个 key),之后请求“穿透”到数据库中,从数据库查询出来的是空值(NULL)。
用户访问一个不存在的 key,“穿透”到数据库中,数据库返回空值,并不会回填到缓存中,那么后续不管查询这个key多少次,都会缓存命中失败,直接“穿透”到数据库中。这样会严重影响数据库的性能。
如果是恶意破坏的请求,或者恶意地大量访问不存在的key,则会使系统的整体性能严重下降,并最终影响正常的用户请求。
1. 解决方案
大量的缓存“穿透”会对系统产生致命的影响,所以,对于缓存“穿透”需要重视起来。一般有以下两种解决方案。
(1)回种空值。
发生“穿透”的原因是,在数据库中根本不存在被访问的数据。这样无论访问多少次都获取不到这些数据,“穿透”会一直发生。
所以,在访问不存在的数据时,虽然第一次“穿透”到数据库获取的是空值(NULL),但可以用这个空值(NULL)回填缓存(称为“回种空值”)。
但是空值并不是真实的业务数据,如果有人恶意访问这些并不存在的 key,则会占用很大的内存空间,从而将正常的业务 key 给自动淘汰掉。所以,在将这些不存在 key 写入缓存时,可以加上一个很短的有效期以使其快速过期,伪代码如下:
Object nullValue = new Object();
try {
// 从数据库中查询数据
Object valueFromDB = selectFromDB(uid);
if (valueFromDB == null {
// 如果从数据库查询到空值,则把空值写入缓存,并设置较短的超时时间
redisCache.set(uid, nullValue, 10);
} else {
redis.Cache.set(uid, valueFromDB, 1000);
}
} catch Exception(e) {
redisCache.set(uid, nullValue, 10)
}
回种空值这种方案,会阻挡很大一部分的“穿透”请求。但是如果大批量地访问不存在的 key,则这种方案会造成缓存容量的紧张,甚至占满内存空间。
在具体实施过程中。如果要解决这种“穿透”,则需要额外增加很多的缓存节点,有点得不偿失,此时可以考虑使用第二种方案“实用布隆过滤器”。
(2)使用布隆过滤器
布隆过滤器用来检测一个元素是否在一个集合中。这种算法由一个二进制数组加上一个Hash算法组成。其基本原理如下:
数据结构: 布隆过滤器通常由一个位数组(Bit Array)和一组哈希函数组成。
位数组: 位数组是布隆过滤器的主要存储结构,它通常初始化为全部为0。每个元素在位数组中对应多个位,初始时都被设为0。
哈希函数: 布隆过滤器需要 k 个哈希函数。这些哈希函数可以将任意大小的数据映射到位数组的某个位置。对于每个元素,通过这 k 个哈希函数计算出 k 个哈希值,然后将对应的位数组位置设为1。
插入操作: 当向布隆过滤器中插入一个元素时,通过 k 个哈希函数计算出 k 个哈希值,并将对应的位数组位置设为1。
查询操作: 当查询一个元素是否存在于集合中时,同样通过 k 个哈希函数计算出 k 个哈希值,然后检查这 k 个位置的值。如果所有的位置都为1,则认为元素可能存在;如果有任何一个位置为0,则元素一定不存在。
由于哈希函数的存在,布隆过滤器有一定的误判率,即可能存在 "误判为存在" 的情况,但不存在 "误判为不存在" 的情况。
优点:
缺点:
应用场景: