數(shù)據(jù)庫跟緩存的雙寫一致性
1 關(guān)于一致性
為加速系統(tǒng)性能一般都會引入緩存機(jī)制,比如 Redis。這種情況下當(dāng)用戶讀數(shù)據(jù)時一般會按照如下流程:

關(guān)于讀的流程大家是沒有異議的,但是對于數(shù)據(jù)的更新呢,如何操作才算合理呢?
先更新數(shù)據(jù)庫再更新緩存。
先刪緩存再更新數(shù)據(jù)庫。
先更新數(shù)據(jù)庫再刪緩存。
2 一致性解決方法
2.1 緩存TTL
簡單直接又暴力的方法,如果有些數(shù)據(jù)不重要,我們讀完一次數(shù)據(jù)到緩存后設(shè)置個TTL即可,等待超時后緩存自動從數(shù)據(jù)庫讀取下數(shù)據(jù)。
2.2 先更新數(shù)據(jù)庫 再更新緩存
假如我們有A、B兩個請求,A請求將age = 14,B請求將age = 12。我們看下正常執(zhí)行跟非正常執(zhí)行情況:

可發(fā)現(xiàn)如果出現(xiàn)網(wǎng)絡(luò)震蕩會導(dǎo)致緩存的數(shù)據(jù)是舊數(shù)據(jù)。因此這種方法不可取。并且如果是如下場景也不合適:
寫場景多而讀場景少的業(yè)務(wù)需求,此時緩存不是經(jīng)常性的讀,卻被頻繁的更新。
如果緩存的數(shù)據(jù)是經(jīng)過各種復(fù)雜計算后寫入的,那每次寫入緩存都要運算一次,此法不可取。
2.3 先刪緩存 再更新數(shù)據(jù)庫
假如A先請求更改數(shù)據(jù),B請求讀數(shù)據(jù),如果因為網(wǎng)絡(luò)導(dǎo)致發(fā)生如下情況也會造成緩存臟數(shù)據(jù),如果此時緩存沒有設(shè)置TTL那會一直是臟數(shù)據(jù)。

上面這種情況如何解決呢?一般可以采用
延時雙刪策略,他的核心執(zhí)行流程如下:public void write(String key,Object value){
redis.delKey(key);
db.updateValue(value);
Thread.sleep(1000); // 再次刪除
redis.delKey(key);
}
該思路落實到流程圖上如下所示:

sleep的時間要根據(jù)業(yè)務(wù)數(shù)據(jù)邏輯耗時而定,反正目的是
確保讀請求結(jié)束,寫請求可以刪除讀請求造成的緩存臟數(shù)據(jù)。當(dāng)然如果用的是主從寫讀架構(gòu),那處理思路跟上面類似,無非就是休眠時間再加上主從同步的時間即可。

可是其實第二次刪除還是有不妥的地方:
二次刪除前面涉及到休眠,可能導(dǎo)致系統(tǒng)性能降低,可以采用異步的方式,再起一個線程來進(jìn)行異步刪除。
如果二次刪除失敗了,還是會導(dǎo)致緩存臟數(shù)據(jù)存在的啊!
2.4 先更新數(shù)據(jù)庫 再刪緩存
針對緩存更新問題,老外提出了一個名為《Cache-Aside pattern》的緩存更新套路,該策略在Facebook中也廣泛使用,該策略指出:
失效:應(yīng)用程序先從緩存取數(shù)據(jù),沒有得到,則從數(shù)據(jù)庫中取數(shù)據(jù),成功后,放到緩存中。命中:應(yīng)用程序從緩存中取數(shù)據(jù),取到后返回。更新:先把數(shù)據(jù)存到數(shù)據(jù)庫中,成功后,再讓緩存失效。
假如此時A、B兩個線程同時請求,正常來講不管你是讀寫分離還是單機(jī)版,讀一般比寫快。那刪除緩存一般是有效的。

但是也有可能別的原因?qū)е伦x比寫還慢,導(dǎo)致我們刪了個寂寞,雖然這種情況很少發(fā)生。

該方案相比先刪除緩存再更新數(shù)據(jù)庫還是穩(wěn)妥些的,但是也不是萬無一失的。不管是先刪緩存再更新數(shù)據(jù)庫還是先更新數(shù)據(jù)庫再刪緩存,
如果刪除緩存失敗了都會導(dǎo)致緩存跟數(shù)據(jù)不一致問題!2.5 消息隊列 確保消息刪除
通過消息隊列的確認(rèn)消費機(jī)制來刪除緩存。

缺點也很明顯:
對業(yè)務(wù)線代碼造成大量的侵入,引入了中間件。
消息的延遲刪除也會造成短暫的不一致。
2.6 專門程序+消息隊列 確保消息刪除
該方案啟動一個訂閱程序去訂閱數(shù)據(jù)庫的binlog,獲得需要操作的數(shù)據(jù)。在應(yīng)用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進(jìn)行刪除緩存操作。

訂閱binlog程序在mysql中有現(xiàn)成的中間件叫canal,可以完成訂閱binlog日志的功能。
3 總結(jié)
分析后你會發(fā)現(xiàn)數(shù)據(jù)更新時緩存是刪除不是更新,而刪除緩存一般有三種方法:
如果緩存數(shù)據(jù)不敏感,直接給緩存設(shè)置TTL即可。
先刪緩存再更新數(shù)據(jù)庫,此時需配合延時雙刪技術(shù),但可能導(dǎo)致二次刪除失敗。
先更新數(shù)據(jù)庫再刪緩存,此時需配合binlog消費 + 消息隊列來實現(xiàn)。
4 參考
Java后端:http://rjzheng.cnblogs.com
艾小仙分布式鎖:https://t.1yb.co/jaaA
推薦閱讀:
Nginx的這個默認(rèn)配置,差點讓我的職場生涯折戟沉沙
原創(chuàng)好文!億級流量網(wǎng)關(guān)設(shè)計思路
歡迎關(guān)注微信公眾號:互聯(lián)網(wǎng)全棧架構(gòu),收取更多有價值的信息。
