分布式快取是現在很多分布式應用中必不可少的組件,但是用到了分布式快取,就可能會涉及到快取與資料庫雙存盤雙寫,你只要是雙寫,就一定會有資料一致性的問題,那么你如何解決一致性問題?
Cache Aside Pattern
最經典的快取+資料庫讀寫的模式,就是 Cache Aside Pattern,讀的時候,先讀快取,快取沒有的話,就讀資料庫,然后取出資料后放入快取,同時回傳回應,更新的時候,先更新資料庫,然后再洗掉快取,
為什么是洗掉快取,而不是更新快取?
原因很簡單,很多時候,在復雜點的快取場景,快取不單單是資料庫中直接取出來的值,比如可能更新了某個表的一個欄位,然后其對應的快取,是需要查詢另外兩個表的資料并進行運算,才能計算出快取最新的值的,
另外更新快取的代價有時候是很高的,是不是說,每次修改資料庫的時候,都一定要將其對應的快取更新一份?也許有的場景是這樣,但是對于比較復雜的快取資料計算的場景,就不是這樣了,如果你頻繁修改一個快取涉及的多個表,快取也頻繁更新,
但是問題在于,這個快取到底會不會被頻繁訪問到?舉個栗子,一個快取涉及的表的欄位,在 1 分鐘內就修改了 20 次,或者是 100 次,那么快取更新 20 次、100 次;但是這個快取在 1 分鐘內只被讀取了 1 次,有大量的冷資料,
實際上,如果你只是洗掉快取的話,那么在 1 分鐘內,這個快取不過就重新計算一次而已,開銷大幅度降低,用到快取才去算快取,其實洗掉快取,而不是更新快取,就是一個 lazy 計算的思想,不要每次都重新做復雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算,像 mybatis,hibernate,都有懶加載思想,
查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都里面的 1000 個員工的資料也同時查出來啊,80% 的情況,查這個部門,就只是要訪問這個部門的資訊就可以了,先查部門,同時要訪問里面的員工,那么這個時候只有在你要訪問里面的員工的時候,才會去資料庫里面查詢 1000 個員工,
最初級的快取不一致問題及解決方案
問題:先修改資料庫,再洗掉快取,如果洗掉快取失敗了,那么會導致資料庫中是新資料,快取中是舊資料,資料就出現了不一致,

解決思路:先洗掉快取,再修改資料庫,如果資料庫修改失敗了,那么資料庫中是舊資料,快取中是空的,那么資料不會不一致,因為讀的時候快取沒有,則讀資料庫中舊資料,然后更新到快取中,
比較復雜的資料不一致問題分析
資料發生了變更,先洗掉了快取,然后要去修改資料庫,此時還沒修改,一個請求過來,去讀快取,發現快取空了,去查詢資料庫,查到了修改前的舊資料,放到了快取中,隨后資料變更的程式完成了資料庫的修改,
完了,資料庫和快取中的資料不一樣了,,,為什么上億流量高并發場景下,快取會出現這個問題?只有在對一個資料在并發的進行讀寫的時候,才可能會出現這種問題,其實如果說你的并發量很低的話,特別是讀并發很低,每天訪問量就 1 萬次,那么很少的情況下,會出現剛才描述的那種不一致的場景,
但是問題是,如果每天的是上億的流量,每秒并發讀是幾萬,每秒只要有資料更新的請求,就可能會出現上述的資料庫+快取不一致的情況,
解決方案如下:
更新資料的時候,根據資料的唯一標識,將操作路由之后,發送到一個 jvm 內部佇列中,讀取資料的時候,如果發現資料不在快取中,那么將重新讀取資料+更新快取的操作,根據唯一標識路由之后,也發送同一個 jvm 內部佇列中,
一個佇列對應一個作業執行緒,每個作業執行緒串行拿到對應的操作,然后一條一條的執行,這樣的話,一個資料變更的操作,先洗掉快取,然后再去更新資料庫,但是還沒完成更新,此時如果一個讀請求過來,讀到了空的快取,那么可以先將快取更新的請求發送到佇列中,此時會在佇列中積壓,然后同步等待快取更新完成,
這里有一個優化點,一個佇列中,其實多個更新快取請求串在一起是沒意義的,因此可以做過濾,如果發現佇列中已經有一個更新快取的請求了,那么就不用再放個更新請求操作進去了,直接等待前面的更新操作請求完成即可,
待那個佇列對應的作業執行緒完成了上一個操作的資料庫的修改之后,才會去執行下一個操作,也就是快取更新的操作,此時會從資料庫中讀取最新的值,然后寫入快取中,
如果請求還在等待時間范圍內,不斷輪詢發現可以取到值了,那么就直接回傳;如果請求等待的時間超過一定時長,那么這一次直接從資料庫中讀取當前的舊值,
高并發的場景下,該解決方案要注意的問題:
1、讀請求長時阻塞由于讀請求進行了非常輕度的異步化,所以一定要注意讀超時的問題,每個讀請求必須在超時時間范圍內回傳,該解決方案,最大的風險點在于說,可能資料更新很頻繁,導致佇列中積壓了大量更新操作在里面,然后讀請求會發生大量的超時,最后導致大量的請求直接走資料庫,
務必通過一些模擬真實的測驗,看看更新資料的頻率是怎樣的,另外一點,因為一個佇列中,可能會積壓針對多個資料項的更新操作,因此需要根據自己的業務情況進行測驗,可能需要部署多個服務,每個服務分攤一些資料的更新操作,如果一個記憶體佇列里居然會擠壓 100 個商品的庫存修改操作,每隔庫存修改操作要耗費 10ms 去完成,那么最后一個商品的讀請求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到資料,這個時候就導致讀請求的長時阻塞,
一定要做根據實際業務系統的運行情況,去進行一些壓力測驗,和模擬線上環境,去看看最繁忙的時候,記憶體佇列可能會擠壓多少更新操作,可能會導致最后一個更新操作對應的讀請求,會 hang 多少時間,如果讀請求在 200ms 回傳,如果你計算過后,哪怕是最繁忙的時候,積壓 10 個更新操作,最多等待 200ms,那還可以的,如果一個記憶體佇列中可能積壓的更新操作特別多,那么你就要加機器,讓每個機器上部署的服務實體處理更少的資料,那么每個記憶體佇列中積壓的更新操作就會越少,
其實根據之前的專案經驗,一般來說,資料的寫頻率是很低的,因此實際上正常來說,在佇列中積壓的更新操作應該是很少的,像這種針對讀高并發、讀快取架構的專案,一般來說寫請求是非常少的,每秒的 QPS 能到幾百就不錯了,實際粗略測算一下如果一秒有 500 的寫操作,如果分成 5 個時間片,每 200ms 就 100 個寫操作,放到 20 個記憶體佇列中,每個記憶體佇列,可能就積壓 5 個寫操作,
每個寫操作性能測驗后,一般是在 20ms 左右就完成,那么針對每個記憶體佇列的資料的讀請求,也就最多 hang 一會兒,200ms 以內肯定能回傳了,經過剛才簡單的測算,我們知道,單機支撐的寫 QPS 在幾百是沒問題的,如果寫 QPS 擴大了 10 倍,那么就擴容機器,擴容 10 倍的機器,每個機器 20 個佇列,
2、讀請求并發量過高這里還必須做好壓力測驗,確保恰巧碰上上述情況的時候,還有一個風險,就是突然間大量讀請求會在幾十毫秒的延時 hang 在服務上,看服務能不能扛的住,需要多少機器才能扛住最大的極限情況的峰值,但是因為并不是所有的資料都在同一時間更新,快取也不會同一時間失效,所以每次可能也就是少數資料的快取失效了,然后那些資料對應的讀請求過來,并發量應該也不會特別大,
3、多服務實體部署的請求路由可能這個服務部署了多個實體,那么必須保證說,執行資料更新操作,以及執行快取更新操作的請求,都通過 Nginx 服務器路由到相同的服務實體上,比如說,對同一個商品的讀寫請求,全部路由到同一臺機器上,可以自己去做服務間的按照某個請求引數的 hash 路由,也可以用 Nginx 的 hash 路由功能等等,
4、熱點商品的路由問題,導致請求的傾斜萬一某個商品的讀寫請求特別高,全部打到相同的機器的相同的佇列里面去了,可能會造成某臺機器的壓力過大,就是說,因為只有在商品資料更新的時候才會清空快取,然后才會導致讀寫并發,所以其實要根據業務系統去看,如果更新頻率不是太高的話,這個問題的影響并不是特別大,但是的確可能某些機器的負載會高一些,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/121028.html
標籤:PHP
上一篇:編程
