上周在工做中遇到了一个问题场景,即查询商品的配件信息时(商品:配件为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开发与运维》