數(shù)據(jù)庫與緩存數(shù)據(jù)一致性解決方案

一、序言

在分布式并發(fā)系統(tǒng)中扣墩,數(shù)據(jù)庫與緩存數(shù)據(jù)一致性是一項(xiàng)富有挑戰(zhàn)性的技術(shù)難點(diǎn)哲银。本文將討論數(shù)據(jù)庫與緩存數(shù)據(jù)一致性問題,并提供通用的解決方案呻惕。

假設(shè)有完善的工業(yè)級分布式事務(wù)解決方案荆责,那么數(shù)據(jù)庫與緩存數(shù)據(jù)一致性便迎刃而解,實(shí)際上亚脆,目前分布式事務(wù)不成熟做院。

二、不同的聲音

在數(shù)據(jù)庫與緩存數(shù)據(jù)一致解決方式中濒持,有各種聲音键耕。

  • 先操作數(shù)據(jù)庫后緩存還是先緩存后數(shù)據(jù)庫
  • 緩存是更新還是刪除
1、操作的先后順序

在并發(fā)系統(tǒng)中柑营,數(shù)據(jù)庫與緩存雙寫場景下屈雄,為了追求更大的并發(fā)量,操作數(shù)據(jù)庫與緩存顯而易見不會同步進(jìn)行官套。前者操作成功后者以異步的方式進(jìn)行棚亩。

關(guān)系型數(shù)據(jù)庫作為成熟的工業(yè)級數(shù)據(jù)存儲方案,有完善的事務(wù)處理機(jī)制虏杰,數(shù)據(jù)一旦落盤,不考慮硬件故障勒虾,可以負(fù)責(zé)任的說數(shù)據(jù)不會丟失纺阔。

所謂緩存,無非是存儲在內(nèi)存中的數(shù)據(jù)修然,服務(wù)一旦重啟笛钝,緩存數(shù)據(jù)全部丟失质况。既然稱之為緩存,那么時刻做好了緩存數(shù)據(jù)丟失的準(zhǔn)備玻靡。盡管Redis有持久化機(jī)制结榄,是否能夠保證百分之百持久化?Redis將數(shù)據(jù)異步持久化到磁盤有不可囤捻,緩存是緩存臼朗,數(shù)據(jù)庫是數(shù)據(jù)庫,兩個不同的東西蝎土。把緩存當(dāng)數(shù)據(jù)庫使用是一件極其危險(xiǎn)的事情视哑。

從數(shù)據(jù)安全的角度來講,先操作數(shù)據(jù)庫誊涯,然后以異步的方式操作緩存挡毅,響應(yīng)用戶請求。

2暴构、處理緩存的態(tài)度

緩存是更新還是刪除跪呈,對應(yīng)懶漢式飽漢式,從處理線程安全實(shí)踐來講取逾,刪除緩存操作相對難度低一些耗绿。如果在刪除緩存的前提下滿足了查詢性能,那么優(yōu)先選擇刪除緩存菌赖。

更新緩存盡管能夠提高查詢效率缭乘,然后帶來的線程并發(fā)臟數(shù)據(jù)處理起來較麻煩,序言引入MQ等其它消息中間件琉用,因此非必要不推薦堕绩。

三、線程并發(fā)分析

理解線程并發(fā)所帶來問題的關(guān)鍵是先理解系統(tǒng)中斷邑时,操作系統(tǒng)在任務(wù)調(diào)度時奴紧,中斷隨時都在發(fā)生,這是線程數(shù)據(jù)不一致產(chǎn)生的根源晶丘。以4和8線程CPU為例黍氮,同一時刻最多處理8個線程,然而操作系統(tǒng)管理的線程遠(yuǎn)遠(yuǎn)超過8個浅浮,因此線程們以一種看似并行的方式進(jìn)行沫浆。

(一)查詢數(shù)據(jù)

1、非并發(fā)環(huán)境

在非并發(fā)環(huán)境中滚秩,使用如下方式查詢數(shù)據(jù)并無不妥:先查詢緩存专执,如果緩存數(shù)據(jù)不存在,查詢數(shù)據(jù)庫郁油,更新緩存本股,返回結(jié)果攀痊。

public BuOrder getOrder(Long orderId) {
    String key = ORDER_KEY_PREFIX + orderId;
    BuOrder buOrder = RedisUtils.getObject(key, BuOrder.class);
    if (buOrder != null) {
        return buOrder;
    }
    BuOrder order = getById(orderId);
    RedisUtils.setObject(key, order, 5, TimeUnit.MINUTES);
    return order;
}

如果在高并發(fā)環(huán)境中有一個嚴(yán)重缺陷:當(dāng)緩存失效時,大量查詢請求涌入拄显,瞬間全部打到DB上苟径,輕則數(shù)據(jù)庫連接資源耗盡,用戶端響應(yīng)500錯誤躬审,重則數(shù)據(jù)庫壓力過大服務(wù)宕機(jī)棘街。

2、并發(fā)環(huán)境

因此在并發(fā)環(huán)境中盒件,需要對上述代碼進(jìn)行修改蹬碧,使用分布式鎖。大量請求涌入時炒刁,獲得鎖的線程有機(jī)會訪問數(shù)據(jù)庫查詢數(shù)據(jù)恩沽,其余線程阻塞。當(dāng)查詢完數(shù)據(jù)并更新緩存翔始,然后釋放鎖罗心。等待的線程重新檢查緩存,發(fā)現(xiàn)能夠獲取到數(shù)據(jù)城瞎,直接將緩存數(shù)據(jù)響應(yīng)渤闷。

這里提到分布式鎖,那么使用表鎖還是行鎖呢脖镀?使用分布式行鎖提高并發(fā)量飒箭;使用二次檢查機(jī)制,確保等待獲得鎖的線程能夠快速返回結(jié)果

@Override
public BuOrder getOrder(Long orderId) {
    /* 如果緩存不存在蜒灰,則添加分布式鎖更新緩存 */
    String key = ORDER_KEY_PREFIX + orderId;
    BuOrder order = RedisUtils.getObject(key, BuOrder.class);
    if (order != null) {
        return order;
    }
    String orderLock = ORDER_LOCK + orderId;
    RLock lock = redissonClient.getLock(orderLock);
    if (lock.tryLock()) {
        order = RedisUtils.getObject(key, BuOrder.class);
        if (order != null) {
            LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
            return order;
        }
        BuOrder buOrder = getById(orderId);
        RedisUtils.setObject(key, buOrder, 5, TimeUnit.MINUTES);
        LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
    }
    return RedisUtils.getObject(key, BuOrder.class);
}

(二)更新數(shù)據(jù)

1弦蹂、非并發(fā)環(huán)境

非并發(fā)環(huán)境中,如下代碼盡管可能會產(chǎn)生數(shù)據(jù)不一致問題(數(shù)據(jù)被覆蓋)强窖。盡管使用數(shù)據(jù)庫層面樂觀鎖能夠解決數(shù)據(jù)被覆蓋問題凸椿,然而無效更新流量依舊會流向數(shù)據(jù)庫。

public Boolean editOrder(BuOrder order) {
    /* 更新數(shù)據(jù)庫 */
    updateById(order);
    /* 刪除緩存 */
    RedisUtils.deleteObject(OrderServiceImpl.ORDER_KEY_PREFIX + order.getOrderId());
    return true;
}
2翅溺、并發(fā)環(huán)境

上面分析中使用數(shù)據(jù)庫樂觀鎖能夠解決并發(fā)更新中數(shù)據(jù)被覆蓋的問題脑漫,然而當(dāng)同一行記錄被修改后,版本號發(fā)生改變咙崎,后續(xù)并發(fā)流向數(shù)據(jù)庫的請求為無效流量优幸。減小數(shù)據(jù)庫壓力的首要策略是將無效流量攔截在數(shù)據(jù)庫之前。

使用分布式鎖能夠保證并發(fā)流量有序訪問數(shù)據(jù)庫褪猛,考慮到數(shù)據(jù)庫層面已經(jīng)使用了樂觀鎖劈伴,第二個及以后獲得鎖的線程操作數(shù)據(jù)庫為無效流量。

線程在獲得鎖時采用超時退出的策略,等待獲得鎖的線程超時快速退出跛璧,快速響應(yīng)用戶請求,重試更新數(shù)據(jù)操作新啼。

public Boolean editOrder(BuOrder order) {
    String orderLock = ORDER_LOCK + order.getOrderId();
    RLock lock = redissonClient.getLock(orderLock);
    try {
        /* 超時未獲取到鎖追城,快速失敗,用戶端重試 */
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            /* 更新數(shù)據(jù)庫 */
            updateById(order);
            /* 刪除緩存 */
            RedisUtils.deleteObject(OrderServiceImpl.ORDER_KEY_PREFIX + order.getOrderId());
            /* 釋放鎖 */
            LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
            return true;
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return false;
}

(三)依賴環(huán)境

上述代碼使用了封裝鎖的工具類燥撞。

<dependency>
  <groupId>xin.altitude.cms</groupId>
  <artifactId>ucode-cms-common</artifactId>
  <version>1.4.3.2</version>
</dependency>

LockOptional根據(jù)鎖的狀態(tài)執(zhí)行后續(xù)操作座柱。

四、先數(shù)據(jù)庫后緩存

(一)數(shù)據(jù)一致性

1物舒、問題描述

接下來討論先更新數(shù)據(jù)庫色洞,后刪除緩存是否存在并發(fā)問題。

(1)緩存剛好失效
(2)請求A查詢數(shù)據(jù)庫冠胯,得一個舊值
(3)請求B將新值寫入數(shù)據(jù)庫
(4)請求B刪除緩存
(5)請求A將查到的舊值寫入緩存

上述并發(fā)問題出現(xiàn)的關(guān)鍵是第5步比第3火诸、4步后發(fā)生,由操作系統(tǒng)中斷不確定因素可知荠察,此種情況卻有發(fā)生的可能置蜀。

2、解決方式

從實(shí)際情況來看悉盆,將數(shù)據(jù)寫入Redis遠(yuǎn)比將數(shù)據(jù)寫入數(shù)據(jù)庫耗時要短盯荤,盡管發(fā)生的概率較低,但仍會發(fā)生焕盟。

(1)增加緩存過期時間

增加緩存過期時間允許一定時間范圍內(nèi)臟數(shù)據(jù)存在秋秤,直到下一次并發(fā)更新出現(xiàn),可能會出現(xiàn)臟數(shù)據(jù)脚翘。臟數(shù)據(jù)會周期性存在灼卢。

(2)更新和查詢共用一把行鎖

更新和查詢共用一把行分布式鎖,上述問題不復(fù)存在堰怨。當(dāng)讀請求獲取到鎖時芥玉,寫請求處于阻塞狀態(tài)(超時會快速失敗返回),能夠保證步驟5在步驟3之前進(jìn)行备图。

(3)延遲刪除緩存

使用RabbitMQ延遲刪除緩存灿巧,去除步驟5的影響。使用異步的方式進(jìn)行揽涮,幾乎不影響性能抠藕。

(二)特殊情況

數(shù)據(jù)庫有事務(wù)機(jī)制保證操作成功與否;Redis單條指令具有原子性蒋困,然后組合起來卻不具備原子特征盾似,具體來說是數(shù)據(jù)庫操作成功,然后應(yīng)用異常掛掉,導(dǎo)致Redis緩存未刪除零院。Redis服務(wù)網(wǎng)絡(luò)連接超時出現(xiàn)此問題溉跃。

如果設(shè)置有緩存過期時間,那么在緩存尚未過期前告抄,臟數(shù)據(jù)一直存在撰茎。如果未設(shè)置過期時間,那么直到下一次修改數(shù)據(jù)前打洼,臟數(shù)據(jù)一直存在龄糊。(數(shù)據(jù)庫數(shù)據(jù)已經(jīng)發(fā)生改變,緩存尚未更新)

解決方式

在操作數(shù)據(jù)庫前募疮,向RabbitMQ寫入一條延遲刪除緩存的消息炫惩,然后執(zhí)行數(shù)據(jù)庫操作,執(zhí)行緩存刪除操作阿浓。不管代碼層面緩存是否刪除成功他嚷,MQ刪除緩存作為保底操作。

五搔扁、小結(jié)

上述方式提供的數(shù)據(jù)庫與緩存數(shù)據(jù)一致性解決方式爸舒,屬于耦合版,當(dāng)然還有訂閱binlog日志的解耦版稿蹲。解耦版由于增加了訂閱binlog組件扭勉,對系統(tǒng)穩(wěn)定性提出更高的要求。

數(shù)據(jù)庫與緩存一致性問題看似是解決數(shù)據(jù)問題苛聘,實(shí)質(zhì)上解決并發(fā)問題:在盡可能保證更多并發(fā)量的前提下涂炎,在保證數(shù)據(jù)庫安全的前提下,保證數(shù)據(jù)庫與緩存數(shù)據(jù)一致设哗。


喜歡本文點(diǎn)個??贊??支持一下唱捣,如有需要,可通過微信dream4s與我聯(lián)系网梢。相關(guān)源碼在GitHub震缭,視頻講解在B站,本文收藏在博客天地战虏。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拣宰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子烦感,更是在濱河造成了極大的恐慌巡社,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件手趣,死亡現(xiàn)場離奇詭異晌该,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門朝群,熙熙樓的掌柜王于貴愁眉苦臉地迎上來燕耿,“玉大人,你說我怎么就攤上這事姜胖「卓茫” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吧凉。 經(jīng)常有香客問我,道長阀捅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任饲鄙,我火速辦了婚禮凄诞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘忍级。我一直安慰自己,他們只是感情好汛蝙,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著朴肺,像睡著了一般窖剑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上戈稿,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機(jī)與錄音需了,去河邊找鬼。 笑死援所,一個胖子當(dāng)著我的面吹牛欣除,可吹牛的內(nèi)容都是我干的住拭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼杠娱,長吁一口氣:“原來是場噩夢啊……” “哼谱煤!你這毒婦竟也來了摊求?” 一聲冷哼從身側(cè)響起刘离,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茧痕,沒想到半個月后恼除,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豁辉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年徽级,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片灰追。...
    茶點(diǎn)故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖朴下,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情殴胧,我是刑警寧澤佩迟,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站报强,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏秉溉。R本人自食惡果不足惜碗誉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一父晶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧甲喝,春花似錦、人聲如沸埠胖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谊惭。三九已至侮东,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悄雅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工众眨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留容诬,地道東北人娩梨。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓狈定,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纽什。 傳聞我的和親對象是個殘疾皇子躲叼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容