一.Redis緩存雪崩
Redis緩存雪崩和穿透乍一看好像差不多,概念容易混淆.
緩存雪崩是指在我們設(shè)置緩存失效時間上時采用了相同的過期時間,導(dǎo)致緩存在某一時刻同時失效,請求全部打到后端數(shù)據(jù)庫,數(shù)據(jù)庫一時請求過大,數(shù)據(jù)庫cpu和IO一時負載過大,造成雪崩。如果不能理解的話,閉上眼睛想象一下,雪山崩塌的場景,還不能理解的話,那就算了吧.

這塊其實業(yè)界有很多解決方案,但是沒有哪一種方案說是完美的,需要結(jié)合實際并發(fā)量.第一種就是加鎖排隊,串行的去執(zhí)行,但是這本質(zhì)上是一種緩解,允許等待,從用戶角度來說不是很好.第二種比較多的就是mutex互斥鎖,比如Redis分布式鎖SetNX,如果緩存里面沒有并不是去加載數(shù)據(jù)庫.第三種限流:比如用滑動窗口控制,在大流量來的時候縮緊;或者通過令牌桶、漏桶算法等.第四種就是做二級緩存,或者雙緩存策略。比如B為原始緩存,B2為拷貝緩存,B失效時,可以訪問B2,B1緩存失效時間設(shè)置為短期,B2設(shè)置為長期。這塊有點master-slave,主從的意思.
這些技術(shù)手段,想達到的終極目標(biāo)無外乎就是讓失效時間達到均勻的狀態(tài).
二.Redis穿透
Redis穿透是大量的請求在緩存中沒有命中,導(dǎo)致每次都要到數(shù)據(jù)庫里面去查詢,這樣導(dǎo)致緩存被穿透.
解決方案:
1.Bloom filter
布隆過濾器背景:(Bloom Filter)是由伯頓·布隆(Burton Bloom)于1970年提出來的,它實際上是一個很長的二進制向量和一系列隨機映射函數(shù)。
我們知道Redis之所以快除了單線程避免了CPU上下文切換,采用了epoll機制,還有一個重要的問題是他的存儲結(jié)構(gòu),時間復(fù)雜度是o(1),但是redis的存儲空間是很珍貴的,過多的key對于redis來說是一件麻煩的事情,比如你有幾億個key,這種一股腦的丟到Redis,很不合理.Bloom Filter或許就是一種改善的解決方案,極大的減小了存儲空間.布隆過濾器是一個位向量或者說是位數(shù)組.
但是對于布隆過濾器的使用一定要謹慎,Bloom過濾器比較雞肋的地方是它存在一定的概率的誤判,我們在學(xué)術(shù)上稱他為假陽性,而且隨著元素的增加,這種誤判的機率會隨著增加,但是誤判的概率幾乎可以忽略,影響不到,一般key不多的情況,用散列表就可以了,唯一的好處就是節(jié)省空間.目前它只支持add和isExist操作,不支持delete操作,這個理解它的原理的很容易明白,因為你刪除更新那一位1,正好可能也是別的key的entry.下面看一下代碼吧:
布隆過濾器在Guava中的實現(xiàn):
創(chuàng)建:
static <T> BloomFilter<T> create(
Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
checkNotNull(funnel);
checkArgument(
expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
checkNotNull(strategy);
if (expectedInsertions == 0) {
expectedInsertions = 1;
}
/*
* TODO(user): Put a warning in the javadoc about tiny fpp values, since the resulting size
* is proportional to -log(p), but there is not much of a point after all, e.g.
* optimalM(1000, 0.0000000000000001) = 76680 which is less than 10kb. Who cares!
*/
long numBits = optimalNumOfBits(expectedInsertions, fpp);
int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
try {
return new BloomFilter<T>(new BitArray(numBits), numHashFunctions, funnel, strategy);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
}
}
create里面有4個參數(shù),funnel:輸入的數(shù)據(jù),expectedInsertions:預(yù)估計插入的元素總量,fpp:你自己想達到的誤判率,strategy:實現(xiàn)的實例
BloomFilterStrategies類:
MURMUR128_MITZ_64() {
@Override
public <T> boolean put(
T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
long hash1 = lowerEight(bytes);
long hash2 = upperEight(bytes);
boolean bitsChanged = false;
long combinedHash = hash1;
for (int i = 0; i < numHashFunctions; i++) {
// Make the combined hash positive and indexable
bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
combinedHash += hash2;
}
return bitsChanged;
}
@Override
public <T> boolean mightContain(
T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
long hash1 = lowerEight(bytes);
long hash2 = upperEight(bytes);
long combinedHash = hash1;
for (int i = 0; i < numHashFunctions; i++) {
// Make the combined hash positive and indexable
if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) {
return false;
}
combinedHash += hash2;
}
return true;
}
底層bit數(shù)組實現(xiàn):
static final class BitArray {
final long[] data;
long bitCount;
BitArray(long bits) {
this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]);
}
// Used by serialization
BitArray(long[] data) {
checkArgument(data.length > 0, "data length is zero!");
this.data = data;
long bitCount = 0;
for (long value : data) {
bitCount += Long.bitCount(value);
}
this.bitCount = bitCount;
}
/** Returns true if the bit changed value. */
boolean set(long index) {
if (!get(index)) {
data[(int) (index >>> 6)] |= (1L << index);
bitCount++;
return true;
}
return false;
}
boolean get(long index) {
return (data[(int) (index >>> 6)] & (1L << index)) != 0;
}
/** Number of bits */
long bitSize() {
return (long) data.length * Long.SIZE;
}
...
}
2.緩存這些空對象,但是注意失效時間設(shè)置的小一點,防止Redis的key過多.
三.Redis預(yù)熱
緩存預(yù)熱就是系統(tǒng)發(fā)布之前,先把緩存數(shù)據(jù)加載到緩存系統(tǒng)里面,這樣避免了活動正式開始之前,首次沒有命中,要查數(shù)據(jù)庫的問題,另外一方面預(yù)熱也是提前對流量的一種預(yù)估方式,很多大型活動或者秒殺,都會提前來一波預(yù)熱.
四.降級
當(dāng)并發(fā)量大的時候,響應(yīng)很慢或者超時,影響到核心鏈路的業(yè)務(wù)處理(比如支付、訂單等),或者導(dǎo)致到上游的系統(tǒng)的扇出,這種情形我需要對服務(wù)進行降級,換句話說就是保帥丟兵的思想.