緩存一致性問題

一.場景

項目中爲了對dao(data access object)層數據做緩存,設計了一層das(data access service),在das上採用了aop的方式作統一緩存處理。這裏做的是緩存刪除操作。

二.問題

數據庫和緩存的一致性問題。

三.定義

A: 刪除緩存
B: 提交數據庫事務
C: 讀取緩存(如果緩存數據爲空,會從數據庫讀取舊數據)
A-B: A和B同步
A–B: A和B異步

四.分析

1.順序異常分析

1.1 先A後B

注意:刪除緩存的操作在數據庫事務中執行的情況也屬於先A後B
(1)A失敗了,B不會執行,不影響一致性。
(2)A成功了,B失敗了。因爲是刪除緩存操作(讀請求會從數據庫讀取舊數據),所以不影響一致性。
(3)先A,後B,存在時間間隙,如果在這段間隙中有其它讀請求C,那麼就會出現數據不一致。

1.2 先B後A

(1)B失敗,A不會執行,不影響一致性。
(2)B成功,A失敗,會出現數據不一致。

1.3小結

從上面分析可以發現,兩種順序的執行都會存在導致數據不一致的問題。

2.方案分析

2.1先A後B

解決問題(3)
(1)阻塞加鎖的方式,A-B。優點:保證強一致,缺點:性能不高,高併發情況不適合,緩存讀寫阻塞不好實現。
(2)在B操作中加入數據庫持久化日誌,使用定時任務補償,A-B–AA-B同步(併發不高情況下,可以降低C執行的概率),B–A異步(補償)。優點:基本不影響緩存性能,缺點:增加日誌數據和定時任務的開銷。

2.2先B後A

解決問題(2)
(1)在B操作中加入數據庫持久化日誌,使用定時任務補償,B-A–A,原理跟上面第(2)點一樣。
(2)事務消息,生產者事務生產和消費者事務消費,B操作中需要加入數據庫持久化日誌,異步A操作,定時任務做MQ補償,B–A。優點:異步隊列處理性能高,操作解耦,適合A操作量大的情況,缺點:轉移到解決MQ一致性的問題上,而且還增加日誌數據、定時任務和MQ的開銷。

2.3小結

考慮用戶體驗的話,A-B–A方案比較合適;A操作複雜耗時的情況下,B–A的方案比較合適。

2.4方案改進

(1)B–A方案可以通過失敗重試的方式來提高一致性的效率,即B-A(n)–A,意思是在A同步重試n次失敗後再發出消息作補償,優點:提高同步成功率,避免異步開銷,缺點:業務線程操作耗時,需要適當設置重試時間策略。
(2)B–A的方案不一定要用分佈式MQ,還可以用本地消息隊列,優點:不需要處理MQ一致性問題,提升效率,缺點:業務應用程序需要增加本地消息隊列的開銷。

五.項目中的實踐

普通緩存(查詢併發量少)

業務代碼

	@Transactional
	@DelCache
	public void updateStock(Long itemId, Integer num){
		//刪除緩存
		deleteCache(itemId, num);
		//打印日誌
		printLog(itemId, num);
		//更新數據庫
		updateDb(itemId, num);

	}

@DelCache aop 在@Transactional 事務提交後執行

	public void afterReturning(JoinPoint point, Object result) throws Exception{
		try{
			//再一次刪除緩存
	    	deleteCache(itemId, num);
		} cache (Exception){
			//數據庫中插入補償日誌,後面由定時任務輪詢重試刪除緩存
			insertCompensateLog(itemId, num);
		}
	}

小結:項目中的這種做法相對簡單,只有在出現異常的時候才需要把數據持久化,交給定時任務做重試,即A-B-A,緩存雙刪。

秒殺緩存(查詢併發量大)

業務代碼

	public void deductStock(Long itemId, Integer num){
		//扣減庫存+校驗超賣(原子操作)
		decrStock(itemId, num);
		//打印日誌
		printLog(itemId, num);
		try{
			//更新數據庫
			updateDb(itemId, num);
		} catch (Exception e){
			//打印日誌
			printLog(itemId, num);
			//異常緩存回滾
			incrStock(itemId, num);
			//把異常拋給上層
			throw e;
		}
	}

小結:項目秒殺場景,這裏考慮到併發量會很大,所以選擇更新緩存的操作,先緩存扣減庫存,以抵抗高併發壓力。注意,這裏的 deductStock 不需要加事務。
還有一個情況:就是緩存回滾會可能失敗,因爲這種情況出現的概率是非常低的,這裏會採用定時任務輪詢作補償。具體方案:
1.在 updateDb 的時候需要同時在同一個事務插入一條庫存扣減記錄。
2.實際業務中,每個訂單都有一個自動取消時間,如果在規定時間內不進行支付就會取消,並且作庫存回滾。
3.如果更新庫存的數據庫操作失敗了,那麼,訂單是不會創建的,庫存的記錄時間如果超過了自動取消時間,並且訂單不存在或者訂單狀態未支付,那麼就把這個庫存記錄的數量加到緩存中。

普通緩存(查詢併發量大)

這種情況的一致性要求一般不高,但是查詢量大,如果使用刪除緩存的操作,作緩存更新,後面讀緩存的時候需要使用同步加載數據到緩存,也就是會出現阻塞的情況。爲了避免出現阻塞,可以把這種查詢請求量大,但是一致性要求不高的操作,做緩存讀寫分離,也就是把這種操作分離出來,讀庫的數據不作失效設置,通過寫庫進行數據寫入,然後通過同步機制進行讀庫數據的更新。這個過程是有一定的時間差的,但可以解決出現阻塞的情況。

六.總結

在分佈式場景下,解決一致性的問題往往需要耗費很多資源。需要根據業務場景適當取捨。

參考:
B-A(n)–A 失敗重試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章