小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
Redis是我们日常在工作中使用非常多的缓存解决手段,使用缓存,能够提升我们应用程序的性能,同时极大程度的降低数据库的压力。但如果使用不当,同样会造成许多问题,其中三大经典问题就包括了缓存穿透、缓存击穿和缓存雪崩。是不是听上去一脸懵逼?没关系,看完这篇就明白了。
缓存穿透
缓存穿透是指用户在查找一个数据时查找了一个根本不存在的数据。按照缓存设计流程,首先查询redis缓存,发现并没有这条数据,于是直接查询数据库,发现也没有,于是本次查询结果以失败告终。
当存在大量的这种请求或恶意使用不存在的数据进行访问攻击时,大量的请求将直接访问数据库,造成数据库压力甚至可能直接瘫痪。以电商商城为例,以商品id进行商品查询,这时如果使用一个不存在的id进行攻击,每次的攻击都将访问在数据库上。
来看一下应对方案:
1、缓存空对象
修改数据库写回缓存逻辑,对于缓存中不存在,数据库中也不存在的数据,我们仍然将其缓存起来,并且设置一个缓存过期时间。
如上图所示,查询数据库失败时,仍以查询的key值缓存一个空对象(key,null)。但是这么做仍然存在不少问题:
a、这时在缓存中查找这个key值时,会返回一个null的空对象。需要注意的是这个空对象可能并不是客户端需要的,所以需要对结果为空进行处理后,再返回给客户端 b、占用redis中大量内存。因为空对象能够被缓存,redis会使用大量的内存来存储这些值为空的key c、如果在写缓存后数据库中存入的这个key的数据,由于缓存没有过期,取到的仍为空值,所以可能出现短暂的数据不一致问题
2、布隆过滤器
布隆过滤器是一个二进制向量,或者说二进制的数组,或者说是位(bit)数组。
![](/qrcode.jpg)
因为是二进制的向量,它的每一位只能存放0或者1。当需要向布隆过滤器中添加一个数据映射时,添加的并不是原始的数据,而是使用多个不同的哈希函数生成多个哈希值,并将每个生成哈希值指向的下标位置置为1。所以,别再说从布隆过滤器中取数据啦,我们根本就没有存原始数据。
例如"Hydra"的三个哈希函数生成的下标分别为1,3,6,那么将这三位置为1,其他数据以此类推。那么这样的数据结构能够起到什么效果呢?我们可以根据这个位向量,来判断数据是否存在。
具体流程:
a、计算数据的多个哈希值;
b、判断这些bit是否为1,全部为1,则数据可能存在;
c、若其中一个或多个bit不为1,则判断数据不存在。
需要注意,布隆过滤器是存在误判的,因为随着数据存储量的增加,被置为1的bit数量也会增加,因此,有可能在查询一个并不存在的数据时,碰巧所有bit都已经被其他数据置为了1,也就是发生了哈希碰撞。因此,布隆过滤器只能做到判断数据是否可能存在,不能做到百分百的确定。
Google的guava
包为我们提供了单机版的布隆过滤器实现,来看一下具体使用
首先引入maven依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.1-jre</version>
</dependency>
复制代码
向布隆过滤器中模拟传入1000000条数据,给定误判率,再使用不存在的数据进行判断:
public class BloomTest {
public static void test(int dataSize,double errorRate){
BloomFilter<Integer> bloomFilter=
BloomFilter.create(Funnels.integerFunnel(), dataSize, errorRate);
for(int i = 0; i< dataSize; i++){
bloomFilter.put(i);
}
int errorCount=0;
for(int i = dataSize; i<2* dataSize; i++){
if(bloomFilter.mightContain(i)){
errorCount++;
}
}
System.out.println("Total error count: "+errorCount);
}
public static void main(String[] args) {
BloomTest.test(1000000,0.01);
BloomTest.test(1000000,0.001);
}
}
复制代码
测试结果:
Total error count: 10314
Total error count: 994
复制代码
可以看出,在给定误判率为0.01时误判了10314次,在误判率为0.001时误判了994次,大体符合我们的期望。
但是因为guava的布隆过滤器是运行在的jvm内存中,所以仅支持单体应用,并不支持微服务分布式。那么有没有支持分布式的布隆过滤器呢,这时Redis站了出来,自己造成的问题自己来解决!
Redis的BitMap(位图)支持了对位的操作,通过一个bit位来表示某个元素对应的值或者状态。
//对key所存储的字符串值,设置或清除指定偏移量上的位(bit)
setbit key offset value
//对key所存储的字符串值,获取指定偏移量上的位(bit)
getbit key offset
复制代码
既然布隆过滤器是对位进行赋值,我们就可以使用BitMap提供的setbit和getbit命令非常简单的对其进行实现,并且setbit操作可以实现自动数组扩容,所以不用担心在使用过程中数组位数不够的情况。
//源码参考https://www.cnblogs.com/CodeBear/p/10911177.html
public class RedisBloomTest {
private static int dataSize = 1000;
private static double errorRate = 0.01;
//bit数组长度
private static long numBits;
//hash函数数量
private static int numHashFunctions;
public static void main(String[] args) {
numBits = optimalNumOfBits(dataSize, errorRate);
numHashFunctions = optimalNumOfHashFunctions(dataSize, numBits);
System.out.println("Bits length: "+numBits);
System.out.println("Hash nums: "+numHashFunctions);
Jedis jedis = new Jedis("127.0.0.1", 6379);
for (int i = 0; i <= 1000; i++) {
long[] indexs = getIndexs(String.valueOf(i));
for (long index : indexs) {
jedis.setbit("bloom", index, true);
}
}
num:
for (int i = 1000; i < 1100; i++) {
long[] indexs = getIndexs(String.valueOf(i));
for (long index : indexs) {
Boolean isContain = jedis.getbit("bloom", index);
if (!isContain) {
System.out.println(i + "不存在");
continue num;
}
}
System.out.println(i + "可能存在");
}
}
//根据key获取bitmap下标
private static long[] getIndexs(String key) {
long hash1 = hash(key);
long hash2 = hash1 >>> 16;
long[] result = new long[numHashFunctions];
for (int i = 0; i < numHashFunctions; i++) {
long combinedHash = hash1 + i * hash2;
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
result[i] = combinedHash % numBits;
}
return result;
}
private static long hash(String key) {
Charset charset = Charset.forName("UTF-8");
return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
}
//计算hash函数个数
private static int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
//计算bit数组长度
private static long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
}
复制代码
基于BitMap实现分布式布隆过滤器的过程中,哈希函数的数量以及位数组的长度都是动态计算的。可以说,给定的容错率越低,哈希函数的个数则越多,数组长度越长,使用的redis内存开销越大。
guava中布隆过滤器的数组最大长度是由int值的上限决定的,大概为21亿,而redis的位数组为512MB,也就是2^32位,所以最大长度能够达到42亿,容量为guava的两倍。
好了,这篇文章我们先介绍到这,下一篇再来说说剩下的两个问题~
最后
如果觉得对您有所帮助,小伙伴们可以点赞、转发一下,非常感谢
公众号
码农参上
,加个好友,做个点赞之交啊