中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java并發編程的數據庫與緩存數據一致性方案怎么實現

發布時間:2023-05-10 10:34:38 來源:億速云 閱讀:104 作者:zzz 欄目:編程語言

本篇內容介紹了“Java并發編程的數據庫與緩存數據一致性方案怎么實現”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

一、序言

在分布式并發系統中,數據庫與緩存數據一致性是一項富有挑戰性的技術難點。假設有完善的工業級分布式事務解決方案,那么數據庫與緩存數據一致性便迎刃而解,實際上,目前分布式事務不成熟。

二、不同的聲音

在數據庫與緩存數據一致解決方式中,有各種聲音。

  • 先操作數據庫后緩存還是先緩存后數據庫

  • 緩存是更新還是刪除

1、操作的先后順序

在并發系統中,數據庫與緩存雙寫場景下,為了追求更大的并發量,操作數據庫與緩存顯而易見不會同步進行。前者操作成功后者以異步的方式進行。

關系型數據庫作為成熟的工業級數據存儲方案,有完善的事務處理機制,數據一旦落盤,不考慮硬件故障,可以負責任的說數據不會丟失。

所謂緩存,無非是存儲在內存中的數據,服務一旦重啟,緩存數據全部丟失。既然稱之為緩存,那么時刻做好了緩存數據丟失的準備。盡管Redis有持久化機制,是否能夠保證百分之百持久化?Redis將數據異步持久化到磁盤有不可,緩存是緩存,數據庫是數據庫,兩個不同的東西。把緩存當數據庫使用是一件極其危險的事情。

從數據安全的角度來講,先操作數據庫,然后以異步的方式操作緩存,響應用戶請求。

2、處理緩存的態度

緩存是更新還是刪除,對應懶漢式和飽漢式,從處理線程安全實踐來講,刪除緩存操作相對難度低一些。如果在刪除緩存的前提下滿足了查詢性能,那么優先選擇刪除緩存。

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

三、線程并發分析

理解線程并發所帶來問題的關鍵是先理解系統中斷,操作系統在任務調度時,中斷隨時都在發生,這是線程數據不一致產生的根源。以4和8線程CPU為例,同一時刻最多處理8個線程,然而操作系統管理的線程遠遠超過8個,因此線程們以一種看似并行的方式進行。

查詢數據
1、非并發環境

在非并發環境中,使用如下方式查詢數據并無不妥:先查詢緩存,如果緩存數據不存在,查詢數據庫,更新緩存,返回結果。

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;
}

如果在高并發環境中有一個嚴重缺陷:當緩存失效時,大量查詢請求涌入,瞬間全部打到DB上,輕則數據庫連接資源耗盡,用戶端響應500錯誤,重則數據庫壓力過大服務宕機。

2、并發環境

因此在并發環境中,需要對上述代碼進行修改,使用分布式鎖。大量請求涌入時,獲得鎖的線程有機會訪問數據庫查詢數據,其余線程阻塞。當查詢完數據并更新緩存,然后釋放鎖。等待的線程重新檢查緩存,發現能夠獲取到數據,直接將緩存數據響應。

這里提到分布式鎖,那么使用表鎖還是行鎖呢?使用分布式行鎖提高并發量;使用二次檢查機制,確保等待獲得鎖的線程能夠快速返回結果

@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);
}
更新數據
1、非并發環境

非并發環境中,如下代碼盡管可能會產生數據不一致問題(數據被覆蓋)。盡管使用數據庫層面樂觀鎖能夠解決數據被覆蓋問題,然而無效更新流量依舊會流向數據庫。

public Boolean editOrder(BuOrder order) {
    /* 更新數據庫 */
    updateById(order);
    /* 刪除緩存 */
    RedisUtils.deleteObject(OrderServiceImpl.ORDER_KEY_PREFIX + order.getOrderId());
    return true;
}
2、并發環境

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

使用分布式鎖能夠保證并發流量有序訪問數據庫,考慮到數據庫層面已經使用了樂觀鎖,第二個及以后獲得鎖的線程操作數據庫為無效流量。

線程在獲得鎖時采用超時退出的策略,等待獲得鎖的線程超時快速退出,快速響應用戶請求,重試更新數據操作。

public Boolean editOrder(BuOrder order) {
    String orderLock = ORDER_LOCK + order.getOrderId();
    RLock lock = redissonClient.getLock(orderLock);
    try {
        /* 超時未獲取到鎖,快速失敗,用戶端重試 */
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            /* 更新數據庫 */
            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;
}
依賴環境

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

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

LockOptional根據鎖的狀態執行后續操作。

四、先數據庫后緩存

數據一致性
1、問題描述

接下來討論先更新數據庫,后刪除緩存是否存在并發問題。

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

上述并發問題出現的關鍵是第5步比第3、4步后發生,由操作系統中斷不確定因素可知,此種情況卻有發生的可能。

2、解決方式

從實際情況來看,將數據寫入Redis遠比將數據寫入數據庫耗時要短,盡管發生的概率較低,但仍會發生。

  • (1)增加緩存過期時間

增加緩存過期時間允許一定時間范圍內臟數據存在,直到下一次并發更新出現,可能會出現臟數據。臟數據會周期性存在。

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

更新和查詢共用一把行分布式鎖,上述問題不復存在。當讀請求獲取到鎖時,寫請求處于阻塞狀態(超時會快速失敗返回),能夠保證步驟5在步驟3之前進行。

  • (3)延遲刪除緩存

使用RabbitMQ延遲刪除緩存,去除步驟5的影響。使用異步的方式進行,幾乎不影響性能。

特殊情況

數據庫有事務機制保證操作成功與否;Redis單條指令具有原子性,然后組合起來卻不具備原子特征,具體來說是數據庫操作成功,然后應用異常掛掉,導致Redis緩存未刪除。Redis服務網絡連接超時出現此問題。

如果設置有緩存過期時間,那么在緩存尚未過期前,臟數據一直存在。如果未設置過期時間,那么直到下一次修改數據前,臟數據一直存在。(數據庫數據已經發生改變,緩存尚未更新)

解決方式

在操作數據庫前,向RabbitMQ寫入一條延遲刪除緩存的消息,然后執行數據庫操作,執行緩存刪除操作。不管代碼層面緩存是否刪除成功,MQ刪除緩存作為保底操作。

“Java并發編程的數據庫與緩存數據一致性方案怎么實現”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

永泰县| 紫阳县| 策勒县| 章丘市| 无锡市| 普宁市| 河池市| 丹江口市| 桦南县| 织金县| 邵东县| 临漳县| 古丈县| 玉门市| 沈阳市| 芜湖市| 资溪县| 张掖市| 肃宁县| 成安县| 即墨市| 姜堰市| 驻马店市| 巴中市| 庆城县| 丁青县| 灵川县| 沧源| 元谋县| 宁明县| 山西省| 格尔木市| 安顺市| 稷山县| 中山市| 晋城| 句容市| 子洲县| 普洱| 嫩江县| 福清市|