一、序言
在分布式并發系統中,資料庫與快取資料一致性是一項富有挑戰性的技術難點,本文將討論資料庫與快取資料一致性問題,并提供通用的解決方案,
假設有完善的工業級分布式事務解決方案,那么資料庫與快取資料一致性便迎刃而解,實際上,目前分布式事務不成熟,
二、不同的聲音
在資料庫與快取資料一致解決方式中,有各種聲音,
- 先操作資料庫后快取還是先快取后資料庫
- 快取是更新還是洗掉
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洗掉快取作為保底操作,
五、小結
上述方式提供的資料庫與快取資料一致性解決方式,屬于耦合版,當然還有訂閱binlog日志的解耦版,解耦版由于增加了訂閱binlog組件,對系統穩定性提出更高的要求,
資料庫與快取一致性問題看似是解決資料問題,實質上解決并發問題:在盡可能保證更多并發量的前提下,在保證資料庫安全的前提下,保證資料庫與快取資料一致,
喜歡本文就【??推薦??】一下,激勵我持續創作,這個Github同樣精彩,收到您的star我會很激動,本文歸檔在專題博客,視頻講解在B站,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/444304.html
標籤:Java
上一篇:flowable 表結構說明
