快取設計和使用中的經典問題

2021-10-10 13:07:21 字數 4341 閱讀 7062

該問題只是針對寫操作,讀操作併發量再大也不會出現資料不一致的情況。

下面有三種基本的解決方案:

該方案是老外提出來的,據說facebook就是採用的這種方式。

方案內容:

讀:先讀快取,若快取存在就直接返回;若快取不存在,就查詢資料庫,然後將查詢結果放到快取中。

寫:先更新資料庫,然後刪除快取。

為什麼是刪除快取,而不是更新快取?

更新快取後並不是每一次都會用到,等到需要的時候在去查詢資料庫,更新快取,體現的是一種懶載入思想(像 mybatis,hibernate,都有懶載入思想)。而且若快取需要經過複雜的計算,才會被放到快取,每次寫都進行複雜的計算,造成不必要的資源浪費。

比如乙個快取涉及的表的字段,在 1 分鐘內就修改了 20 次,或者是 100 次,那麼快取更新 20 次、100 次;但是這個快取在 1 分鐘內只被讀取了 1 次,有大量的經過複雜計算後的資料確沒被訪問。但如果只是刪除快取的話,那麼在 1 分鐘內,這個快取只不過在訪問的時候才被重新計算一次而已,開銷大幅度降低,用到快取才去算快取。

對於該方案刪除快取失敗的情況,我們還可以加入失敗重試機制保證成功機率,和設定資料的有效期保證資料的最終一致性。

讀:先讀快取,若快取存在就直接返回;若快取不存在,就查詢資料庫,然後將查詢結果放到快取中。

寫:先刪除快取,再更新資料庫。

該方案只在寫的順序上與cache aside pattern不一樣。這樣如果資料庫修改失敗了,那麼資料庫中是舊資料,快取中是空的。讀的時候發現快取中沒有,就將資料庫中的資料更新到快取中。

缺點:對同乙個資料進行併發操作時也會出現資料不一致問題。比如在併發量大的時候,資料庫還沒開始更新時,乙個讀的請求過來將原來舊的資料更新到快取中,這樣快取中是舊資料,資料庫中是新資料,資料還是不一致。針對這個問題,我們有2.2延時雙刪這種解決方案。

針對上述方案2,我們採用延時雙刪的方式解決併發造成的資料不一致情況,此方案應用後也會出現暫時的資料不一致情況。方案為更新資料庫之後,延時一段時間後,再刪除快取。偽**為:

cache.del(key); 

db.update(data);

thread.sleep(1200);

cache.del(key);

這樣即使在更新資料庫前,有讀的執行緒來將髒資料放入快取,也會在更新完成並延遲一段時間後,將快取中的髒資料刪除,下次讀取時也會將新的資料放入快取。也可以等資料庫更新完畢後,將剩下的任務放到訊息佇列中,採用非同步的方式繼續處理。

關於執行緒睡眠的時間,原則上應該比重進計算快取加設定到快取的時間總和多一點才行。若是mysql採用讀寫分離時,睡眠時間的計算上也應該把主從複製的時間算進去。

若要求資料必須實時正確的強一致性關係的話,那可以採用分布式鎖的方式,使所有的請求全都序列化執行(使讀和寫前都要先檢查是否能拿到鎖,以進行下一步的操作),但這樣系統中的某些介面的負載量將大幅下降,針對一些關鍵的地方可以使用這種方式。

綜上所述:

對於資料一致性要求不是很高的情況下,採用經典快取模式即可。對於某些資料一致性要求較高的情況下,我們輔助以分布式鎖的方式保證資料的一致性。

快取擊穿、快取穿透、快取雪崩按照對資料庫的破壞程度依次遞增。

快取擊穿是指快取中某個key不存在或key過期了,按照上面的快取模式,此時應該從db中讀取資料並更新到快取中。但若該key是個熱點key,將會有大量的讀請求衝向資料庫,一時間db壓力暴增大量請求阻塞,極度情況下db可能會宕掉。

剛開始針對某個key不存在的情況,可以採用快取預熱的方式,提前快取好資料。

針對key過期的情況,為了保護資料庫沒那麼大壓力,一般有以下幾種方式:

1、加互斥鎖,至於是加分布式鎖還是單機鎖看情況(集群分布式環境下用分布式鎖會好一些,單機時加單機鎖就好了)。就是讓乙個執行緒回寫快取,其他執行緒等待回寫快取執行緒執行完,重新讀快取即可。偽**如下:

object result = getdatafromcache(key);

if(result != null)elseelse }}

加鎖確實是個簡單粗暴的方法,有效的保護了db。但治標不治本,大併發情況下,會造成短時間大量請求阻塞問題。

2、熱點資料永不過期

即不給熱點key設定過期時間。

3、假的過期時間戳

即在設定快取時,給每個快取附件乙個比實際過期時間小的假的過期時間戳,每次請求時若達到了假的過期時間,就利用序列刪除或者用訊息佇列、非同步執行緒的方式,重新更新快取,這樣只要該key一直熱,他就一直有效。「你動我就動」,該方式在大併發下效能最好。

上面快取擊穿是cache中沒有,db中有。若此時db中也沒有,就變成快取穿透了。這樣按照快取模式的讀模式,每次請求都會將快取層和資料庫層全都穿透一遍,快取層完全失去了保護資料庫層的作用。系統正常設計的情況下,一般是不會出現快取穿透現象的,出現快取穿透的原因一般是惡意攻擊較多。知道了快取穿透的原因,解決起來也好辦了:

1、快取空值

快取空值是最簡單有效的方式了,當db中沒查著,回寫null值到快取中。當該值有更新的時候在重新回寫新的值到快取中。思路就和上面快取擊穿時加互斥鎖的**一樣。同時可以在本地使用乙個儲存空值對應key的快取區域,查詢時先檢查是否包含某個空值key,若存在都不用查詢快取了,等更新快取是再檢查是否需要從該區域移除該空值key。

2、布隆過濾器

布隆過濾器由乙個長度為m位元的位陣列(bit array)與k個雜湊函式(hash function)組成的資料結構。位陣列初始化均為0,所有的雜湊函式都可以分別把輸入資料盡量均勻地雜湊。由於雜湊碰撞的原因,布隆過濾器有個特點:bf認為不存在的資料一定不存在,但bf認為存在的資料可能不存在。

3、具體的小操作

比如:攔截id<=0,大於多少多少請求等

相比於快取擊穿和快取穿透是針對於單個key過期,快取雪崩是多個key大面積集中過期,致使大量請求直接砸在了db上,造成db瞬間壓力過大甚至癱瘓,對資料庫危害性最大。幾個比較有效的方法是:

1、均勻過期

均勻過期是解決快取雪崩比較有效的方法,即給每個資料設定不同的過期時間(可以在原有的過期時間上加上隨機時間如0-20分鐘之間)。

2、假的過期時間(還是比較通用的方法)

3、加互斥鎖(還是比較通用的方法)

4、設定二級快取(還是比較通用的方法)

5、資料永不過期

從字面意思就可以看出來,快取預熱就是將熱點資料,在正式對外提供服務之前預先放到快取中,避免了快取擊穿的問題。解決方法大致分為兩種:

2)資料量很大時,將快取預熱的工作單獨進行,比如可以單獨乙個專案專門用於快取預熱功能。

快取降級和分布式微服務中的服務降級是乙個目的,都是在系統訪問量巨大,導致系統的資源不足時,將一些非核心的業務暫時關閉或者限制它的一些非常消耗資源的功能,將更多地系統資源交給核心業務,要保證核心功能正常可用,算是乙個棄卒保帥、集中力量辦大事的思想吧。比如說我們可以限制快取為唯讀的,或者讀取一些本地的舊快取,甚至直接關閉該功能。

1、暫存(cache-aside pattern)

即將資料庫中的資料暫時儲存在快取中,訪問資料時先讀取快取,若快取中有直接返回;快取中沒有再讀取資料庫,然後同步資料到快取。寫操作時先更新資料庫,再刪除快取。快取的位置是處在資料庫之前,起到了加速讀取和保護資料庫的作用。

2、只有快取記憶體(cache-as-sor)

去掉資料庫,讓快取來充當資料庫的角色,所有的操作全在快取中進行。

3、直寫(write-through)

當發生寫操作時,所有的寫操作全都在乙個執行緒中序列執行。保證了資料的實時一致性,但每個寫操作的響應時間會加長,總的降低了系統的吞吐量。

4、後寫(write-behind)

當發生寫操作時,將一些寫操作非同步執行,比如對於複雜的寫操作另起乙個執行緒去非同步執行,或者像剩餘操作放到訊息佇列裡面,後續非同步執行。後寫不能保證資料的實時一致性,但可以保證資料的最終一致性,同時每個寫操作的響應時間會縮短,總的提高了系統吞吐量。

關於快取無非就是讀和寫,以上四種基本涵蓋了快取讀寫所有的情況,關於選用直寫還是後寫,可以從資料的實時一致性和系統的訪問量上來綜合考慮,針對具體的情況靈活運用即可。

其他問題遇到再說吧。

快取失效的經典問題

指查詢的資料,在資料庫不存在的情況。這樣程式會先訪問cache,沒有找到資料,再查詢資料庫,也沒有找到,然後返回空。如果惡意大量的訪問資料庫中不存在的資料,造成資源浪費,對資料庫造成壓力,甚至會導致業務不可用,甚至壓垮資料庫。空值快取,即如果資料庫查詢的時候,沒有查詢到資料,也將空值快取到cache...

快取設計需要考慮的問題

前言 沒有一種快取方案可以解決一切的業務場景或資料型別,我們需要根據自身的特殊場景和背景,選擇最適合的快取方案 一.是否需要使用快取 需要使用快取的業務場景 比如前台頁面展示,購物車 二.快取物件的粒度 一種資料乙個物件,簡單,讀取寫入都快,但是種類一多,快取的管理成本就會很高 多種資料放在乙個集合...

snarty中的快取問題

快取 1 提高訪問效率高 使用快取 實現類似頁面靜態化效果 美工和php 檔案合併後的編譯檔案形成靜態頁面 減少了 從資料庫獲取資料資訊,訪問資料庫的次數等 2 快取分類 l 頁面快取 步驟 在主配置檔案中宣告以下內容 開啟快取功能 cache 設定快取檔案所在路徑 設定快取的時間 s 瀏覽web ...