Redis緩存系列--(六)緩存和數據庫一致性更新原則


緩存和數據庫一致性更新原則

緩存是一種高性能的內存的存儲介質,它通過key-value的形式來存儲一些數據;而數據庫是一種持久化的存儲復雜關系的存儲介質。使用緩存和數據庫結合的模式就使得軟件系統的性能得到了更好的提升(更好的存儲介質,更貼近請求的存儲距離,比如本地緩存),並且給系統提供了更簡便的數據抽象。
緩存和數據庫一致性更新的本質就是要保證用戶訪問緩存和數據庫中的數據都是一樣的!

數據一致性的必要性

那么為什么需要一致性的更新呢?下面我們舉一個應用場景來說明一下,具體場景如下圖:
緩存和數據庫數據不一致示例
A用戶下訂單時,查詢了物品庫存緩存,目前庫存為1,於是拍了貨物之后將緩存刪除,緩存中已經沒有了庫存數據,但是還沒有更新數據庫的庫存數據(庫存仍為1);B用戶此時(A用戶還沒更新數據庫庫存前)查詢庫存時查到緩存中沒有了庫存數據就去查詢數據庫的庫存,查詢到有庫存,於是又更新了緩存的庫存數據為1。在B更新了庫存緩存之后,A此時才去更新數據庫中庫存的數據為0。這樣,就造成了數據庫庫存信息和緩存庫存信息不一致的情況。
這樣類似的場景很多,數據的不一致會導致很多異常的請求從而造成不可預料的后果。那么從刪除緩存到更新數據庫這個中間過程,為什么會有這么長的時間呢?原因有以下幾種可能:

  1. Java編程語言中,JVM可能在這段時間進行了GC,這樣就會導致系統出現短暫的暫停;
  2. 在虛擬化的環境中,虛擬機可能被運維掛起或者遷移,這樣也會造成系統的暫停;
  3. 如果系統負載過高,服務器上的操作系統會因為上下文的切換,造成這兩個操作之間的時間變長,從而導致有其他的請求在此段時間內操作;
  4. 如果應用程序進行同步磁盤的IO操作,同樣也會導致這兩個操作之間的操作時間過長;
  5. 當Redis的內存剩余空間很少的時候,對Redis的操作也有可能很耗時;
  6. 還有Redis操作時因為網絡的原因,導致操作時間過長

常見的緩存訪問模式

cache aside模式

這種訪問模式,緩存和數據庫時相互分開的。這種訪問模式又有以下幾種:

  1. 應用服務中有本地緩存
    本地緩存的優點是:
  • 應用服務訪問緩存的速度會很快;
  • 緩存是針對每個應用服務的,更加精細;
  • 應用服務在維護的時候就可以對緩存進行維護,減少了緩存的運維成本
    本地緩存的缺點:
  • 當有很多應用服務的業務類似或者相同的時候,那么這樣的話本地緩存就會顯得很臃腫多余。這樣其實可以使用緩存中間件來替代本地緩存。
  • 本地緩存對於應用服務來說,就會增加維護的成本
  • 如果一個應用服務有多個集群,如果這個應用中有本地緩存的話,它的容量不會太大,這樣會因為服務訪問輪詢,對緩存失效的數據進行剔除,這樣就會造成數據庫的訪問壓力變大。
  1. 使用緩存中間件
    使用緩存中間件,有兩種架構模式,如下圖:
    緩存中間件架構圖

這兩種緩存架構各有優缺點,左邊的就是省去了中間的數據訪問的網絡開銷,吞吐量相對來說比第二種要大,但是它會增加應用服務的數據訪問邏輯,對應用來說難度增大;右邊的相對來說省去了應用服務的數據讀取環節,對應用服務很友好,統一了數據讀取的邏輯處理,但是它自己的服務吞吐量成為了一個瓶頸,並且增加了應用服務網絡的開銷。

Cache-Aside訪問模式的操作方式

那么Cache-Aside訪問模式具體的操作方式有哪幾種呢?

  1. 緩存的讀取方式
    一般Redis緩存的讀取步驟如下:① 數據訪問服務先讀取緩存,查找所要查找的數據;②緩存查找成功,則直接返回;查找失敗則繼續查找數據庫;③查找數據庫數據成功后,將查找數據寫入緩存中從而方便之后相同數據的查找。

它在spring中的具體代碼實現如下:

@Cacheable("default", key="#search.keyword")
public Record getRecordForSearch(Search search)
  1. 緩存的更新模式
    當用戶要更新數據庫的數據時,就需要對於Redis的緩存進行更新,對於緩存的更新有以下幾種不同的方式。

    • 方式1:先更新緩存,再更新數據庫

      這種方式下,假如用戶在更新了緩存數據后,更新數據庫失敗了,那么就會導致緩存數據和數據庫數據不一致的情況。

      下面我們來介紹一種此模式下數據不一致的情況:如果在A用戶查找一條數據D1(舊數據)時,緩存中沒有查找到數據D1;在A沒查到緩存之后,B用戶需要更新數據D1,B先更新緩存數據D1(新數據);B更新緩存之后,A用戶才去數據庫查找該數據並且查找到了數據D1(舊數據),將D1(舊數據)寫入緩存中;A查找到數據之后,B(因為上邊講的六種緩存和讀取數據庫中間時間過長的原因)才開始更新數據庫,將D1(新數據)更新到數據庫中。這樣就又造成了數據不一致的狀況,請看這種情況的具體圖解:

      先更新緩存后更新數據庫的異常情況

    • 方式2:先更新數據庫,再更新緩存

      這種方式下,數據的更新更加可靠,因為數據庫更新成功,數據時持久存在的。但是有一種情況,數據庫更新成功,緩存更新失敗,下一次用戶來讀取數據時,先讀取緩存讀到的是舊數據,還是會出現數據不一致的情況;如果數據庫更新成功,即使緩存服務出現異常,其他用戶讀取同樣的數據時會在讀取數據庫后來更新緩存的數據;

      下面我們來介紹一種此模式下數據不一致的情況:

      用戶A去更新了數據庫的數據D1(A修改后的數據)后,用戶B又更新了數據庫的數據D1(B修改后的數據),接着又更新了緩存中的數據D1(B修改后的數據);在B更新了緩存中的數據D1后,此時用戶A才更新緩存中的數據D1(A修改后的數據)。這樣一來,B用戶覆蓋了A用戶更新在數據庫的數據,A用戶覆蓋了B用戶在緩存中更新的數據,導致了緩存和數據庫的數據又出現不一致情況。

在spring中,更新緩存的具體實現如下:

/**
注:更新緩存時會更新緩存對象的所有參數
*/
@CachePut(value="tb_record", key="#search.keyword)
public Record updateRecordForSearch(Search search)
  1. 緩存的刪除模式

    • 方式1:先刪除緩存,再更新數據庫

      這種方式在正常的情況下,使數據的讀取更加及時有效。因為它能在用戶更新的第一時間刪除掉舊的緩存數據,將最新數據更新到數據庫中去,但是它會造成讀取數據庫的壓力變大。如果刪除緩存數據不成功,則會造成用戶讀取的緩存數據為舊數據,這樣就會造成數據的不一致情況

      下面我們來介紹一種此模式下數據不一致的情況:

      用戶A去更新數據D1時,先刪除了緩存中的數據D1;然后此時用戶B來讀取數據D1時,查詢緩存中沒有D1數據,然后就去數據庫查詢到了數據D1(舊的數據D1),隨后將數據D1重新寫入緩存中(舊數據);這時(由於上文提到的6種原因),用戶A才去更新數據庫的D1數據(A修改后的數據),這樣就造成了數據不一致的情況。

    • 方式2:先更新數據庫,再刪除緩存

      這種方式能夠及時的將最新數據更新到數據庫中,能夠保證持久化數據更新的及時性。但是用戶讀取數據時,可能讀取的數據不是最新的;如果用戶再更新數據時沒有刪除緩存成功,也會造成緩存中的數據不是最新的數據。

      下面我們來介紹一種此模式下數據不一致的情況:

      用戶A來讀取數據D1,因為緩存失效的原因讀取Redis緩存沒有找到D1數據,然后讀取了數據庫中的數據D1;此時,用戶B來更新數據D1,他先更新了數據庫的數據D1(用戶B更改后的),然后刪除了緩存中的數據;這時,用戶A(因為上邊提到的6中原因)才對緩存進行寫入操作,但它的緩存卻是舊數據D1的緩存,這樣就又造成了數據不一致的情況。

在spring中該模式的實現代碼如下:

@CacheEvict(value="tb_record", key="#search.keyword")
public Record updateRecordForSearch(Search search)

Cache Through模式

Cache Through模式中,緩存和數據庫的操作是一個整體。應用需要先經過緩存,緩存來處理讀和寫的處理並且來代理數據庫的讀寫操作

Read Through讀取模式

這種模式下緩存和數據庫是交互的,在數據讀取的時候步驟如下:

  1. 先讀取緩存,如果緩存存在則直接返回數據。
  2. 如果緩存不存在,則查詢數據庫中的數據,如果數據庫中有則將數據寫入緩存中並返回查詢結果;如果數據庫中也不存在,則返回數據不存在。

Write Through寫入更新模式

數據寫入或者更新的時候步驟如下:

  1. 查找緩存中是否有該數據,如果有該數據則更新緩存數據,然后再更新數據庫的數據(更新緩存和數據庫是一個同步操作)
  2. 如果該數據緩存不存在,則更新數據庫的數據

Cache Through流程圖如下:
Cache Through模式流程圖

Write Behind寫入更新模式

Write Behind模式,數據寫入或更新的時候,先對緩存進行更新,然后通過異步方式在某個時間點收集所有的寫操作批量寫入或更新。因為是異步的方式進行數據的更新,所以這里就會使緩存的數據和數據庫數據不一致的情況,所以當緩存數據和數據庫數據庫不一致時,緩存中的數據被標記為"dirty"。它的大概流程如下:
write-behind模式流程圖

一致性更新目標

一致性更新的目標分為兩種:一種是最終一致性強一致性最終一致性它的結果是在系統能夠容忍的時間內最終達到Redis緩存和數據庫數據的一致性,它的性能會更好一點,但這種方式顯然會在某一時間段內緩存和數據庫的數據是不一致的,需要根據業務需求的容忍度來進行適當采用;強一致性則保證緩存和數據庫的數據是絕對一致的,也就是說緩存和數據庫的數據更新操作可以看做一種原子性操作,實現相對比較復雜。這種方案常用在對數據一致性要求較高的場景,它的弊端就是性能沒有最終一致性的方案好。

最終一致性的解決方案

  • 設置緩存過期時間
    最終一致性可以通過設置緩存的過期時間來實現。也就是說在緩存過期以后,用戶再次讀取數據就從數據庫中讀取最新的數據,然后再更新到緩存中,從而達到緩存和數據庫最終的一致性。為了避免緩存在同一時間過期從而導致雪崩,我們可以根據業務可以容忍的過期時間范圍設置隨機的緩存過期時間

  • 異步更新
    數據訪問服務的所有讀操作都來讀Redis緩存,而所有的寫操作都直接通過數據庫來進行操作。然后在數據庫和緩存之間增加一個數據同步服務,定時的將數據庫的數據同步到Redis緩存中。這里有一個弊端就是,Redis緩存的容量也會隨數據庫數據量的增大而增大

  • 重試機制
    在數據的更新操作中,先進行數據庫數據的更新,然后再刪除Redis的緩存。如果刪除緩存失敗,就將刪除失敗的記錄加入到消息隊列中,然后消費消息隊列中的Redis刪除失敗的記錄進行重試刪除操作。這樣,就能夠保證數據庫和緩存數據的最終一致性了。

如果擔心數據庫中因為添加了這樣一個重試刪除機制而變得很復雜,可以將這個重試機制交給一個非業務的消息隊列組件來實現。

強一致性的解決方案

  • 添加第三方事務操作
    我們可以把數據庫和緩存的操作添加到同一個事務中,從而達到數據更新的原子性。

  • 通過分布式鎖來實現
    我們也可以通過添加分布式鎖,將要更新的資源進行一個分布式加鎖的操作,實現資源操作的互斥性,等數據庫和緩存的數據操作完成后再將此資源鎖釋放掉。

總結

關於具體的每種模式的實現,我會在后邊的文章中繼續補充,如果有解釋不對的地方,還請大家不吝賜教。謝謝~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM