在现在互联网架构中,几乎每个互联网项目都会引入缓存系统,比如 redis、Memcached。来保护下游数据库和提升系统并发量。不管使用哪种缓存系统都有可能遇到 缓存穿透 的问题。
当然缓存系统是不可避免的,少量的缓存穿透对系统也没有损害,不可避免的原因有以下几点:
正常情况下的缓存穿透是没什么伤害的,但是如果你的系统遭遇攻击,存在大量的缓存穿透的话,那么可能就是一个麻烦了,如果大量的缓存穿透超过了后端服务器的承受能力,那么就有可能造成服务崩溃,这是不可接受的。
基于存在这种大量缓存穿透的可能性,所以我们就需要从根源上解决缓存穿透的问题,解决缓存穿透,目前一般有两种方案: 缓存空值和使用布隆过滤器 。
如果我们系统是遇到攻击的话,那么很有可能查询的值是伪造的,必然大概率不存在我们的系统中,这样无论查询多少次,在缓存中一直不存在,这样缓存穿透就一直存在。
在这种情况下,我们可以在缓存系统中缓存一个空值,防止穿透一直存在,但是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个比较短的过期时间,让空值在短时间之内能够快速过期淘汰。下面是一段伪代码:
Object nullValue = new Object();
try {
Object valueFromDB = getFromDB(uid); //从数据库中查询数据
if (valueFromDB == null) {
cache.set(uid, nullValue, 10); //如果从数据库中查询到空值,就把空值写入缓存,设置较短的超时时间
} else {
cache.set(uid, valueFromDB, 1000);
}
} catch(Exception e) {
cache.set(uid, nullValue, 10);
}
虽然这种方法可以解决缓存穿透的问题,但是这种方式也存在弊端, 因为在缓存系统中存了大量的空值,浪费缓存的存储空间,如果缓存空间被占满了,还会还会剔除掉一些已经被缓存的用户信息反而会造成缓存命中率的下降。
1970年布隆提出了一种布隆过滤器的算法,用来判断一个元素是否在一个集合中。布隆过滤器底层是一个超级大的 bit 数组,默认值都是 0 ,一个元素通过多个hash函数映射到这个 bit 数组上,并且将 0 改成 1。当然布隆过滤器也不需要我们实现,在 google 的 guava 包中有提供布隆过滤器,有兴趣的小伙伴可以研究研究。
布隆过滤器存在一定的误判,因为采用hash算法,就一定会存在哈希冲突,这样就可能造成不在数据库中的元素被判断在布隆过滤器中存在,但是 不在布隆过滤器中的元素一定不存在数据库中。
利用布隆过滤器的这个特点可以解决缓存穿透的问题, 在服务启动的时候先把数据的查询条件,例如数据的 ID 映射到布隆过滤器上,当然如果新增数据时,除了写入到数据库中之外,也需要将数据的ID存入到布隆过滤器中 。
我们在查询某条数据时,先判断这个查询的 ID 是否存在布隆过滤器中,如果不存在就直接返回空值,而不需要继续查询数据库和缓存,存在布隆过滤器中才继续查询数据库和缓存,这样就解决缓存穿透的问题。
当然布隆过滤器有缺陷,除了上面我们讲到过的存在一定的误判,还有一个就是 不支持删除。
缓存空值和使用布隆过滤器都可以在一定程度上解决缓存穿透的问题,各自有各自的优势,具体如何使用根据特定的场景舍取。