Redis是一種流行的內(nèi)存緩存解決方案,但在實(shí)際使用中,會遇到一些緩存相關(guān)的問題,其中最主要的就是緩存穿透,擊穿和雪崩問題。
一、緩存穿透
緩存穿透是指當(dāng)查詢一個不存在的緩存時,由于緩存中沒有相應(yīng)的數(shù)據(jù),而數(shù)據(jù)庫中也不存在對應(yīng)的數(shù)據(jù),導(dǎo)致每次查詢都必須訪問數(shù)據(jù)庫,這會導(dǎo)致數(shù)據(jù)庫的負(fù)載過高。攻擊者可以利用這個漏洞進(jìn)行惡意攻擊,通過不斷查詢不存在的緩存,來使緩存服務(wù)器或者數(shù)據(jù)庫癱瘓。
關(guān)于緩存穿透常用的的有兩種解決辦法:
第一種,緩存空數(shù)據(jù),當(dāng)查詢一個不存在的數(shù)據(jù)時,可以將這個查詢結(jié)果緩存起來,以空對象的形式存儲在緩存中。這樣,在下一次查詢時,如果緩存中存在這個空對象,就可以直接返回,而不會再次查詢數(shù)據(jù)庫或接口。
第二種,使用布隆過濾器
<?php
/**
* 布隆過濾器類
*/
class BloomFilter {
private $redis; // Redis對象
private $bitmapSize; // 位圖大小
private $hashCount; // 哈希函數(shù)數(shù)量
/**
* 構(gòu)造函數(shù)
* @param int $bitmapSize 位圖大小,默認(rèn)為1024
* @param int $hashCount 哈希函數(shù)數(shù)量,默認(rèn)為3
*/
public function __construct($bitmapSize = 1024, $hashCount = 3) {
// 實(shí)例化Redis對象并連接到Redis服務(wù)器
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$this->redis = $redis;
$this->bitmapSize = $bitmapSize;
$this->hashCount = $hashCount;
}
/**
* 插入元素到布隆過濾器中
* @param string $element 要插入的元素
*/
public function insert($element) {
// 對元素進(jìn)行多次哈希,并在位圖中設(shè)置相應(yīng)位置為1
for ($i = 1; $i <= $this->hashCount; $i++) {
$hash = md5($element . $i);
$position = hexdec(substr($hash, 0, 6)) % $this->bitmapSize;
$this->redis->setBit('bloom_filter', $position, 1);
}
}
/**
* 判斷元素是否存在于布隆過濾器中
* @param string $element 要判斷的元素
* @return bool 如果存在返回true,否則返回false
*/
public function contains($element) {
// 對元素進(jìn)行多次哈希,并檢查位圖相應(yīng)位置是否為1
for ($i = 1; $i <= $this->hashCount; $i++) {
$hash = md5($element . $i);
$position = hexdec(substr($hash, 0, 6)) % $this->bitmapSize;
if (!$this->redis->getBit('bloom_filter', $position)) {
// 如果任意一個位置為0,則表示元素不存在于布隆過濾器中
return false;
}
}
// 如果所有位置都為1,則表示元素可能存在于布隆過濾器中
return true;
}
}
// 緩存穿透解決方案
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 實(shí)例化布隆過濾器對象
$bloomFilter = new BloomFilter($redis);
$cacheKey = 'product_info_123';
if ($bloomFilter->contains($cacheKey)) {
$productInfo = $redis->get($cacheKey);
if ($productInfo) {
return $productInfo;
}
} else {
// 如果布隆過濾器中不存在該鍵,則直接返回空結(jié)果
return null;
}
// 如果緩存中不存在該鍵,則從數(shù)據(jù)庫中查詢并存入緩存和布隆過濾器中
$productInfo = $database->queryProductInfoById(123);
if (!$productInfo) {
// 如果數(shù)據(jù)庫中也不存在該記錄,則插入一個空記錄,防止緩存穿透攻擊
$productInfo = '';
$redis->set($cacheKey, 60, $productInfo);
} else {
$redis->set($cacheKey, 60, $productInfo);
$bloomFilter->insert($cacheKey);
}
return $productInfo;
二、緩存擊穿
緩存擊穿是指當(dāng)某個熱點(diǎn)數(shù)據(jù)失效時,大量的并發(fā)請求訪問這個熱點(diǎn)數(shù)據(jù),導(dǎo)致所有請求都訪問數(shù)據(jù)庫,從而使數(shù)據(jù)庫的負(fù)載急劇增加。為了避免這種情況發(fā)生,可以將熱點(diǎn)數(shù)據(jù)的緩存時間設(shè)置為永不過期,或者設(shè)置一個較長的過期時間。
$redis->set($key, $value, -1) 和$redis->set($key, $value) 都可以用來設(shè)置緩存數(shù)據(jù),并且讓緩存永不過期。
三、緩存雪崩
緩存雪崩是指當(dāng)某一時刻緩存中的大量數(shù)據(jù)同時失效時,大量的并發(fā)請求訪問這些失效的數(shù)據(jù),導(dǎo)致所有請求都訪問數(shù)據(jù)庫,從而使數(shù)據(jù)庫的負(fù)載急劇增加。為了避免這個問題,我們可以設(shè)置緩存的過期時間隨機(jī)化,使緩存的失效時間不同,避免大量數(shù)據(jù)同時失效。此外,我們也可以采用緩存預(yù)熱的方式,提前將熱點(diǎn)數(shù)據(jù)加載到緩存中,以減少緩存的失效率。
在實(shí)際應(yīng)用中,可以通過設(shè)置一個隨機(jī)數(shù)生成器,隨機(jī)生成一個緩存時間偏移量,然后將緩存時間加上偏移量作為實(shí)際緩存時間。例如,在一個緩存時間為60秒的基礎(chǔ)上,可以隨機(jī)生成一個0到10秒的偏移量,然后將緩存時間設(shè)置為60秒加上該偏移量,即在60到70秒之間隨機(jī)。
// 緩存時間
$cache_time = 60;
// 生成隨機(jī)偏移量
$offset = rand(0, 10);
// 設(shè)置緩存時間
$expire_time = $cache_time + $offset;
// 設(shè)置緩存
$redis->set($key, $value, $expire_time);