天天看點

Redis 使用Redis作為緩存,你真的考慮周全了嗎?

前言

看到标題,可能小夥伴們會虎軀一震?嗯?難道不應該使用Redis做緩存?

答:

不是你想的那樣,

隻是說,有幾種情況,使用緩存我們需要了解考慮周全,選擇正确的使用姿勢。

正文

好,我們進入該篇正題。

(一定要耐心結合我舉例進行推演才能更加明白)

我們既然選擇了緩存,用redis存儲緩存資料,必然是為了一個字,快。

就是想避免每次都通路資料庫,能直接從緩存很快地拿出資料。

那麼,大家在使用緩存的時候,會不會冥冥之中有對資料的準确性有過懷疑? 對資料的時效性有過質疑?

先列出,我們使用緩存的時候,會選擇到的方式  四種:

 (對于讀操作,不用多言,那肯定是先讀緩存,沒有才從資料庫擷取。)

是以着重針對 寫 操作 分析:

 第一種:

先更新資料庫,再更新緩存

第二種:

先更新緩存,再更新資料庫

第三種:

先删除緩存,再更新資料庫

第四種:

先更新資料庫,再删除緩存

 逐一分析可能存在的問題

先更新資料庫,再更新緩存

存在問題1:

-假設原來的值是500,先需要更新資料庫的值變為1000, 成功了; 資料庫的值為 1000;

-接着就會去更新緩存, 因為未知原因(網絡等),導緻更新失敗; 緩存的值為 500不變。

-那麼讀操作來了,先去緩存裡面讀資料,拿到的是 500, 無疑這是讀到了舊的資料。 

存在問題2:

高并發場景時,

-假設原來的值是500,線程A更新資料庫的值變為1000, 成功了; 資料庫的值為 1000,接着就準備去更新緩存;

-這時候,線程B來了,更新資料庫的值變成200,接着就會去更新緩存;

-因為網絡的波動原因,B線程從後追上A線程, 率先更新了緩存的值,200; 此時緩存資料為200,非常正确。

-但緊接着,A線程緩過神來了,把緩存的值更新為了1000;

-這時候,讀操作來了,先讀緩存,拿出來的值是1000,實際上應該是200,無疑這是讀到了錯誤資料。

 特别是寫操作遠大于讀操作的項目場景, 這個還是很讓人頭疼的。

 ******************************************懶人手動分割線******************************************

 ******************************************懶人手動分割線****************************************** 

 ******************************************懶人手動分割線****************************************** 

先更新緩存,再更新資料庫

存在問題1: 

-假設原來的值是500,先需要更新緩存的值1000,成功了;緩存的值為1000; 

-接着就會去更新資料庫, 因為未知原因(網絡等),導緻更新失敗; 資料庫的值為 500不變。

-那麼讀操作來了,先去緩存裡面讀資料,拿到的是1000,可是資料庫是500 。無疑這是讀到了錯誤資料。

因為資料庫更新不成功,緩存的資料應該也是不可以成功的。

存在問題2: 

高并發場景時,

-假設原來的值是500,線程A更新緩存的值變為1000,成功了;緩存的值為1000,,接着就準備去更新資料庫;

-這時候,線程B來了,更新更新緩存的值變成200,接着就會去更新資料庫;

-因為網絡的波動原因,B線程從後追上A線程, 率先更新了資料庫,200; 此時資料庫資料為200。

 -但緊接着,A線程緩過神來了,把資料庫的值的值更新為了1000;

-此時此刻,緩存資料是200,資料庫資料是1000; 讀操作來了,無疑這是讀到了錯誤資料。

 特别是寫操作遠大于讀操作的項目場景, 這個還是很讓人頭疼的。

 ******************************************懶人手動分割線******************************************

 ******************************************懶人手動分割線****************************************** 

 ******************************************懶人手動分割線****************************************** 

看完上面2種情況,明顯我們都知道,在高并發切寫遠多于讀的時候,這兩種情況都是很不可取的;

是以,就衍生出一個政策,在寫操作的時候,不要去更新緩存,而且選擇直接删除緩存。

更新緩存的操作,放在讀操作進行;

讀操作為:如果緩存有,讀出;無,讀出資料庫的值,更新緩存。

 ps:當然讀多寫少的場景,上面2種方式也都還行。

那就是 先删除緩存,再更新資料庫  還是  先更新資料庫,再删除緩存

先删除緩存,再更新資料庫

存在問題1:

-假設原來緩存的值是500,資料庫也是500;

-先删除緩存, 因為未知原因(網絡等),導緻更新資料庫時失敗;

-這時候,讀操作來了,無影響,因為緩存沒有,直接讀資料庫,正常;

-這時候,寫操作又來,無影響,緩存已經沒有,繼續更新資料庫,正常;

這麼一看,好像還蠻不錯!

然而并不然!因為删緩存的政策代替更新緩存,上面講到了是把緩存寫入操作給了讀操作進行。

繼續看高并發的場景。

存在問題2:

高并發場景時,

-假設原來緩存的值是500,資料庫也是500;

-線程A删掉了緩存,正準備去更新資料庫,把值變成1000;

-因為網絡的波動原因線程A懵了,呆滞了;

-這時候,線程B來了,線程B進行的讀操作,查詢,一看緩存沒資料了(被線程A删掉了),接着就去讀資料庫,值為500;

-線程B讀完資料庫的值500後,緊接着把資料寫入緩存! 此時緩存的資料是500;

-此時線程A緩過神來,更新資料庫值為1000。

-此後,緩存的資料是500,資料庫資料是1000, 無疑,又是存在錯誤資料!

特别是讀操作遠大于寫操作的項目場景, 這個好像又開始很讓人頭疼了。

 ******************************************懶人手動分割線******************************************

 ******************************************懶人手動分割線****************************************** 

 ******************************************懶人手動分割線****************************************** 

先更新資料庫,再删除緩存

存在問題1:

 -假設原來緩存的值是500,資料庫也是500;更新資料庫值變成1000,成功了;

-删除緩存失敗,還是500;

-這時候,讀操作來了,讀出緩存的資料500,無疑這是讀到了錯誤資料;

存在問題2:

高并發場景時,

 -假設原來緩存的值是500,資料庫也是500;

-線程A來了,更新資料庫的值為1000,删除了緩存;

-這時候,線程B也來了,線程B是一個讀操作,發現緩存沒有,讀取了資料庫的值1000,然後準備寫入緩存;

-因為網絡的波動原因線程B懵了,呆滞了;

-這時候線程C來了,線程C更新資料庫值為2000,然後很利索删除緩存(這時候其實本來就沒緩存);

-然後線程B緩過神來了,很利索把值1000寫入了緩存。

-這時候,其他讀操作來了,讀出換成的資料1000,而資料庫實際是2000,無疑這是讀到了錯誤資料;

 ******************************************懶人手動分割線******************************************

 ******************************************懶人手動分割線****************************************** 

 ******************************************懶人手動分割線****************************************** 

??? 

 嗯?什麼?

一輪下來把四種姿勢都看完了,發現都不好???

 那咱們總得選一種用緩存啊?

如果項目場景是讀多寫少    :

而且是并發不考慮的場景,其實更新緩存的方式也是能用一用,不過還是建議用下面的删除緩存的方式。

如果項目場景是寫多讀少    :

其實這時候,使用删除緩存的政策顯然好很多。

先删除緩存,再更新資料庫   分析:

那麼選擇 如果 先删除緩存,再更新資料庫 ,上面提到了,就是怕高并發的場景,導緻資料庫的資料是正常的,而緩存的資料是不對的。

既然是因為這個緩存資料是髒的,那麼針對這個問題,于是乎有了 延時雙删除政策:

先删除 緩存 ,再更新資料庫, 延時後再删除緩存 

從字面上其實已經能知道,就是補一刀把後續的髒緩存資料删掉,這麼具體的延時時間是多少,就得根據具體項目業務時間去衡量了。 

先更新資料庫,再删除緩存   分析:

那麼選擇 如果 先更新資料庫 ,再删除緩存 ,其實這個方式是 一個國外比較推薦的使用緩存方式 : Cache-Aside pattern

 上面提到了,這種方式在高并發的場景也是存在問題的。

 但是為什麼外國人這麼推薦這種方式呢?

回顧這種方式,在高并發的情形,出現問題的原因是 讀操作的後面寫入資料到緩存的環節上。

但是讀操作實際上肯定比寫操作快得多,是以發生上邊描述的出現髒資料的場景的機率也是比較小。

每次讀操作的時候最後會将資料庫資料寫到緩存, 而寫操作最後會删除調緩存。

我們想象下,讀非常快,寫稍微慢的場景, 這樣就算有讀操作寫入的舊的緩存資料,也會被慢寫操作的删掉。

這樣碰巧發生出現不一緻資料的機率就會小很多,這也是外國人推薦的原因。

最後來一個個人的總結:

并發稍微低,那麼我們可以用   先删除緩存,再更新資料庫  再配上延時雙删除政策。

并發稍微高,那麼我們可以用 Cache-Aside pattern ,  先更新資料庫 ,再删除緩存 。

其實看完這篇的,大家都知道,髒資料 在不适用 分布式鎖 或者 其他能保證資料順序的方法的情況下,都是存在的。

隻不過是出現這種髒資料的機率以及嚴重性,是否是項目的業務需求可以接收。

對于該篇文章分析存在的問題,都是有額外的補救方法,如加鎖,重試,消息隊列等等

好,該篇介紹就到此吧。

繼續閱讀