开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情
每日英语:
A friend is someone who gives you total freedom to be yourself.
朋友是让你能完全做自己的人。 -吉姆·莫里森
缓存灾难解决
缓存虽然能大幅提升系统数据处理效率,但也存在一些潜在风险。
缓存穿透:
缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。
缓存击穿:
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
缓存雪崩:
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
布隆过滤器
布隆过滤器(Bloom Filter)是一种空间效率高的概率型数据结构,它专门用来检测集合中是否存在特定的元素。
布隆过滤器采用了散列表(又叫哈希表,Hash table)的数据结构,它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit Array)中的一个点。这样一来,我们只要看看这个点是不是 1 就知道可以集合中有没有它了。这就是布隆过滤器的基本思想。
常用场景:
1:垃圾邮件检测
2:爬虫去重
3:缓存穿透
优点:
1:不需要存储数据本身,只用比特表示,因此空间占用相对于传统方式有巨大的优势,并且能够保密数据;
2:时间效率较高;
缺点:
1:存在假阳性的概率,不适用于任何要求100%准确率的情境;
2:只能插入和查询元素,不能删除元素;
Guava布隆过滤器
google的guava工具包中就提供了布隆过滤器,而且是当前主流的布隆过滤器之一,我们可以直接在项目中使用它。我们来学习一下它的使用:
1)引入依赖
<!--google的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
2)编写测试
public class BloomFilterTest {
private static int size = 1000000;
//Google的布隆过滤器
private static BloomFilter<Integer> bloomFilter =BloomFilter.create(Funnels.integerFunnel(), size);
public static void main(String[] args) {
//放一百万个key到布隆过滤器中
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
List<Integer> list = new ArrayList<Integer>(1000);
//取10000个不在过滤器里的值,看看有多少个会被认为在过滤器里
for (int i = size + 10000; i < size + 20000; i++) {
if (bloomFilter.mightContain(i)) {
list.add(i);
}
}
System.out.println("误判的数量:" + list.size());
}
}
单机更适合使用guava布隆过滤器,但如果是在分布式应用中,我们推荐使用Redis的布隆过滤器,可以通过Redis打破跨服务通信。
Redis布隆过滤器
Redis 实现布隆过滤器的底层就是通过 bitmap 这种数据结构,当前使用bitmap比较好用的一个客户端工具——Redisson,关于Redisson前面我们已经学习过了,我们接下来演示基于Redisson实现布隆过滤器的操作实战。
功能说明
我们系统中需要对权限进行校验,我们可以把权限url存入到bitmap中,每次进行权限校验之前,可以先看看布隆过滤器中是否存在用户请求的路径,如果存在则进行权限校验,如果不存在,直接拒绝用户请求,这样可以大幅降低无效请求。
路径初始化
在mall-permission-service中引入依赖:
<!--Redisson分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
在mall-permission-service中创建Redisson的锁操作对象:
@Configuration
public class RedissLock {
/***
* 创建RedissonClient对象
* 创建锁、解锁
* @return
*/
@Bean
public RedissonClient redissonClient(){
//创建Config
Config config = new Config();
//集群实现
config.useClusterServers()
.setScanInterval(2000)
.addNodeAddress(
"redis://192.168.100.130:7001",
"redis://192.168.100.130:7002",
"redis://192.168.100.130:7003",
"redis://192.168.100.130:7004",
"redis://192.168.100.130:7005",
"redis://192.168.100.130:7006");
//创建RedissonClient
return Redisson.create(config);
}
}
在com.xz.mall.permission.init.InitPermission中实现路径初始化:
@Autowired
private RedissonClient redissonClient;
/***
* 权限初始化加载
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
//...略;
//uri初始化到布隆过滤器中
RBloomFilter<String> filters = redissonClient.getBloomFilter("UriBloomFilterArray");
//初始化数组长度以及误判概率
filters.tryInit(10000L,0.001);
//这里只演示完全匹配,通配符匹配还需要额外处理
for (Permission permission : permissionMatch0) {
filters.add(permission.getUrl());
}
}
此时我们启动服务初始化数据如下:
无效路径过滤
在mall-api-gateway中引入依赖:
<!--Redisson分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
在mall-api-gateway中创建Redisson的锁操作对象:
@Configuration
public class RedissLock {
/***
* 创建RedissonClient对象
* 创建锁、解锁
* @return
*/
@Bean
public RedissonClient redissonClient(){
//创建Config
Config config = new Config();
//集群实现
config.useClusterServers()
.setScanInterval(2000)
.addNodeAddress(
"redis://192.168.100.130:7001",
"redis://192.168.100.130:7002",
"redis://192.168.100.130:7003",
"redis://192.168.100.130:7004",
"redis://192.168.100.130:7005",
"redis://192.168.100.130:7006");
//创建RedissonClient
return Redisson.create(config);
}
}
在com.xz.mall.api.permission.AuthorizationIntterceptor中创建方法判断当前uri是否存在,代码如下:
@Autowired
private RedissonClient redissonClient;
/***
* 无效路径过滤
*/
public Boolean isInvalid(String uri){
//获取数组对象
RBloomFilter<Object> bloomFilterArray = redissonClient.getBloomFilter("UriBloomFilterArray");
return bloomFilterArray.contains(uri);
}
在com.xz.mall.api.filter.ApiFilter执行调用,代码如下:
此时我们访问一个不存在的url = http://192.168.1.104:9001/mall/cart,此时会报出如下错误:
思考:无效路径过滤如果迁移到Nginx中,效率是否更高?该怎么做?
如果将无效路径迁移到Nginx中,效率一定更高,甚至高出几百倍,可以采用Lua+nginx实现。
总结
本篇主要介绍了一下缓存灾难解决方案,首先解释一下缓存的三大问题,缓存穿透、缓存击穿、缓存雪崩。 接下来介绍了布隆过滤器,Guava布隆过滤器,Redis布隆过滤器。