天天看點

分布式系統(一)資料庫和緩存一緻性深刻了解資料庫和緩存一緻性

深刻了解資料庫和緩存一緻性

1. 簡介

在軟體的發展過程中,我們的資料存儲在不同的媒體中,比如資料庫,Redis緩存中;每個存儲媒體有其特性,利用好這些特性可以最大程度的發揮程式的性能;而在這些存儲媒體中,如何保證各個媒體中資料的一緻性是軟體發展中一個非常重要的點;

2. 發展

在早期的系統裡面,很多的接口都是直接查詢資料庫;當請求數量比較小,查詢的資料量比較少時,資料庫還是可以抗的住這樣的查詢;

分布式系統(一)資料庫和緩存一緻性深刻了解資料庫和緩存一緻性

随着業務量的增多和查詢次數的增多,資料庫的響應時間會是制約軟體響應速度的瓶頸,此時緩存的使用就開始了;我的了解是緩存是一種思想,加快了請求的響應;不僅僅在開發過程中使用到,像許多的硬體,比如CPU 的一二三級緩存,電腦機械硬碟的緩存,都是為了加快響應;

分布式系統(一)資料庫和緩存一緻性深刻了解資料庫和緩存一緻性

緩存中存儲着常用的資料,要通路資料時,先去緩存中查詢有沒有資料,沒有的話再去資料庫中查詢資料;當遇到修改資料或者新增資料的時候,我們會去操作資料庫;然後再把資料同步到緩存中去;

這裡就引出了本文要說的問題,最後的資料庫資料同步到緩存中去,這個中間是有時間的間隔的;那在這一段時間内的請求如果是請求到緩存的資料,而這個資料已經在資料庫修改還沒有來得及更新到緩存,這就涉及到這個請求讀取到了髒資料;

3. 問題分析

軟體一直處于發展階段,不會因為上面說到髒資料的問題而去不使用緩存;業界針對上面的問題和使用場景,使用不同的緩存思路;

其實我們在這裡可以考慮,在下面寫緩存和寫DB 本來就是兩個操作,而且是并行,肯定在過程中會出現髒資料的情況,我們做的方案就是在減低這種機率,或者保持資料的最終一緻性;來降低資料一緻性帶來的影響;上面說到了并行;還有一種方案就是不并行,對資料的資料加鎖,分布式鎖,對一個資源,或者一個方法加上一個分布式鎖,把非原子性的這種操作變成一個串行的動作,這個就是對資料一緻性有較大的保證;但是帶來的結果就是效率相對較低;

除此之外,除了并行帶來的問題,就是資料存儲失敗的問題,因為緩存和DB 是兩個中間件,有可能發生一個成功,一個失敗的問題,那麼針對失敗,我們也需要做一些措施來盡可能避免失敗帶來的資料不一緻的行為;

綜上總結存在的問題:

1.請求并發帶來的資料不一緻

2.存儲DB和緩存其中一個失敗帶來的資料不一緻;

4.請求并發方案

本節針對并發請求不同方案處理的優劣勢分析;

4.1 緩存更新

  • 寫請求去資料庫
  • 讀請求先緩存 沒有的話 查詢資料庫,并寫入緩存,設定失效時間
分布式系統(一)資料庫和緩存一緻性深刻了解資料庫和緩存一緻性

場景 : 線程A,B 并發執行

都是讀,沒什麼好說,不會有資料不一緻問題;

A寫B讀 ,最差的情況是一個請求讀取到緩存髒資料或者DB 髒資料

A,B 都是寫,問題就較大了,下面就是該設計當請求都是寫請求的問題

1.先DB 後緩存

修改請求的同時,在去更新緩存;

比如是先更新DB:

  • Thread A 把 DB 中 X -> 1
  • Thread B 把 DB 中 X -> 2
  • Thread B 把 緩存 中 X -> 2
  • Thread A 把 緩存 中 X -> 1

正常情況 緩存最後是 X = 2 但是确實 X= 1 ,緩存永久(如果沒有過期時間)髒資料

2.先緩存 後DB

在如果先修改緩存:

  • Thread A 把 緩存 中 X -> 1
  • Thread B 把 緩存 中 X -> 2
  • Thread B 把 DB 中 X -> 2
  • Thread A 把 DB 中 X -> 1

正常情況 DB 中X=2 但是卻 DB 中 X=1 ,DB永久髒資料

4.2 删除緩存

寫資料的請求會删除緩存;讀取的時候和上面一樣判斷緩存中資料是否存在;

同樣這樣的方案也是有兩種;

  • 先删除緩存,後寫DB
  • 先寫DB , 後删除緩存
分布式系統(一)資料庫和緩存一緻性深刻了解資料庫和緩存一緻性

場景:線程A,B 并發執行

都是讀,沒什麼好說,不會有資料不一緻問題;

A寫B讀 ,最差的情況是一個請求讀取到緩存髒資料或者DB 髒資料

A,B 都是寫,都是修改DB,且删除緩存,不會有資料問題

綜合:分析到這裡可以看到對于 緩存更新的優勢了,都是寫請求不會出問題,且一個寫一個讀的情況出現問題的機率也相對較低,看下面分析

1.先删除緩存,後寫DB

原本資料 X = 1

  • Thread A 開始執行 X=2
  • Thread A 删除緩存
  • Thread B 擷取X ,此時資料已經被删除,然後擷取到DB 尚未更新更新的資料 X=1
  • Thread B X=1 放到緩存中去
  • Thread A 執行DB X=2 ,流程結束

正常情況 緩存X=2 但是卻最後

Thread B X=1 放到緩存中去

緩存永久髒資料

2.先寫DB , 後删除緩存

  • 1.Thread A 擷取X ,緩存中不存在,去到DB 中 擷取X =1
  • 2.Thread B 寫DB X=2 ,
  • 3.Thread B 删除緩存 ,
  • 4.Thread A 在第一步擷取的 X =1 更新到緩存

正常情況 緩存X=2 但是卻最後

Thread A 在第一步擷取的 X =1 更新到緩存

緩存永久髒資料

**不同點就是:**第二種方式 2,3 ;如果 Thread A ,Thread B 在步驟1 ,2 是幾乎同時過來的話,1 + 4 的時間是要比 2 + 3 的時間短的,因為正常情況下,對資料的寫會比讀更加的消耗時間; 是以 2 + 3 正常的時間 是 在 4 後面執行,也就是最後資料正常情況下是一緻的;總結就是出現這種情況的同時滿足條件

  • 緩存 X 不存在
  • 讀寫并發
  • 2 + 3 時間 >1+4 時間 ; 是以 3 一般後執行

那麼這種方式就是相對前面三種中較好(矮子裡面找高個了屬于)的一種方式來操作DB 和緩存;

5.單DB/緩存更新失敗

5.1 擴充緩存删除

在對删除緩存的思路分析中,可以分析到,緩存删除,緩存更新 其實是緩存删除比較好的;

緩存删除删除中先寫DB , 後删除緩存 是比較好的;

======>>>> 那麼下面就針對這個方案 在詳細看看

  • 1.Thread A 擷取X ,緩存中不存在,去到DB 中 擷取X =1 ; 執行成功
  • 2.Thread B 寫DB X=2 ,;執行成功
  • 3.Thread B 删除緩存 ,
  • 4.Thread A 在第一步擷取的 X =1 更新到緩存

A,B 線程第一步就是失敗的話,後續就可以不用執行了,就變成一個線程執行,沒有一緻性的問題,排除第一步就失敗;

======>>>> 那麼現在讨論第二步失敗:

  • 3.Thread B 删除緩存 ,如果緩存删除失敗,資料庫成功 ---->>>資料不一緻,不能以為寫緩存失敗而復原,業務上不合理;
  • 4.Thread A 在第一步擷取的 X =1 更新到緩存;---->>>失敗沒有問題 ,最多緩存沒有資料,資料一緻性還在

且我們讨論,一般情況下 3. 是在 4 後面執行的 (上一節說過);且如果想要資料更趨向于一緻,3 要保證在4 後面才資料一緻更能有保障(不是一定資料一緻);

======>>>> 那麼就保證 Thread B 第二步删除成功執行

正常删除什麼問題沒有,萬一删除就失敗呢

======>>>> 那麼久删除失敗

如何防止删除失敗,或者說是删除失敗後如何補償,實作方案就是重試

重試的問題是

  • 一般情況下是在執行一次還是會異常(大機率)
  • 或者說重試多少次才合理??
  • 重試是阻塞的,影響程式運作效率

如何解決這些問題; 異步重試删除,那麼如何異步,我們在開發中異步操作大機率會想到的就是 消息隊列;不錯 我們把重試的消息直接放到消息隊列;

消息隊列優點:

  • 消息隊列有持久化機制,資料不會丢失
  • 消費成功才删除 =======>>>> 消息隊列保證異步
  • 消費失敗繼續添加到隊列 =======>>>> 這一點 來保證重試

5.2 異步重試方案(一)

上面是推導除了異步重試方案在資料一緻性裡面比較好的方案了,整體架構就是

分布式系統(一)資料庫和緩存一緻性深刻了解資料庫和緩存一緻性

5.3 異步重試方案(二)

上面說的方案,在寫請求的時候,在去把對應的修改寫到消息隊列裡面去;

現在有更加簡單的方案,就是訂閱資料庫的

binlog

(資料庫修改的邏輯日志),這個訂閱的服務現在 有成熟的方案,比如阿裡的 Canal;

次方案架構為:

分布式系統(一)資料庫和緩存一緻性深刻了解資料庫和緩存一緻性

6.主從DB&緩存 資料不一緻

上一章節說的是單機環境的DB 和緩存,那如果DB 是主從結構,一般我們的程式還是讀寫分離的,這個時候該如何設計;

場景: 原來的循序調換一下

  • 2.Thread B 寫DB X=2 ,
  • 3.Thread B 删除緩存 ,
  • 1.Thread A 擷取X ,緩存中不存在,去到DB從庫 中 擷取X 此時資料主從還未同步 擷取X =1 ;
  • 4.Thread A X =1 更新到緩存

問題解析:

本來在并發情況下,讀操作是比較快的,更新的髒資料緩存被寫操作覆寫删除掉;但是由于從庫資料同步相對較慢,就又擷取到髒資料并加載到緩存;

解決方案:緩存延遲雙删

Thread B 寫資料庫後删除緩存,完成之後,在稍等一會,再次删除緩存;(當然中間可能會短暫的讀取到髒資料)

=======>>>> 那麼延遲多久?

  • 延遲時間大于主從複制時間
  • 時間要在 其他線程 讀取從庫+ 寫入緩存的時間

大概這個時間是在 1~ 3秒左右;

7.總結

從上面分析的情況可以看到不同場景下的方案的優劣性;采用這樣的方案要是基于自己業務系統對髒資料的容忍性,來換取程式執行的效率;

方案的好壞,需要實際的使用場景去驗證;,5,6中的方案可能會在短暫的時間内讀取到髒資料;但是會在短暫的時間後會達到資料的最終一緻性;

我們引入緩存,就必然引入資料一緻性的問題,我們所有的方案就是降低一緻性帶來的系統問題;

繼續閱讀