什么是缓存穿透?
缓存的穿透是指当查询一个不存在的数据时,由于缓存中没有该数据,导致查询透过缓存直接查数据库,从而增加数据库的负担。通常解决缓存穿透的方法是对于查询不到的数据也将其缓存起来,并设置一个较短的过期时间。
缓存穿透的解决方案
一、Redis缓存穿透可以通过将查询不到的数据也加入缓存,并设置一个较短的过期时间来解决。
二、还可以使用布隆过滤器来过滤掉查询不存在数据的请求,从而避免直接透过缓存查询数据库。
什么是布隆过滤器呢?
布隆过滤器实际上是由一个超长的二进制位数组和一系列的哈希函数组成。二进制位数组初始全部为0,当给定一个待查询的元素时,这个元素会被一系列哈希函数计算映射出一系列的值,所有的值在位数组的偏移量处置为1。
因此,布隆过滤器只会判断某个元素可能存在,而不是肯定存在。如果需要更高的准确性,可以选择使用精确匹配的算法,如哈希表等。但是,相对于布隆过滤器,这些算法需要更多的内存和计算资源。
在实际应用中,布隆过滤器一般用于快速判断某个元素是否存在于某个集合中,比如在缓存系统中过滤掉不存在的数据请求,避免直接透过缓存查询数据库。
Redis布隆过滤器的实现可以使用Redis的位图数据结构。在Redis中,位图是由一系列二进制位组成的数据结构,它可以用来表示某个元素是否存在于某个集合中。
缺点:
布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。
另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面。这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
自己造个轮子,布隆过滤器模拟实现
public class MyBloomFilter {
/**
* 一个长度为10 亿的比特位
*/
private static final int DEFAULT_SIZE = 256 << 22;
/**
* 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
*/
private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};
/**
* 相当于构建 8 个不同的hash算法
*/
private static HashFunction[] functions = new HashFunction[seeds.length];
/**
* 初始化布隆过滤器的 bitmap
*/
private static BitSet bitset = new BitSet(DEFAULT_SIZE);
/**
* 添加数据
*
* @param value 需要加入的值
*/
public static void add(String value) {
if (value != null) {
for (HashFunction f : functions) {
//计算 hash 值并修改 bitmap 中相应位置为 true
bitset.set(f.hash(value), true);
}
}
}
/**
* 判断相应元素是否存在
* @param value 需要判断的元素
* @return 结果
*/
public static boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (HashFunction f : functions) {
ret = bitset.get(f.hash(value));
//一个 hash 函数返回 false 则跳出循环
if (!ret) {
break;
}
}
return ret;
}
/**
* 模拟用户是不是会员,或用户在不在线。。。
*/
public static void main(String[] args) {
for (int i = 0; i < seeds.length; i++) {
functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
}
// 添加1亿数据
for (int i = 0; i < 100000000; i++) {
add(String.valueOf(i));
}
String id = "123456789";
add(id);
System.out.println(contains(id)); // true
System.out.println("" + contains("234567890")); //false
}
}
class HashFunction {
private int size;
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
this.seed = seed;
}
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
int r = (size - 1) & result;
return (size - 1) & result;
}
}
复制代码
来看看redis怎么实现的
使用Redis的位图数据结构实现布隆过滤器的方法如下:
1.初始化布隆过滤器
使用Redis的setbit
命令将位图中的所有位都初始化为0。例如,初始化一个大小为1亿的布隆过滤器:
127.0.0.1:6379> setbit bloomfilter 0 0
(integer) 0
127.0.0.1:6379> setbit bloomfilter 99999999 0
(integer) 0
复制代码
2.添加元素
将要添加的元素经过多个哈希函数计算得到多个位置,然后将这些位置的二进制位设置为1。例如,将元素hello
添加到布隆过滤器中:
127.0.0.1:6379> setbit bloomfilter 10001 1
(integer) 0
127.0.0.1:6379> setbit bloomfilter 20003 1
(integer) 0
127.0.0.1:6379> setbit bloomfilter 30005 1
(integer) 0
复制代码
3.判断元素是否存在
将要查询的元素经过多个哈希函数计算得到多个位置,然后检查这些位置的二进制位是否都为1。如果这些位置的二进制位都为1,则认为元素存在于布隆过滤器中;如果有任何一个位置的二进制位为0,则认为元素不存在于布隆过滤器中。例如,判断元素hello
是否存在于布隆过滤器中:
127.0.0.1:6379> getbit bloomfilter 10001
(integer) 1
127.0.0.1:6379> getbit bloomfilter 20003
(integer) 1
127.0.0.1:6379> getbit bloomfilter 30005
(integer) 1
复制代码
由于元素hello
经过哈希函数计算得到的三个位置的二进制位都为1,因此可以认为元素hello
存在于布隆过滤器中。
以上就是Redis布隆过滤器的实现方法。与传统的布隆过滤器相比,Redis布隆过滤器的优势在于可以直接使用Redis的位图数据结构,无需再实现一个新的数据结构。同时,由于Redis是一个分布式缓存系统,因此Redis布隆过滤器可以很方便地进行分布式部署,从而提高系统的可扩展性和可靠性。
以下是一个使用Spring Boot实现布隆过滤器解决缓存穿透的案例:
1.添加依赖
在pom.xml
文件中添加以下依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
<dependency>
<groupId>com.github.mgunlogson</groupId>
<artifactId>bloom-filter</artifactId>
<version>2.2.0</version>
</dependency>
复制代码
2.创建布隆过滤器
使用Guava库创建一个布隆过滤器,示例代码如下:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
@Service
public class BloomFilterService {
private BloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000000);
}
public boolean mightContain(String key) {
return bloomFilter.mightContain(key);
}
public void put(String key) {
bloomFilter.put(key);
}
}
复制代码
在上面的代码中,我们使用BloomFilter.create()
方法创建了一个布隆过滤器,并使用字符串类型的数据。1000000
是预期元素数量,可以根据实际情况进行调整。
3.缓存查询
在查询缓存之前,先使用布隆过滤器过滤掉不存在的数据请求。示例代码如下:
@Autowired
private BloomFilterService bloomFilterService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
// 判断key是否存在于布隆过滤器中
if (!bloomFilterService.mightContain(key)) {
return null;
}
// 缓存查询
Object value = redisTemplate.opsForValue().get(key);
// 如果缓存中不存在该数据,将其加入布隆过滤器中
if (value == null) {
bloomFilterService.put(key);
}
return value;
}
复制代码
在上面的代码中,我们首先使用BloomFilterService.mightContain()
方法判断请求的key是否存在于布隆过滤器中。如果不存在,则直接返回null。如果存在,则继续进行缓存查询。如果缓存中不存在该数据,将其加入布隆过滤器中,以便下次查询时可以快速判断该数据是否存在于缓存中。
以上是一个使用Spring Boot实现布隆过滤器解决缓存穿透的案例。
注意:
使用布隆过滤器确实有可能会产生误判,但是误判的概率是可以控制的。布隆过滤器的误判概率与预期元素数量和布隆过滤器的大小有关。如果预期元素数量和布隆过滤器的大小适当,那么误判的概率是非常小的。此外,布隆过滤器只会判断某个元素可能存在,而不是肯定存在,因此即使误判了,也不会有太大的影响。如果需要更高的准确性,可以选择使用精确匹配的算法,如哈希表等。但是,相对于布隆过滤器,这些算法需要更多的内存和计算资源。
三、可以在程序中进行严格的参数校验,避免恶意攻击。
例如,对于id参数可以使用正则表达式^[1-9]\\d*$
进行匹配,限制参数范围可以根据实际情况进行设置,比如对于年龄参数限制在1-120之间。
除了上述解决方案之外,还有一些其他的解决方案。比如,可以使用多级缓存的方式,将数据分为不同的缓存层次,从而避免缓存层次之间的依赖性。另外,也可以使用缓存预热的方式,在系统启动时将常用的数据预先加载到缓存中,以避免在运行时缓存失效导致的压力过大问题。
什么是缓存击穿?
缓存击穿是指某个热点数据过期或者被删除,导致大量请求直接绕过缓存访问数据库,导致数据库压力瞬间升高,甚至崩溃。解决缓存击穿的方法是使用分布式锁,保证只有一个线程去查询和更新数据。
缓存击穿的解决方案
对于Redis缓存击穿问题,以下是一些解决方案:
1.使用互斥锁(Mutex)
在缓存失效的瞬间,使用互斥锁(Mutex)进行资源控制,只允许一个请求进入数据库查询,其他请求等待。这样可以避免大量请求同时查询数据库,从而导致数据库崩溃。
2.使用分布式锁
Redis作为分布式缓存,可以使用分布式锁来避免缓存击穿问题。分布式锁可以保证同一时间只有一个线程或进程能够访问某个共享资源,从而避免缓存击穿的问题。
3.使用缓存预热
在系统启动时,将常用的数据预先加载到缓存中,以避免在运行时缓存失效导致的缓存击穿问题。
4.使用热点数据永不过期
对于一些热点数据,可以设置其过期时间为永不过期,从而避免因为缓存失效导致的缓存击穿问题。
5.使用限流策略
通过限制并发访问的请求数量,可以避免缓存击穿问题。可以使用一些开源的限流框架,如Guava RateLimiter、Spring Cloud Gateway等。
以上是一些常见的Redis缓存击穿解决方案,可以根据实际情况进行选择和使用。
什么是缓存雪崩?
缓存雪崩是指在某个时间段,缓存集中过期失效,导致大量请求直接访问数据库,造成短时间内数据库请求量骤增,压力过大,可能造成数据库崩溃。
缓存雪崩的解决方案有以下几种:
1.缓存预加载
在正式流量到来前,提前将可能会用到的数据加载到缓存中,避免在流量高峰期间查询数据库。
2.缓存高可用
使用多级缓存,如本地缓存和分布式缓存。如果某一个缓存出现了问题,可以快速切换到另一级缓存上。
3.缓存限流
通过限制访问频率,降低缓存的压力。
4.数据预热
在系统启动时,将所有需要缓存的数据直接加载到缓存中,避免在查询时缓存未命中。
5.缓存失效时间随机
将缓存的失效时间设置为随机值,或指定时刻后再加随机数,避免缓存同时失效导致数据库压力过大。
6.数据库容灾
建立数据库的主从复制和故障转移机制,避免数据库出现单点故障。
7.限制并发查询
限制并发查询,避免过多的查询同时落在数据库上。
8.缓存数据分片
将缓存数据分散到多个缓存中,避免单个缓存的失效影响整个缓存系统。
区别 区别 区别 这三个太像了
雪崩和击穿的区别:
击穿其实跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。
穿透和击穿的区别:
穿透和击穿有根本的区别,区别在于缓存穿透的情况是传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示,要对调用方保持这种“不信任”的心态。
扩展:
布隆过滤器使用场景和实例
在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题。
如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。
布隆过滤器的典型应用有:
- 数据库防止穿库。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。
- 业务场景中判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。
- 缓存宕机、缓存击穿场景,一般判断用户是否在缓存中,如果在则直接返回结果,不在则查询db,如果来一波冷数据,会导致缓存大量击穿,造成雪崩效应,这时候可以用布隆过滤器当缓存的索引,只有在布隆过滤器中,才去查询缓存,如果没查询到,则穿透到db。如果不在布隆器中,则直接返回。
- WEB拦截器,如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。Squid 网页代理缓存服务器在 cache digests 中就使用了布隆过滤器。Google Chrome浏览器使用了布隆过滤器加速安全浏览服务
- Venti 文档存储系统也采用布隆过滤器来检测先前存储的数据。
- SPIN 模型检测器也使用布隆过滤器在大规模验证问题时跟踪可达状态空间。