關注公眾號:xy的技術圈
使用快取可以緩解大流量壓力,顯著提高程式的效能。我們在使用快取系統時,尤其是大併發情況下,經常會遇到一些“疑難雜症”。本文總結了一些使用快取時常見的問題及解決方案,以後在遇到這類問題時可以作為參考,在設計快取系統的時候也應該考慮這些常見的情況。
為了表述方便,本文以資料庫查詢快取為例,使用快取可以減小對資料庫的壓力。
快取穿透
我們在使用快取時,往往先嚐試去快取中取值,如果沒有,再去資料庫取值,如果資料庫也沒有值,則根據業務需求,返回空或者拋異常。
如果使用者一直訪問一個資料庫不存在的資料,比如id為-1的資料,就會導致每次請求都會先去快取查一次,然後再去資料庫查一次,造成嚴重的效能問題。這種情況就叫快取穿透。
解決方案
以下幾種解決方案:
- 對請求引數做校驗,比如使用者鑑權校驗,id做基礎校驗,
id <= 0
的直接攔截。 - 如果查詢到資料庫沒有值,也將對應的key存進快取中,value為
null
。這樣下次查詢就直接從快取返回了。但這裡的key的快取時間應該比較短,比如30s。防止後面在資料庫插入了這條資料,而使用者獲取不到。 - 使用布隆過濾器,判斷一個key是否已經查過了,如果已經查過了,就不去資料庫查詢。
快取擊穿
快取擊穿指的是,一個key的訪問量非常大,比如某秒殺活動,有1w/s的併發量。這個key在某一時刻過期,那這些大量的請求就會一瞬間到資料庫,資料庫可能會直接崩潰。
解決方案
快取擊穿的解決方案也有幾種,可以配合使用:
- 對於熱點資料,慎重考慮過期時間,確保熱點期間key不會過期,甚至有些可以設定永不過期。
- 使用互斥鎖(比如Java的多執行緒鎖機制),第一個執行緒訪問key的時候就鎖住,等查詢資料庫返回後,把值插入到快取後再釋放鎖,這樣後面的請求就可以直接取快取裡面的資料了。
快取雪崩
快取雪崩指的是,在某一時刻,多個key失效。這樣就會有大量的請求從快取中獲取不到值,全部到資料庫。還有另一種情況,就是快取伺服器當機,也算做快取雪崩。
解決方案
針對上述兩種情況,快取雪崩有兩種解決方案:
- 對每個key的過期時間設定一個隨機值,而不是所有key都相同。
- 使用高可用的分散式快取叢集,確保快取的高可用性,比如redis-cluster。
雙寫不一致
在使用資料庫快取的時候,讀和寫的流程往往是這樣的:
- 讀取的時候,先讀取快取,如果快取中沒有,就直接從資料庫中讀取,然後取出資料後放入快取
- 更新的時候,先刪除快取,再更新資料庫
所謂雙寫不一致,就是在發生寫操作(更新)的時候或寫操作之後,可能會存在資料庫裡面的值和快取中的值不同的情況。
為什麼更新的時候要先刪除快取,再更新資料庫?因為如果先更新資料庫,然後在刪除快取的時候失敗了,就會造成快取裡面的值和資料庫的值不一致。
然而這樣並不能完全避免雙寫不一致問題。假設在大併發情景下,一個執行緒先刪除快取,然後取更新資料庫,這個時候另一個執行緒去取快取,發現沒有值,於是去讀資料庫,然後把資料庫舊的值設定進快取。等第一個執行緒更新完資料庫後,資料庫裡面就是新的值,而快取裡面是舊的值,所以就存在了資料不一致的問題。
一個比較簡單的解決辦法是把過期時間設定得比較低,這樣就只有在快取沒過期之前存在資料不一致問題,在一些業務場景下也還能接受。
另一種解決方案是使用佇列輔助。先更新資料庫,再刪除快取。如果刪除失敗,就放進佇列。然後另一個任務從佇列中取出訊息,不斷去重試刪除相應的key。
還有一種解決方案是使用對一個資料使用一個佇列,使讀寫操作序列化。比如對id為n的資料建立一個佇列。對這條資料的寫操作,刪除快取後,放進一個佇列;然後另一個執行緒過來了,發現沒有快取,則把這個讀操作也放進這個佇列裡面。
不過這樣會增加程式的複雜性,序列化也會降低程式的吞吐量,可能得不償失。一般主流的解決方案還是先刪除快取,再更新資料庫。可以滿足絕大部分需求。
認真寫文章,用心做分享。
個人網站:yasinshaw.com
公眾號:xy的技術圈