在現(xiàn)在的系統(tǒng)架構(gòu)中,緩存的地位可以說(shuō)是非常高的。因?yàn)樵诨ヂ?lián)網(wǎng)的時(shí)代,請(qǐng)求的并發(fā)量可能會(huì)非常高,但是關(guān)系型數(shù)據(jù)庫(kù)對(duì)于高并發(fā)的處理能力并不是非常強(qiáng),而緩存由于是在內(nèi)存中處理,并不需要磁盤(pán)的IO,所以非常適合于高并發(fā)的處理,也就成為了各個(gè)系統(tǒng)中必不可少的一部分了。
不過(guò),由此產(chǎn)生的問(wèn)題也是非常多的,其中一個(gè)就是如何保證數(shù)據(jù)庫(kù)和緩存之間的數(shù)據(jù)一致性。
由于數(shù)據(jù)庫(kù)的操作和緩存的操作不可能在一個(gè)事務(wù)中,也就勢(shì)必會(huì)出現(xiàn)數(shù)據(jù)庫(kù)寫(xiě)入失敗,緩存不能更新,緩存寫(xiě)入失敗的補(bǔ)償機(jī)制。具體我們應(yīng)該怎么做呢?
我們先看一個(gè)最常見(jiàn)的讀緩存的例子在讀取緩存的方式中,上圖這種方式可以說(shuō)是最為廣泛使用的了。讀本身是沒(méi)有什么問(wèn)題的,但是,寫(xiě)入緩存的方式,就是保證數(shù)據(jù)一致性的重中之重了。
這里我們不考慮定時(shí)刷新緩存的方式,也就是下面這類(lèi)方式:
寫(xiě)入數(shù)據(jù)庫(kù)和寫(xiě)入緩存是獨(dú)立的,寫(xiě)入數(shù)據(jù)庫(kù)操作后,需要等待定時(shí)服務(wù)執(zhí)行,執(zhí)行完成后緩存數(shù)據(jù)才會(huì)刷新。
這種方式會(huì)導(dǎo)致數(shù)據(jù)的不一致時(shí)間較長(zhǎng),數(shù)據(jù)刷新時(shí),不管有沒(méi)有改變的數(shù)據(jù),都會(huì)重新加載,效率差。當(dāng)然,并不是說(shuō)這種方式就沒(méi)用,還是有一些場(chǎng)景是可以使用的,例如一些系統(tǒng)配置的緩存,而且,這樣做緩存刷新,代碼量非常少,也便于維護(hù)。
我們今天只考慮雙寫(xiě)的數(shù)據(jù)一致性如何來(lái)考慮。由于不同的寫(xiě)入方式,可能帶來(lái)的結(jié)果也就是不同的。通常情況下,我們都有哪些寫(xiě)入數(shù)據(jù)并刷新緩存的方式呢?
方法一、先更新數(shù)據(jù)庫(kù),在更新緩存這套方案是最簡(jiǎn)單的一種緩存雙寫(xiě)方案,我們先來(lái)看看流程圖
使用這種雙寫(xiě)的方案,只要在數(shù)據(jù)成功寫(xiě)入數(shù)據(jù)庫(kù)后,刷新緩存就可以了,代碼簡(jiǎn)單,維護(hù)也很簡(jiǎn)單。但是,簡(jiǎn)單的前提下,帶來(lái)的問(wèn)題也是很直接的。
首先,線程數(shù)據(jù)安全無(wú)法保證
例如:我們現(xiàn)在同時(shí)有兩個(gè)請(qǐng)求會(huì)操作同一條數(shù)據(jù),一個(gè)是請(qǐng)求A,一個(gè)是請(qǐng)求B。請(qǐng)求A需要先執(zhí)行,請(qǐng)求B后執(zhí)行,那么數(shù)據(jù)庫(kù)的記錄就是請(qǐng)求B執(zhí)行后的記錄。
但是,由于一些網(wǎng)絡(luò)原因或者其他情況,最終執(zhí)行的順序可能就變成了:
請(qǐng)求A Update 數(shù)據(jù)庫(kù) -> 請(qǐng)求B Update 數(shù)據(jù)庫(kù) -> 請(qǐng)求B Update 緩存 -> 請(qǐng)求A Update 緩存。這樣的結(jié)果會(huì)導(dǎo)致:
1. 數(shù)據(jù)庫(kù)和緩存中的數(shù)據(jù)不一致,從而緩存中的數(shù)據(jù)就成為了臟數(shù)據(jù)。
2. 寫(xiě)入操作多于讀操作,就會(huì)頻繁的刷新緩存,但是這些數(shù)據(jù)根本沒(méi)有被讀過(guò)。這樣就會(huì)浪費(fèi)服務(wù)器的資源。
因此,這種雙寫(xiě)方式很難保證數(shù)據(jù)一致性,不建議使用。
方法二、先刪除緩存再更新數(shù)據(jù)庫(kù)由于上述方式存在的問(wèn)題,那么我們就考慮,能不能先刪除緩存,在更新數(shù)據(jù)庫(kù),這樣,在更新數(shù)據(jù)庫(kù)的前后,由于緩存中沒(méi)有數(shù)據(jù)了,請(qǐng)求就會(huì)穿透到數(shù)據(jù)庫(kù)直接讀取數(shù)據(jù)然后放入緩存,這樣,緩存就不會(huì)被頻繁的刷新了。
于是,我們就設(shè)置了一個(gè)新的執(zhí)行順序:
不過(guò),這樣一來(lái),新問(wèn)題又出現(xiàn)了。有兩個(gè)請(qǐng)求,一個(gè)請(qǐng)求A,一個(gè)請(qǐng)求B,請(qǐng)求A去寫(xiě)數(shù)據(jù),請(qǐng)求B去讀數(shù)據(jù)。當(dāng)并發(fā)量高的時(shí)候,就會(huì)出現(xiàn)以下情況:
請(qǐng)求A進(jìn)行寫(xiě)操作,刪除緩存 -> 請(qǐng)求B查詢發(fā)現(xiàn)緩存不存在 -> 請(qǐng)求B去數(shù)據(jù)庫(kù)查詢得到舊值 -> 請(qǐng)求B將舊值寫(xiě)入緩存 -> 請(qǐng)求A將新值寫(xiě)入數(shù)據(jù)庫(kù)這是,臟數(shù)據(jù)又出現(xiàn)了。如果我們沒(méi)有設(shè)置緩存的過(guò)期時(shí)間,那么在下一次下入數(shù)據(jù)前,臟數(shù)據(jù)就會(huì)一直的存在。針對(duì)這種臟數(shù)據(jù)出現(xiàn)的情況,我們決定在寫(xiě)入數(shù)據(jù)后,增加一點(diǎn)延時(shí),再刪除一次數(shù)據(jù),于是就有了方法三。
方法三、延時(shí)雙刪使用延時(shí)雙刪的策略,就能夠很好的解決之前我們應(yīng)該并發(fā)所引起的數(shù)據(jù)不一致的情況。那是不是延時(shí)雙刪就完全沒(méi)有問(wèn)題呢?不。
我們來(lái)假設(shè)一個(gè)場(chǎng)景,就是我們做了讀寫(xiě)分離,那么使用延時(shí)雙刪可能問(wèn)出現(xiàn)什么情況呢?
請(qǐng)求A進(jìn)行寫(xiě)操作,刪除緩存 -> 請(qǐng)求A將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)了 -> 請(qǐng)求B查詢緩存發(fā)現(xiàn),緩存沒(méi)有值 -> 請(qǐng)求B去從庫(kù)查詢,這時(shí),還沒(méi)有完成主從同步,因此查詢到的是舊值 -> 請(qǐng)求B將舊值寫(xiě)入緩存 -> 數(shù)據(jù)庫(kù)完成主從同步,從庫(kù)變?yōu)樾轮怠?/p>
糟糕,又出現(xiàn)數(shù)據(jù)不一致了。
然后在看看性能如何,由于需要延時(shí),如果是同步執(zhí)行,性能必定很差,所以第二次刪除只有做成異步,避免影響性能。那異步執(zhí)行刪除就會(huì)出現(xiàn)新問(wèn)題,如果異步線程執(zhí)行失敗了,那么舊數(shù)據(jù)就不會(huì)被刪除,數(shù)據(jù)不一致又出現(xiàn)了。
不行,我們需要向一個(gè)一勞永逸的辦法,單純的雙刪還是不可靠。
方法四、隊(duì)列刪除緩存我們?cè)诎褦?shù)據(jù)更新到數(shù)據(jù)庫(kù)后,把刪除緩存的消息加入到隊(duì)列中,如果隊(duì)列執(zhí)行失敗,就再次加入到隊(duì)列執(zhí)行直到成功為止。
這樣,我們就能夠有效的保證數(shù)據(jù)庫(kù)和緩存的數(shù)據(jù)一致性了,不管是讀寫(xiě)分離還是其他情況,只要隊(duì)列消息能夠保證安全,那么緩存就一定會(huì)被刷新。
當(dāng)然,根據(jù)這個(gè)方案,我們還可以進(jìn)一步優(yōu)化。因?yàn)檫@里我們的緩存刷新時(shí)基于業(yè)務(wù)代碼的,也就是說(shuō),業(yè)務(wù)代碼和緩存刷新的耦合度很高。有沒(méi)有辦法能夠把緩存刷新獨(dú)立出來(lái),不基于業(yè)務(wù)代碼執(zhí)行呢?
方法五、binlog訂閱刪除緩存為了保證業(yè)務(wù)代碼的獨(dú)立性,我們可以通過(guò)訂閱binlog日志的方式來(lái)刷新緩存。我們先啟動(dòng)mysql的binlog日志,然后如下圖方式設(shè)計(jì)流程:
通過(guò)binlog的訂閱,我們就把業(yè)務(wù)代碼和緩存刷新的非業(yè)務(wù)代碼獨(dú)立開(kāi)來(lái)。代碼量小了,也方便維護(hù)了。程序員們也不需要去關(guān)心什么時(shí)候應(yīng)該刷新緩存,是不是需要刷新緩存。
當(dāng)然,實(shí)戰(zhàn)中,我們還有很多不同的業(yè)務(wù)場(chǎng)景,可能需要的數(shù)據(jù)一致性同步方案也不同,這里也只算是一個(gè)案例。