新聞中心
Redis 擁有高性能的數(shù)據(jù)讀寫(xiě)功能,被我們廣泛用在緩存場(chǎng)景,一是能提高業(yè)務(wù)系統(tǒng)的性能,二是為數(shù)據(jù)庫(kù)抵擋了高并發(fā)的流量請(qǐng)求。

運(yùn)城網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián),運(yùn)城網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為運(yùn)城超過(guò)千家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\成都外貿(mào)網(wǎng)站建設(shè)要多少錢(qián),請(qǐng)找那個(gè)售后服務(wù)好的運(yùn)城做網(wǎng)站的公司定做!
把 Redis 作為緩存組件,需要防止出現(xiàn)以下的一些問(wèn)題,否則可能會(huì)造成生產(chǎn)事故。
- Redis 緩存滿了怎么辦?
- 緩存穿透、緩存擊穿、緩存雪崩如何解決?
- Redis 數(shù)據(jù)過(guò)期了會(huì)被立馬刪除么?
- Redis 突然變慢了如何做性能排查并解決?
- Redis 與 MySQL 數(shù)據(jù)一致性問(wèn)題怎么應(yīng)對(duì)?
今天跟大家一起深入探索緩存的工作機(jī)制和緩存一致性應(yīng)對(duì)方案。
在本文正式開(kāi)始之前,我們需要先取得以下兩點(diǎn)的共識(shí):
1)緩存必須要有過(guò)期時(shí)間;
2)保證數(shù)據(jù)庫(kù)跟緩存的最終一致性即可,不必追求強(qiáng)一致性。
目錄如下:
1. 什么是數(shù)據(jù)庫(kù)與緩存一致性
2. 緩存的使用策略
2.1 Cache-Aside (旁路緩存)
2.2 Read-Through(直讀)
2.3 Write-Through (同步直寫(xiě))
2.4 Write-Behind
3. 旁路緩存下的一致性問(wèn)題分析
3.1 先更新緩存,再更新數(shù)據(jù)庫(kù)
3.2 先更新數(shù)據(jù)庫(kù),再更新緩存
3.3 先刪緩存,再更新數(shù)據(jù)庫(kù)
3.4 先更新數(shù)據(jù)庫(kù),再刪緩存
4. 一致性解決方案有哪些
4.1 緩存延時(shí)雙刪
4.2 刪除緩存重試機(jī)制
4.3 讀取 binlog 異步刪除
總結(jié)
一、什么是數(shù)據(jù)庫(kù)與緩存一致性
數(shù)據(jù)一致性指的是:
- 緩存中存有數(shù)據(jù),緩存的數(shù)據(jù)值 = 數(shù)據(jù)庫(kù)中的值;
- 緩存中沒(méi)有該數(shù)據(jù),數(shù)據(jù)庫(kù)中的值 = 最新值。
反推緩存與數(shù)據(jù)庫(kù)不一致:
- 緩存的數(shù)據(jù)值 ≠ 數(shù)據(jù)庫(kù)中的值;
- 緩存或者數(shù)據(jù)庫(kù)存在舊的數(shù)據(jù),導(dǎo)致線程讀取到舊數(shù)據(jù)。
為何會(huì)出現(xiàn)數(shù)據(jù)一致性問(wèn)題呢?
把 Redis 作為緩存的時(shí)候,當(dāng)數(shù)據(jù)發(fā)生改變我們需要雙寫(xiě)來(lái)保證緩存與數(shù)據(jù)庫(kù)的數(shù)據(jù)一致。
數(shù)據(jù)庫(kù)跟緩存,畢竟是兩套系統(tǒng),如果要保證強(qiáng)一致性,勢(shì)必要引入 2PC 或 Paxos 等分布式一致性協(xié)議,或者分布式鎖等等,這個(gè)在實(shí)現(xiàn)上是有難度的,而且一定會(huì)對(duì)性能有影響。
如果真的對(duì)數(shù)據(jù)的一致性要求這么高,那引入緩存是否真的有必要呢?
二、 緩存的使用策略
在使用緩存時(shí),通常有以下幾種緩存使用策略用于提升系統(tǒng)性能:
- Cache-Aside Pattern(旁路緩存,業(yè)務(wù)系統(tǒng)常用)
- Read-Through Pattern
- Write-Through Pattern
- Write-Behind Pattern
1、Cache-Aside (旁路緩存)
所謂「旁路緩存」,就是讀取緩存、讀取數(shù)據(jù)庫(kù)和更新緩存的操作都在應(yīng)用系統(tǒng)來(lái)完成,業(yè)務(wù)系統(tǒng)最常用的緩存策略。
1)讀取數(shù)據(jù)
讀取數(shù)據(jù)邏輯如下:
- 當(dāng)應(yīng)用程序需要從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)時(shí),先檢查緩存數(shù)據(jù)是否命中。
- 如果緩存未命中,則查詢數(shù)據(jù)庫(kù)獲取數(shù)據(jù),同時(shí)將數(shù)據(jù)寫(xiě)到緩存中,以便后續(xù)讀取相同數(shù)據(jù)會(huì)命中緩存,最后再把數(shù)據(jù)返回給調(diào)用者。
- 如果緩存命中,直接返回。
時(shí)序圖如下:
旁路緩存讀時(shí)序圖
優(yōu)點(diǎn)
- 緩存中僅包含應(yīng)用程序?qū)嶋H請(qǐng)求的數(shù)據(jù),有助于保持緩存大小的成本效益。
- 實(shí)現(xiàn)簡(jiǎn)單,并且能獲得性能提升。
實(shí)現(xiàn)的偽代碼如下:
String cacheKey = "公眾號(hào):碼哥字節(jié)";
String cacheValue = redisCache.get(cacheKey);
//緩存命中
if (cacheValue != null) {
return cacheValue;
} else {
//緩存缺失, 從數(shù)據(jù)庫(kù)獲取數(shù)據(jù)
cacheValue = getDataFromDB();
// 將數(shù)據(jù)寫(xiě)到緩存中
redisCache.put(cacheValue)
}
缺點(diǎn)
由于數(shù)據(jù)僅在緩存未命中后才加載到緩存中,因此初次調(diào)用的數(shù)據(jù)請(qǐng)求響應(yīng)時(shí)間會(huì)增加一些開(kāi)銷(xiāo),因?yàn)樾枰~外的緩存填充和數(shù)據(jù)庫(kù)查詢耗時(shí)。
2) 更新數(shù)據(jù)
使用 cache-aside 模式寫(xiě)數(shù)據(jù)時(shí),如下流程。
旁路緩存寫(xiě)數(shù)據(jù)
- 寫(xiě)數(shù)據(jù)到數(shù)據(jù)庫(kù)
- 將緩存中的數(shù)據(jù)失效或者更新緩存數(shù)據(jù)
使用 cache-aside 時(shí),最常見(jiàn)的寫(xiě)入策略是直接將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù),但是緩存可能會(huì)與數(shù)據(jù)庫(kù)不一致。
我們應(yīng)該給緩存設(shè)置一個(gè)過(guò)期時(shí)間,這個(gè)是保證最終一致性的解決方案。
如果過(guò)期時(shí)間太短,應(yīng)用程序會(huì)不斷地從數(shù)據(jù)庫(kù)中查詢數(shù)據(jù)。同樣,如果過(guò)期時(shí)間過(guò)長(zhǎng),并且更新時(shí)沒(méi)有使緩存失效,緩存的數(shù)據(jù)很可能是臟數(shù)據(jù)。
最常用的方式是刪除緩存使緩存數(shù)據(jù)失效。
為啥不是更新緩存呢?
性能問(wèn)題
當(dāng)緩存的更新成本很高,需要訪問(wèn)多張表聯(lián)合計(jì)算,建議直接刪除緩存,而不是更新緩存數(shù)據(jù)來(lái)保證一致性。
安全問(wèn)題
在高并發(fā)場(chǎng)景下,可能會(huì)造成查詢查到的數(shù)據(jù)是舊值,具體待會(huì)會(huì)分析。
2、Read-Through(直讀)
當(dāng)緩存未命中,也是從數(shù)據(jù)庫(kù)加載數(shù)據(jù),同時(shí)寫(xiě)到緩存中并返回給應(yīng)用系統(tǒng)。
雖然 read-through 和 cache-aside 非常相似,在 cache-aside 中應(yīng)用系統(tǒng)負(fù)責(zé)從數(shù)據(jù)庫(kù)獲取數(shù)據(jù)和填充緩存。
而 Read-Through 將獲取數(shù)據(jù)存儲(chǔ)中的值的責(zé)任轉(zhuǎn)移到了緩存提供者身上。
Read-Through
Read-Through 實(shí)現(xiàn)了關(guān)注點(diǎn)分離原則。代碼只與緩存交互,由緩存組件來(lái)管理自身與數(shù)據(jù)庫(kù)之間的數(shù)據(jù)同步。
3、Write-Through(同步直寫(xiě))
與 Read-Through 類(lèi)似,發(fā)生寫(xiě)請(qǐng)求時(shí),Write-Through 將寫(xiě)入責(zé)任轉(zhuǎn)移到緩存系統(tǒng),由緩存抽象層來(lái)完成緩存數(shù)據(jù)和數(shù)據(jù)庫(kù)數(shù)據(jù)的更新,時(shí)序流程圖如下:
Write-Through
Write-Through 的主要好處是應(yīng)用系統(tǒng)的不需要考慮故障處理和重試邏輯,交給緩存抽象層來(lái)管理實(shí)現(xiàn)。
單獨(dú)直接使用該策略是沒(méi)啥意義的,因?yàn)樵摬呗砸葘?xiě)緩存,再寫(xiě)數(shù)據(jù)庫(kù),對(duì)寫(xiě)入操作帶來(lái)了額外延遲。
當(dāng)Write-Through 與 Read-Through 配合使用,就能成分發(fā)揮 Read-Through 的優(yōu)勢(shì),同時(shí)還能保證數(shù)據(jù)一致性,不需要考慮如何將緩存設(shè)置失效。
Write-Through
這個(gè)策略顛倒了 Cache-Aside 填充緩存的順序,并不是在緩存未命中后延遲加載到緩存,而是在數(shù)據(jù)先寫(xiě)緩存,接著由緩存組件將數(shù)據(jù)寫(xiě)到數(shù)據(jù)庫(kù)。
優(yōu)點(diǎn)
- 緩存與數(shù)據(jù)庫(kù)數(shù)據(jù)總是最新的;
- 查詢性能最佳,因?yàn)橐樵兊臄?shù)據(jù)有可能已經(jīng)被寫(xiě)到緩存中了。
缺點(diǎn)
不經(jīng)常請(qǐng)求的數(shù)據(jù)也會(huì)寫(xiě)入緩存,從而導(dǎo)致緩存更大、成本更高。
4、Write-Behind
這個(gè)圖一眼看去似乎與 Write-Through 一樣,其實(shí)不是的,區(qū)別在于最后一個(gè)箭頭的箭頭:它從實(shí)心變?yōu)榫€。
這意味著緩存系統(tǒng)將異步更新數(shù)據(jù)庫(kù)數(shù)據(jù),應(yīng)用系統(tǒng)只與緩存系統(tǒng)交互。
應(yīng)用程序不必等待數(shù)據(jù)庫(kù)更新完成,從而提高應(yīng)用程序性能,因?yàn)閷?duì)數(shù)據(jù)庫(kù)的更新是最慢的操作。
Write-Behind
這種策略下,緩存與數(shù)據(jù)庫(kù)的一致性不強(qiáng),對(duì)一致性高的系統(tǒng)不建議使用。
三、旁路緩存下的一致性問(wèn)題分析
業(yè)務(wù)場(chǎng)景用的最多的就是 Cache-Aside (旁路緩存) 策略,在該策略下,客戶端對(duì)數(shù)據(jù)的讀取流程是先讀取緩存,如果命中則返回;未命中,則從數(shù)據(jù)庫(kù)讀取并把數(shù)據(jù)寫(xiě)到緩存中,所以讀操作不會(huì)導(dǎo)致緩存與數(shù)據(jù)庫(kù)的不一致。
重點(diǎn)是寫(xiě)操作,數(shù)據(jù)庫(kù)和緩存都需要修改,而兩者就會(huì)存在一個(gè)先后順序,可能會(huì)導(dǎo)致數(shù)據(jù)不再一致。針對(duì)寫(xiě),我們需要考慮兩個(gè)問(wèn)題:
1)先更新緩存還是更新數(shù)據(jù)庫(kù)?
2)當(dāng)數(shù)據(jù)發(fā)生變化時(shí),選擇修改緩存(update),還是刪除緩存(delete)?
將這兩個(gè)問(wèn)題排列組合,會(huì)出現(xiàn)四種方案:
- 先更新緩存,再更新數(shù)據(jù)庫(kù);
- 先更新數(shù)據(jù)庫(kù),再更新緩存;
- 先刪除緩存,再更新數(shù)據(jù)庫(kù);
- 先更新數(shù)據(jù)庫(kù),再刪除緩存。
接下來(lái)的分析大家不必死記硬背,關(guān)鍵在于在推演的過(guò)程中大家只需要考慮以下兩個(gè)場(chǎng)景會(huì)不會(huì)帶來(lái)嚴(yán)重問(wèn)題即可:
- 其中第一個(gè)操作成功,第二個(gè)失敗會(huì)導(dǎo)致什么問(wèn)題?
- 在高并發(fā)情況下會(huì)不會(huì)造成讀取數(shù)據(jù)不一致?
如果第一個(gè)失敗了,第二個(gè)就不用執(zhí)行了,直接在第一步返回 50x 等異常信息即可,不會(huì)出現(xiàn)不一致問(wèn)題。
只有第一個(gè)成功,第二個(gè)失敗才讓人頭痛,想要保證它們的原子性,就涉及到分布式事務(wù)的范疇了。
1、先更新緩存,再更新數(shù)據(jù)庫(kù)
先更新緩存再更新數(shù)據(jù)庫(kù)
如果先更新緩存成功,寫(xiě)數(shù)據(jù)庫(kù)失敗,就會(huì)導(dǎo)致緩存是最新數(shù)據(jù),數(shù)據(jù)庫(kù)是舊數(shù)據(jù),那緩存就是臟數(shù)據(jù)了。
之后,其他查詢立馬請(qǐng)求進(jìn)來(lái)的時(shí)候就會(huì)獲取這個(gè)數(shù)據(jù),而這個(gè)數(shù)據(jù)數(shù)據(jù)庫(kù)中卻不存在。
數(shù)據(jù)庫(kù)都不存在的數(shù)據(jù),緩存并返回客戶端就毫無(wú)意義了。
該方案直接 Pass。
2、先更新數(shù)據(jù)庫(kù),再更新緩存
一切正常的情況如下:
- 先寫(xiě)數(shù)據(jù)庫(kù),成功;
- 再 update 緩存,成功。
1)更新緩存失敗
這時(shí)候我們來(lái)推斷下,假如這兩個(gè)操作的原子性被破壞:第一步成功,第二步失敗會(huì)導(dǎo)致什么問(wèn)題?
會(huì)導(dǎo)致數(shù)據(jù)庫(kù)是最新數(shù)據(jù),緩存是舊數(shù)據(jù),出現(xiàn)一致性問(wèn)題。
該圖我就不畫(huà)了,與上一個(gè)圖類(lèi)似,對(duì)調(diào)下 Redis 和 MySQL 的位置即可。
2)高并發(fā)場(chǎng)景
謝霸歌經(jīng)常 996,腰酸脖子疼,bug 越寫(xiě)越多,想去按摩推拿放提升下編程技巧。
疫情影響,單子來(lái)之不易,高端會(huì)所的技師都爭(zhēng)先恐后想接這一單,高并發(fā)啊兄弟們。
在進(jìn)店以后,前臺(tái)會(huì)將顧客信息錄入系統(tǒng),執(zhí)行 set xx的服務(wù)技師 = 待定的初始值表示目前無(wú)人接待保存到數(shù)據(jù)庫(kù)和緩存中,之后再安排技師按摩服務(wù)。
如下圖所示:
高并發(fā)先更新數(shù)據(jù)庫(kù),再更新緩存
- 98 號(hào)技師先下手為強(qiáng),向系統(tǒng)發(fā)送 set 謝霸歌的服務(wù)技師 = 98 的指令寫(xiě)入數(shù)據(jù)庫(kù),這時(shí)候系統(tǒng)的網(wǎng)絡(luò)出現(xiàn)波動(dòng),卡頓了,數(shù)據(jù)還沒(méi)來(lái)得及寫(xiě)到緩存。
- 接下來(lái),520 號(hào)技師也向系統(tǒng)發(fā)送 set 謝霸歌的服務(wù)技師 = 520寫(xiě)到數(shù)據(jù)庫(kù)中,并且也把這個(gè)數(shù)據(jù)寫(xiě)到緩存中了。
- 這時(shí)候之前的 98 號(hào)技師的寫(xiě)緩存請(qǐng)求開(kāi)始執(zhí)行,順利將數(shù)據(jù) set 謝霸歌的服務(wù)技師 = 98 寫(xiě)到緩存中。
最后發(fā)現(xiàn),數(shù)據(jù)庫(kù)的值 = set 謝霸歌的服務(wù)技師 = 520,而緩存的值= set 謝霸歌的服務(wù)技師 = 98。
520 號(hào)技師在緩存中的最新數(shù)據(jù)被 98 號(hào)技師的舊數(shù)據(jù)覆蓋了。
所以,在高并發(fā)的場(chǎng)景中,多線程同時(shí)寫(xiě)數(shù)據(jù)再寫(xiě)緩存,就會(huì)出現(xiàn)緩存是舊值,數(shù)據(jù)庫(kù)是最新值的不一致情況。
該方案直接 pass。
如果第一步就失敗,直接返回 50x 異常,并不會(huì)出現(xiàn)數(shù)據(jù)不一致。
3、先刪緩存,再更新數(shù)據(jù)庫(kù)
按照前面說(shuō)的套路,假設(shè)第一個(gè)操作成功,第二個(gè)操作失敗推斷下會(huì)發(fā)生什么?高并發(fā)場(chǎng)景下又會(huì)發(fā)生什么?
1)第二步寫(xiě)數(shù)據(jù)庫(kù)失敗
假設(shè)現(xiàn)在有兩個(gè)請(qǐng)求:寫(xiě)請(qǐng)求 A,讀請(qǐng)求 B。
寫(xiě)請(qǐng)求 A 第一步先刪除緩存成功,寫(xiě)數(shù)據(jù)到數(shù)據(jù)庫(kù)失敗,就會(huì)導(dǎo)致該次寫(xiě)數(shù)據(jù)丟失,數(shù)據(jù)庫(kù)保存的是舊值。
接著另一個(gè)讀請(qǐng) B 求進(jìn)來(lái),發(fā)現(xiàn)緩存不存在,從數(shù)據(jù)庫(kù)讀取舊數(shù)據(jù)并寫(xiě)到緩存中。
2)高并發(fā)下的問(wèn)題
先刪緩存,再寫(xiě)數(shù)據(jù)庫(kù)
- 還是 98 號(hào)技師先下手為強(qiáng),系統(tǒng)接收請(qǐng)求把緩存數(shù)據(jù)刪除,當(dāng)系統(tǒng)準(zhǔn)備將 set 肖菜雞的服務(wù)技師 = 98寫(xiě)到數(shù)據(jù)庫(kù)的時(shí)候發(fā)生卡頓,來(lái)不及寫(xiě)入。
- 這時(shí)候,大堂經(jīng)理向系統(tǒng)執(zhí)行讀請(qǐng)求,查下肖菜雞有沒(méi)有技師接待,方便安排技師服務(wù),系統(tǒng)發(fā)現(xiàn)緩存中沒(méi)數(shù)據(jù),于是乎就從數(shù)據(jù)庫(kù)讀取到舊數(shù)據(jù) set 肖菜雞的服務(wù)技師 = 待定,并寫(xiě)到緩存中。
- 這時(shí)候,原先卡頓的 98 號(hào)技師寫(xiě)數(shù)據(jù) set 肖菜雞的服務(wù)技師 = 98到數(shù)據(jù)庫(kù)的操作完成。
這樣子會(huì)出現(xiàn)緩存的是舊數(shù)據(jù),在緩存過(guò)期之前無(wú)法讀取到最數(shù)據(jù)。肖菜雞本就被 98 號(hào)技師接單了,但是大堂經(jīng)理卻以為沒(méi)人接待。
該方案 pass,因?yàn)榈谝徊匠晒?,第二步失敗,?huì)造成數(shù)據(jù)庫(kù)是舊數(shù)據(jù),緩存中沒(méi)數(shù)據(jù)繼續(xù)從數(shù)據(jù)庫(kù)讀取舊值寫(xiě)入緩存,造成數(shù)據(jù)不一致,還會(huì)多一次 cahche。
不論是異常情況還是高并發(fā)場(chǎng)景,會(huì)導(dǎo)致數(shù)據(jù)不一致。miss。
4、先更新數(shù)據(jù)庫(kù),再刪緩存
經(jīng)過(guò)前面的三個(gè)方案,全都被 pass 了,分析下最后的方案到底行不行。
按照套路,分別判斷異常和高并發(fā)會(huì)造成什么問(wèn)題。
該策略可以知道,在寫(xiě)數(shù)據(jù)庫(kù)階段失敗的話就直返返回客戶端異常,不需要執(zhí)行緩存操作了。
所以第一步失敗不會(huì)出現(xiàn)數(shù)據(jù)不一致的情況。
1)刪緩存失敗
重點(diǎn)在于第一步寫(xiě)最新數(shù)據(jù)到數(shù)據(jù)庫(kù)成功,刪除緩存失敗怎么辦?
可以把這兩個(gè)操作放在一個(gè)事務(wù)中,當(dāng)緩存刪除失敗,那就把寫(xiě)數(shù)據(jù)庫(kù)回滾。
高并發(fā)場(chǎng)景下不合適,容易出現(xiàn)大事務(wù),造成死鎖問(wèn)題。
如果不回滾,那就出現(xiàn)數(shù)據(jù)庫(kù)是新數(shù)據(jù),緩存還是舊數(shù)據(jù),數(shù)據(jù)不一致了,咋辦?
所以,我們要想辦法讓緩存刪除成功,不然只能等到有效期失效那可不行。
使用重試機(jī)制。
比如重試三次,三次都失敗則記錄日志到數(shù)據(jù)庫(kù),使用分布式調(diào)度組件 xxl-job 等實(shí)現(xiàn)后續(xù)的處理。
在高并發(fā)的場(chǎng)景下,重試最好使用異步方式,比如發(fā)送消息到 mq 中間件,實(shí)現(xiàn)異步解耦。
亦或是利用 Canal 框架訂閱 MySQL binlog 日志,監(jiān)聽(tīng)對(duì)應(yīng)的更新請(qǐng)求,執(zhí)行刪除對(duì)應(yīng)緩存操作。
2)高并發(fā)場(chǎng)景
再來(lái)分析下高并發(fā)讀寫(xiě)會(huì)有什么問(wèn)題……
先寫(xiě)數(shù)據(jù)庫(kù)后刪緩存
- 98 號(hào)技師先下手為強(qiáng),接下肖菜雞的這筆生意,數(shù)據(jù)庫(kù)執(zhí)行 set 肖菜雞的服務(wù)技師 = 98;還是網(wǎng)絡(luò)卡頓了下,沒(méi)來(lái)得及執(zhí)行刪除緩存操作。
- 主管 Candy 向系統(tǒng)執(zhí)行讀請(qǐng)求,查下肖菜雞有沒(méi)有技師接待,發(fā)現(xiàn)緩存中有數(shù)據(jù) 肖菜雞的服務(wù)技師 = 待定,直接返回信息給客戶端,主管以為沒(méi)人接待。
- 原先 98 號(hào)技師接單,由于卡頓沒(méi)刪除緩存的操作現(xiàn)在執(zhí)行刪除成功。
讀請(qǐng)求可能出現(xiàn)少量讀取舊數(shù)據(jù)的情況,但是很快舊數(shù)據(jù)就會(huì)被刪除,之后的請(qǐng)求都能獲取最新數(shù)據(jù),問(wèn)題不大。
還有一種比較極端的情況,緩存自動(dòng)失效的時(shí)候又遇到了高并發(fā)讀寫(xiě)的情況,假設(shè)這會(huì)有兩個(gè)請(qǐng)求,一個(gè)線程 A 做查詢操作,一個(gè)線程 B 做更新操作,那么會(huì)有如下情形產(chǎn)生:
緩存忽然失效
- 緩存的過(guò)期時(shí)間到期,緩存失效。
- 線程 A 讀請(qǐng)求讀取緩存,沒(méi)命中,則查詢數(shù)據(jù)庫(kù)得到一個(gè)舊的值(因?yàn)?B 會(huì)寫(xiě)新值,相對(duì)而言就是舊的值了),準(zhǔn)備把數(shù)據(jù)寫(xiě)到緩存時(shí)發(fā)送網(wǎng)絡(luò)問(wèn)題卡頓了。
- 線程 B 執(zhí)行寫(xiě)操作,將新值寫(xiě)數(shù)據(jù)庫(kù)。
- 線程 B 執(zhí)行刪除緩存。
- 線程 A 繼續(xù),從卡頓中醒來(lái),把查詢到的舊值寫(xiě)到入緩存。
出了不一致的情況的概率微乎其微,發(fā)生上述情況的必要條件是:
- 步驟 (3)的寫(xiě)數(shù)據(jù)庫(kù)操作要比步驟(2)讀操作耗時(shí)短速度快,才可能使得步驟(4)先于步驟(5)。
- 緩存剛好到達(dá)過(guò)期時(shí)限。
通常 MySQL 單機(jī)的 QPS 大概 5K 左右,而 TPS 大概 1k 左右,(ps:Tomcat 的 QPS 4K 左右,TPS = 1k 左右)。
數(shù)據(jù)庫(kù)讀操作是遠(yuǎn)快于寫(xiě)操作的(正是因?yàn)槿绱耍抛鲎x寫(xiě)分離),所以步驟(3)要比步驟(2)更快這個(gè)情景很難出現(xiàn),同時(shí)還要配合緩存剛好失效。
所以,在用旁路緩存策略的時(shí)候,對(duì)于寫(xiě)操作推薦使用:先更新數(shù)據(jù)庫(kù),再刪除緩存。
四、一致性解決方案有哪些
最后,針對(duì) Cache-Aside (旁路緩存) 策略,寫(xiě)操作使用先更新數(shù)據(jù)庫(kù),再刪除緩存的情況下,我們來(lái)分析下數(shù)據(jù)一致性解決方案都有哪些?
1、緩存延時(shí)雙刪
如果采用先刪除緩存,再更新數(shù)據(jù)庫(kù)如何避免出現(xiàn)臟數(shù)據(jù)?
采用延時(shí)雙刪策略。
- 先刪除緩存。
- 寫(xiě)數(shù)據(jù)庫(kù)。
- 休眠 500 毫秒,再刪除緩存。
這樣子最多只會(huì)出現(xiàn) 500 毫秒的臟數(shù)據(jù)讀取時(shí)間。關(guān)鍵是這個(gè)休眠時(shí)間怎么確定呢?
延遲時(shí)間的目的就是確保讀請(qǐng)求結(jié)束,寫(xiě)請(qǐng)求可以刪除讀請(qǐng)求造成的緩存臟數(shù)據(jù)。
所以我們需要自行評(píng)估項(xiàng)目的讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí),在讀耗時(shí)的基礎(chǔ)上加幾百毫秒作為延遲時(shí)間即可。
2、刪除緩存重試機(jī)制
緩存刪除失敗怎么辦?比如延遲雙刪的第二次刪除失敗,那豈不是無(wú)法刪除臟數(shù)據(jù)。
使用重試機(jī)制,保證刪除緩存成功。
比如重試三次,三次都失敗則記錄日志到數(shù)據(jù)庫(kù)并發(fā)送警告讓人工介入。
在高并發(fā)的場(chǎng)景下,重試最好使用異步方式,比如發(fā)送消息到 mq 中間件,實(shí)現(xiàn)異步解耦。
重試機(jī)制
第(5)步如果刪除失敗且未達(dá)到重試最大次數(shù)則將消息重新入隊(duì),直到刪除成功,否則就記錄到數(shù)據(jù)庫(kù),人工介入。
該方案有個(gè)缺點(diǎn),就是對(duì)業(yè)務(wù)代碼中造成侵入,于是就有了下一個(gè)方案,啟動(dòng)一個(gè)專(zhuān)門(mén)訂閱 數(shù)據(jù)庫(kù) binlog 的服務(wù)讀取需要?jiǎng)h除的數(shù)據(jù)進(jìn)行緩存刪除操作。
3、讀取 binlog 異步刪除
binlog異步刪除
- 更新數(shù)據(jù)庫(kù);
- 數(shù)據(jù)庫(kù)會(huì)把操作信息記錄在 binlog 日志中;
- 使用 canal 訂閱 binlog 日志獲取目標(biāo)數(shù)據(jù)和 key;
- 緩存刪除系統(tǒng)獲取 canal 的數(shù)據(jù),解析目標(biāo) key,嘗試刪除緩存。
- 如果刪除失敗則將消息發(fā)送到消息隊(duì)列;
- 緩存刪除系統(tǒng)重新從消息隊(duì)列獲取數(shù)據(jù),再次執(zhí)行刪除操作。
總結(jié)
緩存策略的最佳實(shí)踐是 Cache Aside Pattern。分別分為讀緩存最佳實(shí)踐和寫(xiě)緩存最佳實(shí)踐。
讀緩存最佳實(shí)踐:先讀緩存,命中則返回;未命中則查詢數(shù)據(jù)庫(kù),再寫(xiě)到緩存中。
寫(xiě)緩存最佳實(shí)踐:
- 先寫(xiě)數(shù)據(jù)庫(kù),再操作緩存;
- 直接刪除緩存,而不是修改,因?yàn)楫?dāng)緩存的更新成本很高,需要訪問(wèn)多張表聯(lián)合計(jì)算,建議直接刪除緩存,而不是更新,另外,刪除緩存操作簡(jiǎn)單,副作用只是增加了一次 chache miss,建議大家使用該策略。
在以上最佳實(shí)踐下,為了盡可能保證緩存與數(shù)據(jù)庫(kù)的一致性,我們可以采用延遲雙刪。
防止刪除失敗,我們采用異步重試機(jī)制保證能正確刪除,異步機(jī)制我們可以發(fā)送刪除消息到 mq 消息中間件,或者利用 canal 訂閱 MySQL binlog 日志監(jiān)聽(tīng)寫(xiě)請(qǐng)求刪除對(duì)應(yīng)緩存。
那么,如果我非要保證絕對(duì)一致性怎么辦,先給出結(jié)論:
沒(méi)有辦法做到絕對(duì)的一致性,這是由 CAP 理論決定的,緩存系統(tǒng)適用的場(chǎng)景就是非強(qiáng)一致性的場(chǎng)景,所以它屬于 CAP 中的 AP。
所以,我們得委曲求全,可以去做到 BASE 理論中說(shuō)的最終一致性。
其實(shí)一旦在方案中使用了緩存,那往往也就意味著我們放棄了數(shù)據(jù)的強(qiáng)一致性,但這也意味著我們的系統(tǒng)在性能上能夠得到一些提升。
所謂 tradeoff 正是如此。
分享文章:別再問(wèn)了,數(shù)據(jù)庫(kù)與緩存一致性問(wèn)題今天全整齊活了!
路徑分享:http://www.dlmjj.cn/article/ccsidpg.html


咨詢
建站咨詢
