1. 程式人生 > 資料庫 >Redis快取系列--(六)快取和資料庫一致性更新原則

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("default", 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("default", 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刪除失敗的記錄進行重試刪除操作。這樣,就能夠保證資料庫和快取資料的最終一致性了。

如果擔心資料庫中因為添加了這樣一個重試刪除機制而變得很複雜,可以將這個重試機制交給一個非業務的訊息佇列元件來實現。

強一致性的解決方案

  • 新增第三方事務操作
    我們可以把資料庫和快取的操作新增到同一個事務中,從而達到資料更新的原子性。

  • 通過分散式鎖來實現
    我們也可以通過新增分散式鎖,將要更新的資源進行一個分散式加鎖的操作,實現資源操作的互斥性,等資料庫和快取的資料操作完成後再將此資源鎖釋放掉。

總結

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