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