高并发下Redis缓存穿透解决方案与原理分析
1.为什么用redis 做缓存
目的:降低数据库的压力
redis 的特性:
存储大小
数据类型k-v
持久化,过期
分布式服务间 共享数据
2.缓存穿透
A 缓存在 数据库里存在
application --> 缓存存在 直接查缓存
B 缓存在 数据库里不存在 数据不一致(没更新缓存、key过期)
Application —> 缓存不存在,数据库存在 —>读取 database(加锁,高并发场景下只允许一个线程查询 )—> 写入缓存
C 缓存不存在 数据库里不存在
应用 查redis 没查到 去 database 没查到 返回空 一直查询此条件 导致 缓存穿透
要避免持续的查询在缓存和数据库都不存在的数据 (把空也更新到缓存 并加key过期时间,避免数据不同步)
攻击:一直查询不同的 缓存和数据库不存在的数据 怎么解决呢 ?
延伸面试题:
如何在海量的数据中 (例如 10亿 无序、不定长、不重复)快速判断一个元素是否存在?
直接放内存 10亿数据 无论放哪种 存储类型 都需要几个G的 内存
最简单的数据结构 0 1 (位、图 )其实就是有序的数据
如何 用 01 存储数据
映射 1.固定长度的输出 2.均匀
Hash 算法 位运算做映射
(Mic 和 Tom两个数据算法得位置一样就产生了 Hash碰撞:)
如何降低Hash碰撞的概率?
1. 扩大位数组的容量(缺点消耗内存)
2. 增加hash函数的个数例如增加筛选条件(缺点 降低存取hash计算的效率)
如何中和 1.2 的问题呢 —>
1970 布隆发表了 布隆过滤器
布隆过滤器的原理
本质(1.位数组(二进制向量)2.一系列随机映射函数)
那上图中 d 在 三个位置都是1一定确定 d存在吗? 不,还是会有hash碰撞 ( 布隆过滤器 存在误判 — 假阳性)
那上图中 e 在三个位置有一个0 ,那e 一定不存在
如果布隆过滤器判断元素在集合中存在—> 不一定存在
如果布隆过滤器判断元素判断不存在—> 一定不存在
所以 如果元素实际存在,布隆过滤器一定判断存在
如果元素实际不存在,布隆过滤器可能判断存在
基于以上原理,我们可以解决缓存穿透的问题 (布隆过滤器判断元素不存在 则一定不存在)
Guavo — 谷歌 封装了 字符串、集合、缓存、限流、二维码 的工具包
国内叫 Hutool
Guavo 里面有个 Blomn Filter 布隆过滤器
使用:a.引入 guava 依赖
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
b. 创建过滤器 测试过滤器的误判率
public class BloomFilterDemo {
private static final int insertions = 1000000;
public static void main(String[] args) {
//初始化一个存储String数据的布隆过滤器,初始化大小为 100w
//默认误判率是 0.03
BloomFilter<String> bf = BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8),insertions,0.0001);
//用于存放所有实际存在的key,判断key是否存在
Set<String> sets = new HashSet<>(insertions);
//用于存放所有实际存在的key,可以取出使用
List<String> lists = new ArrayList<>(insertions);
//向三个容器初始化100w 个随机并且唯一的字符串
for (int i = 0; i < insertions ; i++) {
String uuid = UUID.randomUUID().toString();
bf.put(uuid);
sets.add(uuid);
lists.add(uuid);
}
//判断正确的次数
int right = 0;
//判断错误的次数
int wrong = 0;
for (int i = 0; i < 10000; i++) {
//可以被100整除的时候,取一个存在的数,否则随机生成一个uuid
// 0-10000 之间,可以背100 整除的数有100个
String data = i%100 == 0 ? lists.get(i/100):UUID.randomUUID().toString();
//mightContain (可能存在)
if (bf.mightContain(data)) {
if (sets.contains(data)) {
//判断存在时命中
right ++;
continue;
}
//判断不存在时,错误
wrong ++;
}
}
//计算存在的判断正确 命中率
NumberFormat percentFormat = NumberFormat.getPercentInstance();
//最大小数位数
percentFormat.setMaximumFractionDigits(2);
float percent = (float)wrong/9900;
float bingo = (float)(9900-wrong)/9900;
System.out.println("在100w个元素中,判断100个实际存在的元素,布隆过滤器认为存在的"+right);
System.out.println("在100w个元素中,判断9900个实际不存在的元素,布隆过滤器误认为存在的"+wrong+",命中率:"+percentFormat.format(bingo)+",误判率:"+percentFormat.format(percent));
}
布隆过滤器的地位,数据库的数据加载到布隆过滤器,在访问redis之前
把数据库的所有数据加载到 布隆过滤器里,访问redis缓存之前先查询布隆过滤器,如果不存在则直接返回,这样避免了缓存穿透
为什么布隆过滤器里面没有提供删除方法,因为有hash碰撞