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