2023-07-03:講一講Redis緩存的數據一致性問(wèn)題和處理方案。
答案2023-07-03:
數據一致性當使用緩存時(shí),無(wú)論是在本地內存中緩存還是使用 Redis 等外部緩存系統,會(huì )引入數據同步的問(wèn)題。下面以 Tomcat 向 MySQL 中進(jìn)行數據的插入、更新和刪除操作為例,來(lái)說(shuō)明具體的過(guò)程。
【資料圖】
分析下面幾種解決方案的數據同步方案:
1.先更新緩存,再更新數據庫:先更新緩存可以提高讀取性能,但如果更新緩存成功而更新數據庫失敗,可能導致數據不一致。
2.先更新數據庫,再更新緩存:確保數據的持久性,但如果更新數據庫成功而更新緩存失敗,也可能導致數據不一致。
3.先刪除緩存,后更新數據庫:通過(guò)先刪除緩存,再更新數據庫的方式,可以在數據更新后保證數據的一致性,但會(huì )降低讀取操作的性能。
4.先更新數據庫,后刪除緩存:確保數據的持久性,并在更新數據庫成功后再刪除緩存,以保持數據的一致性。
新增數據類(lèi)對于新增數據的情況,數據會(huì )直接寫(xiě)入數據庫,無(wú)需對緩存進(jìn)行操作。在這種情況下,緩存中本身就沒(méi)有新增數據,而數據庫中保存的是最新值。因此,緩存和數據庫的數據是一致的。
更新緩存類(lèi)1、先更新緩存,再更新DB我們通常不考慮這個(gè)方案,因為存在以下問(wèn)題:即使在更新緩存成功后,若出現更新數據庫時(shí)的異常,會(huì )導致緩存中的數據與數據庫數據完全不一致。由于緩存數據一直存在,這種不一致性很難察覺(jué)到。
2、先更新DB,再更新緩存我們一般不考慮先更新數據庫再更新緩存的方案,與第一個(gè)方案存在相同的問(wèn)題。數據庫更新成功但緩存更新失敗時(shí),仍然會(huì )導致數據不一致的問(wèn)題。此外,這種方案還存在以下問(wèn)題:
并發(fā)問(wèn)題:當有請求A和請求B同時(shí)進(jìn)行更新操作時(shí),可能出現以下情況:線(xiàn)程A先更新數據庫,然后線(xiàn)程B也更新了數據庫,隨后線(xiàn)程B更新了緩存,最后線(xiàn)程A也更新了緩存。這導致了請求A應該先更新緩存,但由于網(wǎng)絡(luò )等原因,請求B卻比請求A更早更新了緩存,從而產(chǎn)生臟數據。
業(yè)務(wù)場(chǎng)景問(wèn)題:如果寫(xiě)數據庫的操作比讀數據的操作更頻繁,采用這種方案會(huì )導致數據還沒(méi)有被讀取,就頻繁更新緩存,從而浪費性能。
除了更新緩存,我們還可以考慮刪除緩存的方案。具體選擇更新緩存還是淘汰緩存取決于“更新緩存的復雜度”。如果更新緩存的成本較低,我們更傾向于更新緩存以提高緩存命中率;而如果更新緩存的代價(jià)較高,我們則更傾向于淘汰緩存。
刪除緩存類(lèi)3、先刪除緩存,后更新DB該方案也會(huì )出問(wèn)題,具體出現的原因如下。
1、此時(shí)來(lái)了兩個(gè)請求,請求 A(更新操作) 和請求 B(查詢(xún)操作)
2、請求 A 會(huì )先刪除 Redis 中的數據,然后去數據庫進(jìn)行更新操作;
3、此時(shí)請求 B 看到 Redis 中的數據時(shí)空的,會(huì )去數據庫中查詢(xún)該值,補錄到 Redis 中;
4、但是此時(shí)請求 A 并沒(méi)有更新成功,或者事務(wù)還未提交,請求B去數據庫查詢(xún)得到舊值;
5、那么這時(shí)候就會(huì )產(chǎn)生數據庫和 Redis 數據不一致的問(wèn)題。
如何解決呢?其實(shí)最簡(jiǎn)單的解決辦法就是延時(shí)雙刪的策略。就是
(1)先淘汰緩存
(2)再寫(xiě)數據庫
(3)休眠1秒,再次淘汰緩存
這段偽代碼就是“延遲雙刪”
redis.delKey(X)db.update(X)Thread.sleep(N)redis.delKey(X)
這么做,可以將1秒內所造成的緩存臟數據,再次刪除。
那么,這個(gè)1秒怎么確定的,具體該休眠多久呢?
針對上面的情形,讀該自行評估自己的項目的讀數據業(yè)務(wù)邏輯的耗時(shí)。然后寫(xiě)數據的休眠時(shí)間則在讀數據業(yè)務(wù)邏輯的耗時(shí)基礎上,加幾百ms即可。這么做的目的,就是確保讀請求結束,寫(xiě)請求可以刪除讀請求造成的緩存臟數據。
但是上述的保證事務(wù)提交完以后再進(jìn)行刪除緩存還有一個(gè)問(wèn)題,就是如果你使用的是** Mysql ****的讀寫(xiě)分離的架構**的話(huà),那么其實(shí)主從同步之間也會(huì )有時(shí)間差。
此時(shí)來(lái)了兩個(gè)請求,請求 A(更新操作) 和請求 B(查詢(xún)操作)
請求 A 更新操作,刪除了Redis,
請求主庫進(jìn)行更新操作,主庫與從庫進(jìn)行同步數據的操作,
請 B 查詢(xún)操作,發(fā)現 Redis中沒(méi)有數據,
去從庫中拿去數據,此時(shí)同步數據還未完成,拿到的數據是舊數據。
此時(shí)的解決辦法有兩個(gè):
1、還是使用雙刪延時(shí)策略。只是,睡眠時(shí)間修改為在主從同步的延時(shí)時(shí)間基礎上,加幾百ms。
2、就是如果是對 Redis進(jìn)行填充數據的查詢(xún)數據庫操作,那么就強制將其指向主庫進(jìn)行查詢(xún)。
繼續深入,采用這種同步淘汰策略,吞吐量降低怎么辦?
那就將第二次刪除作為異步的。自己起一個(gè)線(xiàn)程,異步刪除。這樣,寫(xiě)的請求就不用沉睡一段時(shí)間后了,再返回。這么做,加大吞吐量。
繼續深入,第二次刪除,如果刪除失敗怎么辦?
所以,我們引出了,下面的第四種策略,先更新數據庫,再刪緩存。
4、先更新DB,后刪除緩存Cache Aside模式是一種常用的緩存處理方式。在讀取數據時(shí),先檢查緩存,如果緩存中存在數據,則直接返回;如果緩存中不存在,則從數據庫中讀取,并將數據寫(xiě)入緩存,最后返回響應。在更新數據時(shí),先更新數據庫,然后再刪除緩存。
通常情況下,我們更傾向于使用刪除緩存的操作,因為刪除緩存的速度比在數據庫中更新數據的速度更快,能更有效地避免數據不一致的問(wèn)題。通過(guò)延時(shí)雙刪的處理方式,可以進(jìn)一步減少緩存不一致性的可能性。
標簽: