詳解Mysql中保證緩存與數(shù)據(jù)庫的雙寫一致性
概述
MySQL 和 Redis 都是常見的數(shù)據(jù)存儲方案,MySQL 用于存儲結構化數(shù)據(jù),Redis 用于存儲非結構化數(shù)據(jù)。在一些高并發(fā)場景下,為了提升系統(tǒng)的性能,我們通常會將數(shù)據(jù)存儲在 Redis 緩存中,并通過 Redis 緩存來提高系統(tǒng)的讀取速度。但是,Redis 緩存中的數(shù)據(jù)是不穩(wěn)定的,可能會隨時被刪除或者被更新,因此需要和 MySQL 中的數(shù)據(jù)進行同步,保證數(shù)據(jù)的一致性。
但是使用過緩存的人都應該知道,在實際應用場景中,要想實時刻保證緩存和數(shù)據(jù)庫中的數(shù)據(jù)一樣,很難做到。 基本上都是盡可能讓他們的數(shù)據(jù)在絕大部分時間內(nèi)保持一致,并保證最終是一致的。
同步策略
首先介紹一下雙寫一致性·
:當修改了數(shù)據(jù)庫的數(shù)據(jù)也要同時更新緩存的數(shù)據(jù),緩存和數(shù)據(jù)庫
四種同步策略:
想要保證緩存與數(shù)據(jù)庫的雙寫一致,一共有4種方式,即4種同步策略:
1. 先更新緩存,再更新數(shù)據(jù)庫;
2. 先更新數(shù)據(jù)庫,再更新緩存;
3. 先刪除緩存,再更新數(shù)據(jù)庫;
4. 先更新數(shù)據(jù)庫,再刪除緩存。
從這4種同步策略中,我們需要作出比較的是:
- 更新緩存與刪除緩存哪種方式更合適?
- 應該先操作數(shù)據(jù)庫還是先操作緩存?
更新緩存還是刪除緩存:
下面,我們來分析一下,應該采用更新緩存還是刪除緩存的方式。
1.更新緩存
- 優(yōu)點:每次數(shù)據(jù)變化都及時更新緩存,所以查詢時不容易出現(xiàn)未命中的情況。
- 缺點:更新緩存的消耗比較大。如果數(shù)據(jù)需要經(jīng)過復雜的計算再寫入緩存,那么頻繁的更新緩存,就會影響服務器的性能。如果是寫入數(shù)據(jù)頻繁的業(yè)務場景,那么可能頻繁的更新緩存時,卻沒有業(yè)務讀取該數(shù)據(jù)。
2.刪除緩存
- 優(yōu)點:操作簡單,無論更新操作是否復雜,都是將緩存中的數(shù)據(jù)直接刪除。
- 缺點:刪除緩存后,下一次查詢緩存會出現(xiàn)未命中,這時需要重新讀取一次數(shù)據(jù)庫。
從上面的比較來看,一般情況下,刪除緩存是更優(yōu)的方案。
先操作數(shù)據(jù)庫還是緩存:
下面,我們再來分析一下,應該先操作數(shù)據(jù)庫還是先操作緩存。
案例一、先刪除緩存,在更新數(shù)據(jù)庫
初始時,緩存和數(shù)據(jù)庫均為10。
如上圖,先刪除緩存,再更新數(shù)據(jù)庫,可能會出現(xiàn)的問題:
- 線程1刪除緩存
- 線程2查詢緩存未命中,查詢數(shù)據(jù)庫
- 寫入緩存的值為10,
- 線程1再進行更新數(shù)據(jù)庫,值為20
此時數(shù)據(jù)庫為更新過的值20,而緩存還是舊值10,此時出現(xiàn)了數(shù)據(jù)庫和緩存數(shù)據(jù)不一致情況。
案例二 先操作數(shù)據(jù)庫,再刪除緩存
如上圖,先刪除緩存,再更新數(shù)據(jù)庫,可能會出現(xiàn)的問題:
- 線程1查詢緩存未命中,查詢數(shù)據(jù)庫
- 線程2更新數(shù)據(jù)庫為20,
- 線程2刪除緩存
- 線程1寫入緩存值為10
此時數(shù)據(jù)庫為更新過的值20,而緩存還是舊值10,此時出現(xiàn)了數(shù)據(jù)庫和緩存數(shù)據(jù)不一致情況。
經(jīng)過案例一和案例二的比較,先刪除緩存和先更新數(shù)據(jù)庫都會出現(xiàn)問題。
延時雙刪策略(不推薦)
在寫庫前后都進行redis.del(key)操作,并且設定合理的超時時間。
偽代碼如下:
public void write( String key, Object data ){ redis.delKey(key); db.updateData(data); Thread.sleep(500); redis.delKey(key); }
問題:這個500毫秒怎么確定的,具體該休眠多久時間呢?
需要評估自己的項目的讀數(shù)據(jù)業(yè)務邏輯的耗時。這么做的目的,就是確保讀請求結束,寫請求可以刪除讀請求造成的緩存臟數(shù)據(jù)。當然這種策略還要考慮redis和數(shù)據(jù)庫主從同步的耗時。另外這種策略也會可能會有臟數(shù)據(jù)的風險,而且還會消耗不必要的性能。
在實際場景中,并不推薦延時雙刪策略,一方面可能會有臟數(shù)據(jù)的風險,而且還會消耗不必要的性能。
雖然先更新數(shù)據(jù)庫,再刪除緩存也是會出現(xiàn)數(shù)據(jù)不一致性的問題,但是在實際中,這個問題出現(xiàn)的概率并不高。因為緩存的寫入通常要遠遠快于數(shù)據(jù)庫的寫入,所以在實際中很難出現(xiàn)請求 B 已經(jīng)更新了數(shù)據(jù)庫并且刪除了緩存,請求 A 才更新完緩存的情況。所以,「先更新數(shù)據(jù)庫 + 再刪除緩存」的方案,是可以保證數(shù)據(jù)一致性的。
但是,為了確保萬無一失,在更新完緩存時,給緩存加上較短的過期時間,這樣即時出現(xiàn)緩存不一致的情況,緩存的數(shù)據(jù)也會很快過期,對業(yè)務還是能接受的。另外在更新緩存中加入過期時間,這樣就算出現(xiàn)了緩存和數(shù)據(jù)庫不一致問題,但最終是一致的。
使用分布式鎖實現(xiàn)雙寫一致性
分別在寫數(shù)據(jù)和讀數(shù)據(jù)加分布式鎖,保證同一時間只運行一個請求更新緩存(保證讀寫串行化),就會不會產(chǎn)生并發(fā)問題了,這樣就能保證redis和mysql的數(shù)據(jù)強一致性。
但是這樣的話讀操作和寫操作都需要加鎖,效率就會大大降低。其實在真實場景中放入緩存中的數(shù)據(jù)一般是讀多寫少,如果是讀少寫多,那完全可以不用緩存,直接操作數(shù)據(jù)庫了。
使用讀寫鎖實現(xiàn)雙寫一致性
在讀多寫少的場景下,可以使用讀鎖和寫鎖的機制。
- 共享鎖:讀鎖readLock,加鎖之后,其他線程可以共享讀操作,寫互斥
- 排他鎖:獨占鎖writeLock也加寫鎖,加鎖之后,堵塞其他線程讀寫操作。
使用redisson中的讀寫鎖實現(xiàn)雙寫一致性
想要拿到共享鎖或者排他鎖,都需要先拿到讀寫鎖。通過固定代碼可以拿到讀寫鎖。
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("READ_WRITE_LOCK");
隨后分別拿到共享鎖和排他鎖。(注意兩個鎖需要是同一把讀寫鎖)
RLock readLock = readWriteLock.readLock(); RLock writeLock = readWriteLock.writeLock();
讀操作加入讀鎖(共享鎖)
public void getById(Integer id){ RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("READ_WRITE_LOCK"); RLock readLock = readWriteLock.readLock(); try{ readLock.lock(); System.out.println("readLock..."); Item item = (Item) redisTemplate.opsForValue().get("item"+id); if(item != null){ return item; } item = new Item(id, "手機", "手機", 60.00); redisTemplate.opsForValue().set("item"+id, item); return item; }finally{ readLock.unlock(); } }
寫操作加入寫鎖(排他鎖)
public void updateById(Integer id){ RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("READ_WRITE_LOCK"); RLock writeLock = readWriteLock.writeLock(); try{ writeLock.lock(); System.out.println("writeLock..."); Item item = new Item(id, "手機", "手機", 100.00); try{ Thread.sleep(2000); }catch(InterruptedException e){ e.printStackTrace(); } redisTemplate.delete("item"+id); }finally{ writeLock.unlock(); } }
可以實現(xiàn)強一致性方案,雖然比分布式鎖好一點,但是在高并發(fā)場景下性能也比較低。
使用消息隊列異步通知
如果允許緩存中的數(shù)據(jù)在短時間內(nèi)可以跟數(shù)據(jù)庫數(shù)據(jù)不一致的情況下,可以使用異步通知的方案,可以保證最終一致性。
為了解決雙寫一致性的問題,我們可以引入消息隊列,比如RabbitMQ,來異步更新Redis。將操作同一資源的請求,打到同一個隊列中。
當有數(shù)據(jù)變動時,我們先操作數(shù)據(jù)庫,然后通過消息隊列發(fā)送消息到一個緩存更新的隊列中,異步更新緩存。這種方式能夠讓寫操作變得更加高效,并且避免了高并發(fā)下的緩存與數(shù)據(jù)庫數(shù)據(jù)不一致的問題。
訂閱Mysql的Binlog文件(可借助Canal來進行)
另一種更為可靠的方法是使用MySQL的binlog。我們可以使用Maxwell或者Canal等工具,實時解析binlog,然后更新Redis。
這種方案的好處是即使應用程序崩潰,也不會丟失binlog,因此能夠保證最終的數(shù)據(jù)一致性。但是,這種方案的實現(xiàn)比較復雜,需要對MySQL的內(nèi)部機制有深入的理解。
總結
允許延時一致的業(yè)務,采用異步通知
- 使用MQ中間件,更新數(shù)據(jù)之后,通知緩存更新,將操作同一資源的請求,打到同一個隊列中。
- 利用canal中間件,不需要修改業(yè)務代碼,偽裝為mysql的一個從節(jié)點,canal通過讀取binlog數(shù)據(jù)更新緩存
強一致性,采用Redisson提供的讀寫過
在讀多寫少的場景下,可以使用讀鎖和寫鎖的機制。
- 共享鎖:讀鎖readLock,加鎖之后,其他線程可以共享讀操作,寫互斥
- 排他鎖:獨占鎖writeLock也加寫鎖,加鎖之后,堵塞其他線程讀寫操作。
到此這篇關于Mysql中如何保證緩存與數(shù)據(jù)庫的雙寫一致性的文章就介紹到這了,更多相關保證緩存與數(shù)據(jù)庫的雙寫一致性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
MySQL實現(xiàn)查詢某個字段含有字母數(shù)字的值
這篇文章主要介紹了MySQL實現(xiàn)查詢某個字段含有字母數(shù)字的值方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07mysql存儲過程 在動態(tài)SQL內(nèi)獲取返回值的方法詳解
本篇文章是對mysql存儲過程在動態(tài)SQL內(nèi)獲取返回值進行了詳細的分析介紹,需要的朋友參考下2013-06-06mysql 8.0.18各版本安裝及安裝中出現(xiàn)的問題(精華總結)
這篇文章主要介紹了mysql 8.0.18各版本安裝及安裝中出現(xiàn)的問題,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12