1、缓存穿透
解决方案:
1.1、布隆过滤器
原理:核心是一个bitmap(位数组),初始值都是0,用k个hash函数对某个key进行哈希,哈希出来的值对数组长度取模,取模出来的值就是bitmap位数组的下标,将这个下标改为1。例如有三个hash函数,其中一个hash函数对某个key哈希出来的值是6354719,然后对数组长度取模,比如数组长度为20,则6354719%20=19,则将bitmap位数组的19下标改为1。
容错率:实则就是误判的概率。跟数组长度、hash函数的个数有关,必需配合使用。容错率越小,创建数组长度越大,性能越低。误判解释如下几个图所示:
解释:当往布隆过滤器中添加了1-20的数字时,布隆过滤器中黄色的下标是已经被改为1了,此时位数组长度差不多被改满了。而输入一个不存在布隆过滤器中的数据,比如说111时,它却说可能存在,所以此时出现了误判。
维护:添加表记录的时候往布隆过滤器添加一份;表中如果删除了大量的数据应重建布隆过滤器。
优点:
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和查询时间都是常数({\displaystyle O(k)} )。另外,散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
缺点:
但是布隆过滤器的缺点和优点一样明显。误判率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。
实现:
package com.luban.testcache.filter;
import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Scope;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
import java.util.List;
@ConfigurationProperties("bloom.filter")
@Component
public class RedisBloomFilter {
//预计插入量
private long expectedInsertions;
//可接受的错误率
private double fpp;
@Autowired
private RedisTemplate redisTemplate;
//bit数组长度
private long numBits;
//hash函数数量
private int numHashFunctions ;
public long getExpectedInsertions() {
return expectedInsertions;
}
public void setExpectedInsertions(long expectedInsertions) {
this.expectedInsertions = expectedInsertions;
}
public void setFpp(double fpp) {
this.fpp = fpp;
}
public double getFpp() {
return fpp;
}
@PostConstruct
public void init(){
this.numBits = optimalNumOfBits(expectedInsertions, fpp);
this.numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
}
//计算hash函数个数
private int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
//计算bit数组长度
private 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)));
}
/**
* 判断keys是否存在于集合
*/
public boolean isExist(String key) {
long[] indexs = getIndexs(key);
List list = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Nullable
@Override
public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
redisConnection.openPipeline();
for (long index : indexs) {
redisConnection.getBit("bf:taibai".getBytes(), index);
}
redisConnection.close();
return null;
}
});
return !list.contains(false);
}
/**
* 将key存入redis bitmap
*/
public void put(String key) {
long[] indexs = getIndexs(key);
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Nullable
@Override
public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
redisConnection.openPipeline();
for (long index : indexs) {
redisConnection.setBit("bf:taibai".getBytes(),index,true);
}
redisConnection.close();
return null;
}
});
}
/**
* 根据key获取bitmap下标
*/
private 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;
}
/**
* 获取一个hash值
*/
private long hash(String key) {
Charset charset = Charset.forName("UTF-8");
return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
}
}
使用:容器启动时将数据库中表数据put到布隆过滤器中
@PostConstruct
public void init(){
List<Order> orders = orderService.selectOrderyAll();
for (Order order : orders) {
redisBloomFilter.put(String.valueOf(order.getId()));
}
}
1.2、将一些查询不到的值设为null。
2、缓存击穿
2.1、解决方案:使用分布式锁。
/**
* 分布式锁解决缓存击穿
* @param key 键
* @return
*/
public String getKey(Integer key) {
String value = (String)redisTemplate.opsForValue().get(key+"");
if(value == null){
String mutexKey = "mutex:key:"+key; //设置分布式锁的key
if(redisTemplate.opsForValue().setIfAbsent(mutexKey,"1",180, TimeUnit.SECONDS)){ //给这个key上一把分布式锁,ex表示只有一个线程能执行,过期时间为180秒
// 从数据库中查出该数据放入redis
value = userDao.getUserName(key);
redisTemplate.opsForValue().set(key+"",value);
// 释放锁
redisTemplate.delete(mutexKey);
}else{
// 其他的线程休息100毫秒后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
getKey(key);
}}
return value;
}
redis.set(mutexKey,“1”,“ex 180”,“nx”)则对应redis中的SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]命令,例子如下:
127.0.0.1:6379> set a 1 ex 10 nx
OK
127.0.0.1:6379> ttl a
8
测试一下:第一次查询,输出了sql语句。
看看redis
第二次再查询,还是这个样子,什么都没输出,查redis。
搞定。
3、缓存雪崩
原因:redis中key大面积同时失效,导致大量并发请求都打到了数据库上。
解决方案:
1、设置redis中的key随机的过期时间。
2、配置redis高可用集群(主从复制、读写分离、哨兵、集群等)。