快取

oscarwin發表於2019-06-03

對於資料庫的CRUD操作而言,當併發量較大時會出現讀或者寫的瓶頸。對於大多數場景而言,都是讀多寫少,因此讀更容易成為資料庫的瓶頸。而快取就是為了解決讀的問題而出現的。快取的資料儲存在記憶體中,因此效能很高。

快取更新方案

快取的更新方式從大的方向分可以分為同步更新快取和非同步更新快取

同步更新快取

同步更新快取就是寫資料或者讀資料的時候同步更新快取。

讀的時候更新快取

快取

讀的時候更新快取策略很簡單,如上圖所示,主要有以下幾個步驟:

  1. 讀請求時,如果快取資料存在則直接返回該資料;
  2. 讀請求時,如果快取資料不存在則從資料庫中讀入資料並寫入快取,然後返回資料;
  3. 寫請求時,寫入資料庫成功後,刪除快取

寫的時候更新快取

快取

寫的時候更新快取與讀的時候更新快取原理類似,只是在寫資料時候會先寫資料庫,然後寫快取,而不是刪除快取。

接下來我們對比一下這兩種方式的優缺點。

讀的時候更新快取在資料寫入資料庫後只需要刪除快取即可,操作比較簡單,因此邏輯上會簡單一些,這種方式是最常見的快取更新方式。但是讀請求的時候要先讀資料庫然後寫入快取,如果是一個影響很大的更新,那麼快取失效後的第一次讀請求可能會比較慢。比如常見的好友列表,如果快取失效,需要從資料庫先從關係連結串列查好友的關係鏈,然後去使用者表查每個好友的頭像和暱稱,最後將資料還要寫入快取,這個過程可能會比較耗時。

而寫的時候更新快取,只需要將同樣的更新資料先寫入資料庫,然後寫一遍快取,不用從資料庫中取出來然後寫入快取。不過使用這種方式的時候,讀請求的時查詢快取沒有命中,然後查資料庫的邏輯不能省,因為快取還會因為過期而失效。

這兩種方式都有一個問題,寫請求時寫入資料庫成功,然後同步寫入快取或者刪除快取這兩個動作都可能失敗,如果失敗就會導致資料庫中的資料與快取中的資料不一致。首先,可以採取重試的策略來儘可能減小出現的概率,而且儘量要給快取設定一個過期時間,這樣可以使快取中的資料與資料庫中的資料達到最終一致性。

非同步更新快取

同步更新快取需要在業務邏輯裡單獨處理這一段邏輯,而其本身與業務邏輯是不相關的,我們只能為了提升效能而引入了快取系統。因此可以考慮通過非同步的方式更新快取,將快取更新的服務與業務服務進行解耦。而且非同步更新的方式,將快取更新的操作單獨用一個服務來實現,因此讀寫請求減少了快取更新的邏輯,效能會得到提升。

先寫DB,非同步MQ更新快取

快取

一個簡單的非同步快取更新方案入上圖所示,寫請求寫完資料庫後會拋一個MQ訊息,然後有一個獨立的快取更新服務區接受這個訊息,然後從資料庫讀資料並寫入快取。採用非同步的方案以後,資料無需同步寫入,減輕了業務服務的邏輯任務,在業務場景下可能很多個地方都需要更新快取,採用非同步更新發訊息很方便。不過這裡需要依賴中介軟體訊息佇列,需要訊息佇列能保證不丟訊息。快取更新服務中也會存在快取更新失敗的情況,不過我們可以採用不斷重試的方案來避免這樣的問題。

但是上面這個設計會有一些問題,主要是在併發情況下。

問題1:如果先有一個寫請求更新了資料庫的資料,然後丟擲一條MQ訊息。但是在這個MQ訊息被處理前,這時候一條讀請求被髮起了,那麼這個時候讀請求會讀到快取中的舊資料。

問題2:如果先有一個寫請求更新了資料庫的資料,並拋訊息MQ1。然後接著有另一個寫請求緊跟著也更新了資料,並拋訊息MQ2。如果MQ1和MQ2序列執行,那麼就沒有問題。但是分散式環境下,服務是多機多程式部署,因此MQ2可能比MQ1先被處理。考慮這種極端條件下,如果第二次寫請求前,MQ1的訊息已經到達快取更新服務並從資料庫中取出訊息。就在這時,MQ2訊息到達被另一個程式處理,從資料庫中取出資料並先於MQ1訊息更新了快取,然後這時MQ1訊息取出的資料寫入快取就覆蓋了MQ2訊息的更新的資料。這時候快取中的資料也與資料庫中的資料不一致了。

如果對快取中的資料與資料庫中的資料的一致性要求非常高,可以引入髒標和版本號的機制來實現。如果完全不能接受快取中資料與資料庫資料不一致,就不要使用快取。

快取

  1. 在更新資料到資料庫之前先寫一個髒標來標識快取中的資料是髒的。髒標是用來解決問題1的。如果寫髒標失敗,則本次請求失敗。如果寫髒標成功,但是寫資料庫失敗,本次請求也失敗,這會導致快取失效,下次讀請求時發現快取中的資料是髒資料,然後去讀資料庫。髒標寫成功,資料也寫成功,但是發訊息失敗,則由下一次讀請求來更新快取。為了避免髒標刪除失敗而導致快取雪崩,最好給髒標設定一個過期時間。
  2. 給每一條資料都維護一個版本號,每次更新資料庫都將版本號加1。版本號是用來解決問題2的。更新快取之前先判斷快取中資料的版本號與資料庫中資料的版本號,如果將要寫入快取中的資料的版本號大於快取中資料的版本號則說明要更新的資料更新,此時更新快取。如果資料庫中資料的版本號小於快取中資料的版本號則說明要更新的資料比快取中的資料更舊或者資料相同,此時不更新快取。
  3. 基於版本號的更新可以用redis的lua指令碼來實現原子性
local cache_info = redis.call('GET', KEYS[1])
local cache_version = redis.call('GET', KEYS[2])
if(type(cache_version) ~= 'string' or 
   type(cache_info) ~= 'string' or 
   tonumber(cache_version) < tonumber(ARGV[1])) 
then 
   redis.call('SET', KEYS[2], ARGV[1], 'EX', ARGV[3])
   return redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
else 
   return 0 
end
複製程式碼

ps:KEYS[1]是快取資料的key,KEYS[2]是版本號的key,ARGV[1]是更新後的快取資料,ARGV[2]是更新後的版本號,ARGV[3]是key的過期時間。

先寫快取,非同步將髒資料刷到資料庫

先寫快取然後非同步將資料刷到資料庫的方法與作業系統的檔案系統的讀寫核心流程是相同的。對於作業系統的檔案系統,由於記憶體操作與磁碟操作存在百萬數量級的差別,因此作業系統的檔案系統維護了一個快取記憶體區來減小這種巨大差距帶來的影響。檔案系統讀操作時,先查詢快取記憶體區是否存在資料,如果沒有則從磁碟讀入快取記憶體區。寫資料時,將資料寫入快取記憶體區,系統呼叫write就返回成功了。然後通過一個名為update的後臺程式,不斷的呼叫sync將快取記憶體區的內容寫入磁碟。

先寫快取,然後非同步將資料刷到資料庫的方案流程圖如下:

快取

該方案的好處是,讀寫都是走快取,因此資料極快,可以應對極高的併發請求。不過這種方案會導致快取中資料與資料庫中資料存在不一致的時間段,更為嚴重的是如果機器當機,還沒寫入資料庫的髒資料會丟失。如果要避免資料丟失,還可以使用雙快取的方案,不過這有會是系統更加複雜,維護一致性更加困難。

快取中存在常見問題

快取穿透

一般情況下查詢資料,資料都是存在的。大部分業務系統都需要給使用者建立一個賬戶,如果一個新使用者去查詢使用者資訊,資料庫中不存在這個使用者的資訊,系統會返回前端說明是一個新使用者。正常情況下,這樣沒有問題。如果有人利用這個漏洞,用很多個這種新使用者的賬號,不斷請求使用者系統的介面,所有的請求都會打到DB上,會DB帶來很大的壓力,甚至當機。

像這種查詢系統中壓根不存在的資料,使請求落到DB上的情況,被稱為快取穿透。

對於快取穿透常用解決方案有兩個:快取和空值和布隆過濾器

快取空值

快取空值的方法,正如其名,當查詢到資料不存在時,向快取的key中寫入null。當查詢到該key存在,且值為null時,按資料存在處理。

布隆過濾器

快取

第二種方案是在前一種方案之前再加一層布隆過濾器,如果布隆過濾器能命中,則查快取,如果布隆過濾器沒有命中,則直接返回。布隆過濾器的特點是如果資料存在則布隆過濾器一定會命中,如果資料不存在則布隆過濾器絕大多數情況下不會被命中。因此,即使有部分不存在的資料通過了布隆過濾器的過濾,還是會被空值快取攔截住。

第二種方案是在第一種方案的基礎上形成了,因此第二種方案複雜一些,但是如果有大量不存在的資料被快取會浪費快取的空間,而布隆過濾器能過濾掉絕大多數這樣的情況。因此,如果為null的key的數量不是很多,直接用第一種方法即可,反之,如果為null的key的數量很多,則建議加一層布隆過濾器。

快取洞穿

在高併發下,當快取資料失效的一瞬間,這時所有的請求都會打到DB上,造成DB瞬時壓力陡增,這就是快取洞穿。

防止快取洞穿的方法是當發現快取失效時,在查詢DB之前先加鎖,這樣第一個取到鎖的執行緒更新快取,其他執行緒因為取不到鎖會等待。等到一個執行緒更新快取成功後,其他執行緒就可以從快取中查詢資訊了。

快取雪崩

快取雪崩是指同一時間快取大規模失效,導致請求都直接打到DB上,瞬間的流量將DB打掛,導致整個系統崩潰,這種情況就是快取雪崩。比如快取機器當機或者重啟時都可能導致快取雪崩。

對於快取雪崩首先採用快取叢集的方案來增加容錯性,如果使用redis做快取,可以使用主從+哨兵的部署來方案來提高可用性,避免快取大量失效的問題發生。

對於微服務架構,雪崩已經發生的情況,可以使用開源的Hystrix實現降級和限流,避免DB當機。但是Hystrix不具備很好的通用性,對於spring cloud可以比較方便的使用,對於其他語言下該怎麼做呢?微服務治理的新趨勢是使用server mesh,通過server mesh來避免服務雪崩。server mesh具有更好的通用性,而且對語言完全相容。

熱點資料失效

大量熱點快取資料同時失效,導致大量請求直接打到DB上。對於熱點資料同時失效的問題,可以在過期時間上,加上一個隨機值,避免快取同時失效。

總結

本篇文章,總結了自己對快取知識的認識,介紹了四種常見的快取方案,每種方案各有優劣,需要根據業務需求來選擇合理的方案。然後介紹了使用快取時可能遇到的幾個問題,並總結了常見的解決方案。

相關文章