Redis緩存數據一致性

Redis緩存數據一致性

在互聯網行業,使用緩存來提升應用的性能已經是一件非常常見的手段,但是如何保證緩存與數據庫的一致性確不是一件容易的事。比如下面的場景都可會導致數據不一致性。

  • 場景1:更新數據庫成功,更新緩存失敗,數據不一致;
  • 場景2:更新緩存成功,更新數據庫失敗,數據不一致;
  • 場景3:更新數據庫成功,清除緩存失敗,數據不一致;
  • 場景4:清除緩存成功,更新數據庫失敗,數據弱一致;

緩存和數據庫是兩類不同的存儲資源,如果要追求絕對的數據一致性,唯一的辦法就是分佈式事務。但使用分佈式事務又會引入嚴重的寫入性能損耗,在大多數情況下,業務上是無法接受這樣的損耗的。所以更多的時候,我們追求的是數據的最終一致性,一種比較折中的實現是這樣的:

寫操作:

1. 清除緩存;若失敗則返回錯誤信息(本次寫操作失敗)。

2. 更新數據庫;若失敗則返回錯誤信息(本次寫操作失敗),此時數據弱一致。

3. 更新緩存,即使失敗也返回成功,此時數據弱一致。

讀操作:

1. 查詢緩存,命中則直接返回結果。

2. 查詢數據庫,將結果直接寫入緩存,返回結果。

這種實現簡單明瞭,尤其是讀操作,一看即明白。對於寫操作,會有朋友問為什麼第一步要先清除緩存。大家可以想想,如果去掉第一步,那麼寫操作就可能發生最開始我們提到的場景1的情況:更新數據庫成功,更新緩存失敗,數據不一致。如果在寫操作的第一步先清除緩存,對於場景1的情況,那結果會是數據庫中有值,而緩存中無值,即數據弱一致,並不會造成業務錯誤。

如果你認為上面的實現已經完美,那你可能會失望了。在併發場景中,它並不安全。我們看一個簡單的例子:假如有一個用戶,它的賬戶中有100塊錢。現在有兩個併發的請求:請求1為寫操作,更新用戶的餘額,從100更新為200;請求2為查詢操作,查詢用戶的餘額。由於是併發的,兩個請求之間的執行順序是不確定的,我們來看一下下面的執行順序:

  1. 請求1首先清除用戶的緩存。
  2. 接著請求2查詢緩存,由於緩存中沒有數據,請求2繼續查詢數據庫,得到餘額為100。
  3. 請求1更新數據庫,並將結果寫入緩存。此時,數據庫與緩存中的餘額都是200。
  4. 請求2將數據庫查詢結果100寫入緩存。
  5. 最終,餘額在數據庫中是200,而在緩存中是100,數據不一致。

造成這樣的結果,原因有兩個方面:一是寫操作中更新數據庫與更新緩存是兩個操作,而不是一個原子操作;二是讀操作中讀取數據庫和寫入緩存兩個操作不是原子的。要解決這個問題,需要做一些修改,引入分佈式鎖:

寫操作:

1.清除緩存;若失敗則返回錯誤信息(本次寫操作失敗)。

2.對key加分佈式鎖。

3.更新數據庫;若失敗則返回錯誤信息(本次寫操作失敗)同時釋放鎖,此時數據弱一致。

4.更新緩存,即使失敗也返回成功,同時釋放鎖,此時數據弱一致。

讀操作:

1.查詢緩存,命中則直接返回結果。

2.對key加分佈式鎖。

3.查詢數據庫,將結果直接寫入緩存,返回結果,同時釋放鎖。

引入分佈式鎖後的實現,之前的併發引起的問題不復存在,讀者可以自行驗證。不過我們仔細分析一下讀操作的實現,其實它還可以進一步的優化。如果第二步加鎖的時候失敗了,意味著同一時刻,有別的請求在進行同一個key的寫操作或讀操作,不論怎樣,在別的請求完成之後,緩存中應該已經有(當然也可能沒有,寫操作和讀操作最後更新緩存失敗的情況下)我們需要的數據了,這時我們只需要等待一會再重新查詢緩存即可,所以更優的讀操作的實現:

  1. 查詢緩存,命中則直接返回結果。
  2. 對key加分佈式鎖。如果加鎖失敗,則等待一會再重新跳回第1步開始重新執行。
  3. 查詢數據庫,將結果直接寫入緩存,返回結果,同時釋放鎖。


分享到:


相關文章: