前言:
隨着業務的發展,可能出現了大量數據的請求,在這個時候,如果所有的請求都湧入數據庫,就會造成數據庫的壓力增大,一些簡單的sql查詢因爲數據庫承受大量壓力的而幾何式變慢,甚至造成癱瘓。
因爲,爲了解決這個問題,引入了nosql,而redis則是nosql技術中的一種。
但是引入nosql,則會引入緩存穿透,緩存擊穿,緩存雪崩等問題,因此,本章則會關於這幾個問題,說說我自己的理解和解決。
本文所引用redis結構是一主二從三哨兵。
一.是什麼
既然引入redis會引入緩存穿透,緩存擊穿,緩存雪崩等,我們先來解釋一下這個幾個東西是什麼。
緩存穿透:通過請求一個無論redis還是數據庫都不存在的key,因此請求訪問redis都是null,轉而請求數據庫,還是造成了大量請求同時間到達了數據庫,進而起到了壓垮了數據庫的作用。
緩存擊穿:對於某一個熱點key,不停地被高併發訪問,但是一旦該熱點key過期了,這時候成千上萬的請求立馬訪問到數據庫一層,起到壓垮數據庫的作用。
緩存雪崩:在某一個時間段,大量緩存集體過期,這個時候,由於緩存過期,所有的請求壓力轉移後端去,數據庫去。
二.怎麼解決
2.1緩存穿透解決辦法
面對緩存穿透比較常用的一種辦法就是使用布隆過濾器(Bloom Fliter),通過數據哈希存儲到一個巨大的bitmap中,從而避免進行到數據庫中。另外也有一種簡單的解決辦法就是把不存在key的空緩存也緩存到redis裏面去,設置好過期時間,最好不超過5分鐘。
根據網上找到布隆過濾器,根據guava裏面的布隆過濾器進行改造,方便分佈式使用。
public class BloomFilterHelper<T> {
private int numHashFunctions;
private int bitSize;
private Funnel<T> funnel;
/**
* <p>
* 1.構造函數判斷Funnel是否爲空,賦值。
* 2.計算bit的大小,
* 3.需要哈希次數
* </p>
*
* @param funnel
* @param expectedInsertions
* @param fpp -》false positive
*/
public BloomFilterHelper(Funnel<T> 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<java.lang.String>
* @author Lifeifei
* @date 2019/4/8 13:18
*/
@Bean
public BloomFilterHelper<String> initBloomFilterHelper() {
return new BloomFilterHelper<>((Funnel<String>) (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 <T> void addByBloomFilter(BloomFilterHelper<T> 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 <T> boolean includeByBloomFilter(BloomFilterHelper<T> 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的過期。