Redis缓存穿透问题及解决方案

上周在工做中遇到了一个问题场景,即查询商品的配件信息时(商品:配件为1:N的关系),如若商品并未配置配件信息,则查数据库为空,且不会加入缓存,这就会致使,下次在查询一样商品的配件时,因为缓存未命中,则仍旧会查底层数据库,因此缓存就一直未起到应有的做用,当并发流量大时,会很容易把DB打垮。算法

缓存穿透问题

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,一般出于容错的考虑,若是从存储层查不到数据则不写入缓存层。
通常对于未命中的数据咱们是按照以下方式进行处理的:数据库

1.缓存层不命中。
2.存储层不命中,不将空结果写回缓存。
3.返回空结果。后端

/**
 * 缓存穿透问题:
 * 在数据库层没有查到数据,未存入缓存,
 * 则下次查询一样的数据时,还会查库。
 * 
 * @param id
 * @return
 */
private Object getObjectById(Integer id) {
    // 从缓存中获取数据
    Object cacheValue = cache.get(id);
    if (cacheValue != null) {
        return cacheValue;
    }
    // 从数据库中获取
    Object storageValue = storage.get(id);
    // 若是这里按照id查询DB为空,那么便会出现缓存穿透
    if (storageValue != null) {
        cache.set(id, storageValue);
    }
    return storageValue;
}

缓存穿透将致使不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
缓存穿透问题可能会使后端存储负载加大,因为不少后端存储不具有高并发性,甚至可能形成后端存储宕掉。数组

方案一:缓存空对象

/**
 * 缓存空对象:
 * 此种方式存在漏洞,不通过判断就直接将Null对象存入到缓存中,
 * 若是恶意制造不存在的id那么,缓存中的键值就会不少,恶意攻击时,极可能会被打爆,因此需设置较短的过时时间。
 *
 * @param id
 * @return
 */
public Object getObjectInclNullById(Integer id) {
    // 从缓存中获取数据
    Object cacheValue = cache.get(id);
    // 缓存为空
    if (cacheValue == null) {
        // 从数据库中获取
        Object storageValue = storage.get(key);
        // 缓存空对象
        cache.set(key, storageValue);
        // 若是存储数据为空,须要设置一个过时时间(300秒)
        if (storageValue == null) {
            // 必须设置过时时间,不然有被攻击的风险
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    }
    // 缓存不为空则直接返回
    return cacheValue;
}

缓存空对象会有一个必须考虑的问题:缓存

空值作了缓存,意味着缓存层中存了更多的键,须要更多的内存空间(若是是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过时时间,让其自动剔除。数据结构

方案二:布隆过滤器拦截

布隆过滤器介绍

概念:并发

布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它其实是一个很长的二进制向量和一系列随机映射函数。布隆过滤器能够用于检索一个元素是否在一个集合中。它的优势是空间效率和查询时间都远远超过通常的算法,缺点是有必定的误识别率和删除困难。运维

若是想判断一个元素是否是在一个集合里,通常想到的是将集合中全部元素保存起来,而后经过比较肯定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。可是随着集合中元素的增长,咱们须要的存储空间愈来愈大。同时检索速度也愈来愈慢,上述三种结构的检索时间复杂度分别为 O(n),O(log n),O(n/k)函数

布隆过滤器的原理是,当一个元素被加入集合时,经过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,咱们只要看看这些点是否是都是1就(大约)知道集合中有没有它了:若是这些点有任何一个0,则被检元素必定不在;若是都是1,则被检元素极可能在。这就是布隆过滤器的基本思想。高并发

示例:

google guava包下有对布隆过滤器的封装,BloomFilter。

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterTest {

    // 初始化一个可以容纳10000个元素且容错率为0.01布隆过滤器
    private static final BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000, 0.01);

    /**
     * 初始化布隆过滤器
     */
    private static void initLegalIdsBloomFilter() {
        // 初始化10000个合法Id并加入到过滤器中
        for (int legalId = 0; legalId < 10000; legalId++) {
            bloomFilter.put(legalId);
        }
    }

    /**
     * id是否合法有效,便是否在过滤器中
     *
     * @param id
     * @return
     */
    public static boolean validateIdInBloomFilter(Integer id) {
        return bloomFilter.mightContain(id);
    }

    public static void main(String[] args) {
        // 初始化过滤器
        initLegalIdsBloomFilter();
        // 误判个数
        int errorNum=0;
        // 验证从10000个非法id是否有效
        for (int id = 10000; id < 20000; id++) {
            if (validateIdInBloomFilter(id)){
                // 误判数
                errorNum++;
            }
        }
        System.out.println("judge error num is : " + errorNum);
    }
}

布隆过滤器拦截

设置过时时间,让其自动过时失效,这种在不少时候不是最佳的实践方案。

咱们能够提早将真实正确的商品Id,在添加完成以后便加入到过滤器当中,每次再进行查询时,先确认要查询的Id是否在过滤器当中,若是不在,则说明Id为非法Id,则不须要进行后续的查询步骤了。

/**
 * 防缓存穿透的:布隆过滤器
 * 
 * @param id
 * @return
 */
public Object getObjectByBloom(Integer id) {
    // 判断是否为合法id
    if (!bloomFilter.mightContain(id)) {
        // 非法id,则不容许继续查库
        return null;
    } else {
        // 从缓存中获取数据
        Object cacheValue = cache.get(id);
        // 缓存为空
        if (cacheValue == null) {
            // 从数据库中获取
            Object storageValue = storage.get(id);
            // 缓存空对象
            cache.set(id, storageValue);
        }
        return cacheValue;
    }
}

参考书籍:《Redis开发与运维》

相关文章
相关标签/搜索