高並發架構技術|緩存失效、緩存穿透問題 PHP 代碼解決
問題描述
緩存失效:
引起這個原因的主要因素是高並發下,我們一般設定一個緩存的過期時間時,可能有一些會設置5分鐘啊,10分鐘這些;並發很高時可能會出在某一個時間同時生成了很多的緩存,並且過期時間在同一時刻,這個時候就可能引發——當過期時間到後,這些緩存同時失效,請求全部轉發到DB,DB可能會壓力過重。 處理方法: 一個簡單方案就是將緩存失效時間分散開,不要所以緩存時間長度都設置成5分鐘或者10分鐘;比如我們可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重複率就會降低,就很難引發集體失效的事件。 緩存失效時產生的雪崩效應,將所有請求全部放在資料庫上,這樣很容易就達到資料庫的瓶頸,導致服務無法正常提供。盡量避免這種場景的發生。緩存穿透:
出現場景:指查詢一個一定不存在的數據,由於緩存是不命中時被動寫的,並且出於容錯考慮,如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。當在流量較大時,出現這樣的情況,一直請求DB,很容易導致服務掛掉。
處理方法:方法1.在封裝的緩存SET和GET部分增加個步驟,如果查詢一個KEY不存在,就已這個KEY為前綴設定一個標識KEY;以後再查詢該KEY的時候,先查詢標識KEY,如果標識KEY存在,就返回一個協定好的非false或者NULL值,然後APP做相應的處理,這樣緩存層就不會被穿透。當然這個驗證KEY的失效時間不能太長。
方法2.如果一個查詢返回的數據為空(不管是數據不存在,還是系統故障),我們仍然把這個空結果進行緩存,但它的過期時間會很短,一般只有幾分鐘。
方法3.採用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的bitmap中,一個一定不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。
緩存並發:
出現場景:當網站並發訪問高,一個緩存如果失效,可能出現多個進程同時查詢DB,同時設置緩存的情況,如果並發確實很大,這也可能造成DB壓力過大,還有緩存頻繁更新的問題。處理方法:對緩存查詢加鎖,如果KEY不存在,就加鎖,然後查DB入緩存,然後解鎖;其他進程如果發現有鎖就等待,然後等解鎖後返回數據或者進入DB查詢。
解決方案
直接擼代碼 下面是引用類 StudentController
<?php class StudentController extends BaseController{ //網站首頁展示 public function index(){ //獲取分數最高的10位學生信息 $top10ScoreStudents = StudentService::getTop10Students(); //todo something more detail... } }
接著是關鍵的服務類 StudentService
<?phpnamespace AppService;use AppBaseService as BaseService;//通常來說一個稍大型的 PHP 項目,都有有一個倉儲層 Repositoryuse AppRepositoryStuduent as StudentRepository;class StudentService extends BaseService{ public static function getTop10Students(){ $config = App::getConfig(); $cacheKeyGenerator = CacheKeyGenerator::getInstance(); //使用通用的緩存鍵生成器去獲取緩存新鍵,方便增加統一的前綴(可用於包含 app 名字和版本信息) $top10StuduentsCacheKey = $cacheKeyGenerator->generate($config["cacheKeyAlias"]["top10Students"]["key"]); //下面是常見的緩存獲取代碼 //從 redis client 連接池實例中獲取一個可用的 client 連接,保持連接復用的連接池技術 $redisInstance = RedisInstancePool::getInstance(); //直接從緩存中獲取,若為 null ,則更新緩存 $top10StuduentsCache = $redisInstance->get($top10StuduentsCacheKey ); //這裡使用 === null 判斷是為了避免空數據導致的緩存穿透 if ($top10StuduentsCache === null){ //從 db 中獲取一份最新的緩存數據 //加一個訪問鎖,最多鎖 20 秒,因為一個並發 1000 左右的單秒訪問此介面時,若不加鎖 //必然會導致直接多個請求直接命中資料庫,也就是下面的 「StudentRepository::getTop10Students()」,明顯可能會導致資料庫瓶頸出現 if($lockForTop10Students = LockUtility::lock($config["cacheKeyAlias"]["top10Students"]["key"]),20){ $top10StudentsFromDb = StudentRepository::getTop10Students(); if(empty($top10StudentsFromDb) { //防止空數據穿透到資料庫 $top10StudentsFromDb = []; } $top10StuduentsCache = $top10StudentsFromDb; //設置緩存 //ttl 是保存在全局 config 中的 redis key 生命周期 $redisInstance->set($config["cacheKeyAlias"]["top10Students"]["key"],$top10StudentsFromDb,$config["cacheKeyAlias"]["top10Students"]["ttl"]); LockUtility::unlock($config["cacheKeyAlias"]["top10Students"]["key"]); return $top10StuduentsCache; } else { //沒有獲取到訪問鎖,直接返回空結果 return []; } } return $top10StuduentsCache; }}?>
參考鏈接:
- https://www.cnblogs.com/gauze/p/6679561.html
- https://www.cnblogs.com/lingshao/p/5658757.html
- http://wiki.jikexueyuan.com/project/openresty/lock/cache-miss-storm.html
推薦閱讀:
※php如何不修改一句業務代碼實現整庫數據變化實時監控?
※作為一名合格的PHP程序員,應該進行哪些技術儲備?
※PHP 自學全套書籍,有哪些推薦?
※學習PHP,然後學習C, 深入Linux的, 如果想往底層發展, 應該是如何的一個學習過程?
※你真的了解 TIOBE 編程語言排行榜嗎?