前言:
随着业务的发展,可能出现了大量数据的请求,在这个时候,如果所有的请求都涌入数据库,就会造成数据库的压力增大,一些简单的sql查询因为数据库承受大量压力的而几何式变慢,甚至造成瘫痪。
因为,为了解决这个问题,引入了nosql,而redis则是nosql技术中的一种。
但是引入nosql,则会引入缓存穿透,缓存击穿,缓存雪崩等问题,因此,本章则会关于这几个问题,说说我自己的理解和解决。
本文所引用redis结构是一主二从三哨兵。
既然引入redis会引入缓存穿透,缓存击穿,缓存雪崩等,我们先来解释一下这个几个东西是什么。
缓存穿透:通过请求一个无论redis还是数据库都不存在的key,因此请求访问redis都是null,转而请求数据库,还是造成了大量请求同时间到达了数据库,进而起到了压垮了数据库的作用。
缓存击穿:对于某一个热点key,不停地被高并发访问,但是一旦该热点key过期了,这时候成千上万的请求立马访问到数据库一层,起到压垮数据库的作用。
缓存雪崩:在某一个时间段,大量缓存集体过期,这个时候,由于缓存过期,所有的请求压力转移后端去,数据库去。
面对缓存穿透比较常用的一种办法就是使用布隆过滤器(Bloom Fliter),通过数据哈希存储到一个巨大的bitmap中,从而避免进行到数据库中。另外也有一种简单的解决办法就是把不存在key的空缓存也缓存到redis里面去,设置好过期时间,最好不超过5分钟。
根据网上找到布隆过滤器,根据guava里面的布隆过滤器进行改造,方便分布式使用。
public class BloomFilterHelper {
private int numHashFunctions;
private int bitSize;
private Funnel funnel;
/**
*
* 1.构造函数判断Funnel是否为空,赋值。
* 2.计算bit的大小,
* 3.需要哈希次数
*
*
* @param funnel
* @param expectedInsertions
* @param fpp -》false positive
*/
public BloomFilterHelper(Funnel funnel, int expectedInsertions, double fpp) {
Preconditions.checkArgument(funnel != null, "funnel不能为空");
this.funnel = funnel;
bitSize = optimalNumOfBits(expectedInsertions, fpp);
//计算hash次数
numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
}
/**
* 计算hashmap -使用murmur3-128
* @param value
* @return
*/
public int[] murmurHashOffset(T value) {
int[] offset = new int[numHashFunctions];
long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
offset[i - 1] = nextHash % bitSize;
}
return offset;
}
/**
* 计算bit数组长度
*/
private int optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
/**
* 计算hash方法执行次数
*/
private int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
}
redis环境注册
@Configuration
public class RedisConfig {
@Autowired
private RedisTemplate redisTemplate;
@Bean
public RedisTemplate redisTemplateInit() {
//设置序列化Key的实例化对象
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置序列化Value的实例化对象
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
/**
* <注册BloomFilterHelper>
*
* @param
* @return com.zy.crawler.config.redis.BloomFilterHelper
* @author Lifeifei
* @date 2019/4/8 13:18
*/
@Bean
public BloomFilterHelper initBloomFilterHelper() {
return new BloomFilterHelper<>((Funnel) (from, into) -> into.putString(from, Charsets.UTF_8)
.putString(from, Charsets.UTF_8), 1000000, 0.01);
}
}
RedisService
@Service
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据给定的布隆过滤器添加值
*/
public void addByBloomFilter(BloomFilterHelper bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
redisTemplate.opsForValue().setBit(key, i, true);
}
}
/**
* 根据给定的布隆过滤器判断值是否存在
*/
public boolean includeByBloomFilter(BloomFilterHelper bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
return true;
}
}
redis里面通过setBit进行存储,因此即便某个key不存在,可以直接通过布隆过滤器查询不同hash出的bit是否都存在。从而解决了问题。
2.2缓存击穿解决方法
缓存击穿一般是因为某个热点key过期后,成千上万的请求突然转向数据库增大数据库的压力。因此一般的做法的加个互斥锁(mutex).一旦某个热点过期了,设置互斥锁,第一个获取锁的重新设置热点值,后面的请求则休眠一秒,等待第一个请求设置了热点值后,重新获取热点值。
这是在serviceimpl的方法
@Override
public String getTalkingPoint(String key) {
String talkingPointValue = (String)redisTemplate.opsForValue().get(key);
if(talkingPointValue ==null){
//互斥锁
if(redisTemplate.opsForValue().setIfAbsent(key+"_mutex","key_mutex",3,TimeUnit.MINUTES)){
//从数据库查询
HighTecoEntity byId = this.getById(1);
redisTemplate.opsForValue().set(key,byId.getUser(),3,TimeUnit.MINUTES);
return byId.getUser();
}else{
//拿不到互斥锁,休眠一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.info("获取失败");
return null;
}
String getTalkingValueByRedis = (String) redisTemplate.opsForValue().get(key);
return null;
}
}
return talkingPointValue;
}
2.3缓存雪崩
缓存雪崩相对缓存击穿而言,是单个key值过期和N个key值同时过期,这时短时间的大量数据读写操作极大可能导致数据库垮掉。我们可以选择通过随机因子把不同类型的key分散开来,除此之外,还可以增加其过期时间,即原本是30min,后面可以设置成60min,防止 大规模key的过期。