數據庫和緩存一致性的幾種實現方式,我們來聊聊?


緩存是互聯網高併發系統裏常用的組件,由於多增加了一層,如果沒有正確的使用效果可能適得其反,諸如“緩存是刪除還是更新?”,“先操作數據庫還是先操作緩存?”都是些老生常談的話題,今天我們就來聊一聊緩存與數據庫的雙寫一致性的解決方案。

Cache Aside Pattern

在一開始先科普下最經典的緩存+數據庫讀寫的模式,就是 Cache Aside Pattern。

  • 讀的時候,先讀緩存,緩存沒有的話,就讀數據庫,然後取出數據後放入緩存,同時返回響應。
  • 更新的時候,先更新數據庫,然後再刪除緩存。

推薦觀看:https://www.bilibili.com/video/BV13z411b7mU

爲什麼是刪除緩存,而不是更新緩存?

更新緩存在併發下會帶來種種問題,直接刪除緩存比較簡單粗暴,穩妥。而且還有懶加載的思想,等用到的時候在去數據庫讀出來放進去,不用到你每次去更新他幹嘛,浪費時間資源,而且還有更新失敗、產生髒數據的一些風險,達成這一點共識以後,我們來開始今天的討論。

先更新數據庫,再刪除緩存

1、更新數據庫成功,刪除緩存成功,沒毛病。

2、更新數據庫失敗,程序捕獲異常,不會走到下一步,不會出現數據不一致情況。

3、更新數據庫成功,刪除緩存失敗。數據庫是新數據,緩存是舊數據,發生了不一致的情況。這裏我們來看下怎麼解決

  • 重試的機制,如果刪除緩存失敗,我們捕獲這個異常,把需要刪除的key發送到消息隊列, 然後自己創建一個消費者消費,嘗試再次刪除這個 key。
  • 異步更新緩存,更新數據庫時會往 binlog 寫入日誌,所以我們可以通過一個服務來監聽 binlog 的變化(比如阿里的 canal),然後在客戶端完成刪除 key 的操作。如果刪除失敗的話,再發送到消息隊列。

總之,我們要達到最終一致性!

先刪除緩存,再更新數據庫

1、刪除緩存成功,更新數據庫成功,沒毛病。

2、刪除緩存失敗,程序捕獲異常,不會走到下一步,不會出現數據不一致情況。

3、刪除緩存成功,更新數據庫失敗,此時數據庫中是舊數據,緩存中是空的,那麼數據不會不一致。因爲讀的時候緩存沒有,則讀數據庫中舊數據,然後更新到緩存中。

雖然沒有發生數據不一致的情況,看上去好像一切都很完美,但是以上是在單線程的情況下,如果在併發的情況下可能會出現以下場景

1)線程 A 需要更新數據,首先刪除了 Redis 緩存  
2)線程 B 查詢數據,發現緩存不存在,到數據庫查詢舊值,寫入 Redis,返回  
3)線程 A 更新了數據庫

 

這個時候,Redis是舊的值,數據庫是新的值,還是發生了數據不一致的情況。

延時雙刪

針對上面這種情況,我們有一種延時雙刪的方法

1)刪除緩存
2)更新數據庫
3)休眠 500ms(這個時間,依據讀取數據的耗時而定)
4)再次刪除緩存

 

你把舊值存在Redis以後,過一段時間我在刪除一次,這時把舊值給刪掉了,這樣就能保證Redis和數據庫是同步的了,這麼做在一定程度上可以緩解這個問題,但也不是十分完美,比如第一次緩存刪除成功了,第二次緩存刪除失敗,又該怎麼辦?

內存隊列

除了延時雙刪這個方法,還有個方案就是內存隊列,他的思想是 串行化 ,我們在JVM中維護一個內存隊列。 當更新數據的時候,我們不直接操作數據庫和緩存,而是把數據的Id放到內存隊列;當讀數據的時候發現數據不在緩存中,我們不去數據庫查放到緩存中,而是把數據的Id放到內存隊列。

後臺會有一個線程消費內存隊列裏面的數據,然後一條一條的執行。這樣的話,一個更新數據的操作,先刪除緩存,然後再去更新數據庫,但是還沒完成更新。此時如果一個讀請求過來,讀到了空的緩存,那麼先將緩存更新的請求發送到隊列中,此時會在隊列中積壓,然後同步等待緩存更新完成。

這裏有一個優化點,一個隊列中,其實多個更新緩存請求串在一起是沒意義的,因此可以做過濾,如果發現隊列中已經有一個更新緩存的請求了,那麼就不用再放個更新請求操作進去了,直接等待前面的更新操作請求完成即可。

等內存隊列中將更新數據的操作完成之後,纔會去執行下一個操作,也就是讀數據的操作,此時會從數據庫中讀取最新的值,然後寫入緩存中。 如果請求還在等待時間範圍內,不斷輪詢發現可以取到值了,那麼就直接返回;如果請求等待的時間超過一定時長,那麼這一次直接從數據庫中讀取。

總結

上面說的幾種方案,都是比較常見的,也比較簡單,沒有十全十美的,最後的內存隊列也會影響性能以及增加系統的複雜度。今天討論的Redis和數據庫的數據更新是不可能通過事務達到統一的,什麼叫做事務,就是一損俱損一榮俱榮,要麼都成功要麼都失敗,這是不能保證的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章