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