緩存與數據庫一致性之一:緩存更新設計


一、緩存更新場景介紹

緩存是一種提高系統讀性能的常見技術,對於讀多寫少的應用場景,我們經常使用緩存來進行優化。

例如對於用戶的余額信息表account(uid, money),業務上的需求是:

(1)查詢用戶的余額,SELECT money FROM account WHERE uid=XXX,占99%的請求

(2)更改用戶余額,UPDATE account SET money=XXX WHERE uid=XXX,占1%的請求


由於大部分的請求是查詢,我們在緩存中建立uid到money的鍵值對,能夠極大降低數據庫的壓力。

讀操作流程

有了數據庫和緩存兩個地方存放數據之后(uid->money),每當需要讀取相關數據時(money),操作流程一般是這樣的:

(1)讀取緩存中是否有相關數據,uid->money

(2)如果緩存中有相關數據money,則返回【這就是所謂的數據命中“hit”】

(3)如果緩存中沒有相關數據money,則從數據庫讀取相關數據money【這就是所謂的數據未命中“miss”】,放入緩存中uid->money,再返回

緩存的命中率 = 命中緩存請求個數/總緩存訪問請求個數 = hit/(hit+miss)

上面舉例的余額場景,99%的讀,1%的寫,這個緩存的命中率是非常高的,會在95%以上。

問題

當數據money發生變化的時候:

(1)是更新緩存中的數據,還是淘汰緩存中的數據呢?

(2)是先操縱數據庫中的數據再操縱緩存中的數據,還是先操縱緩存中的數據再操縱數據庫中的數據呢?

(3)緩存與數據庫的操作,在架構上是否有優化的空間呢?

這是本文關注的三個核心問題。

 

1.1、更新緩存 VS 淘汰緩存

什么是更新緩存:數據不但寫入數據庫,還會寫入緩存

什么是淘汰緩存:數據只會寫入數據庫,不會寫入緩存,只會把數據淘汰掉

更新緩存的優點:緩存不會增加一次miss,命中率高

淘汰緩存的優點:簡單

那到底是選擇更新緩存還是淘汰緩存呢,主要取決於“更新緩存的復雜度”。

例如,上述場景,只是簡單的把余額money設置成一個值,那么:

(1)淘汰緩存的操作為deleteCache(uid)

(2)更新緩存的操作為setCache(uid, money)

更新緩存的代價很小,此時我們應該更傾向於更新緩存,以保證更高的緩存命中率

 

如果余額是通過很復雜的數據計算得出來的,例如業務上除了賬戶表account,還有商品表product,折扣表discount

account(uid, money)

product(pid, type, price, pinfo)

discount(type, zhekou)

業務場景是用戶買了一個商品product,這個商品的價格是price,這個商品從屬於type類商品,type類商品在做促銷活動要打折扣zhekou,購買了商品過后,這個余額的計算就復雜了,需要:

(1)先把商品的品類,價格取出來:SELECT type, price FROM product WHERE pid=XXX

(2)再把這個品類的折扣取出來:SELECT zhekou FROM discount WHERE type=XXX

(3)再把原有余額從緩存中查詢出來money = getCache(uid)

(4)再把新的余額寫入到緩存中去setCache(uid, money-price*zhekou)

更新緩存的代價很大,此時我們應該更傾向於淘汰緩存。

however,淘汰緩存操作簡單,並且帶來的副作用只是增加了一次cache miss,建議作為通用的處理方式。

1.2、先操作數據庫 vs 先操作緩存

OK,當寫操作發生時,假設淘汰緩存作為對緩存通用的處理方式,又面臨兩種抉擇:

(1)先寫數據庫,再淘汰緩存

(2)先淘汰緩存,再寫數據庫

究竟采用哪種時序呢?

還記得在《數據庫間的一致性:數據庫冗余表數據一致性》文章(點擊查看)里“究竟先寫正表還是先寫反表”的結論么?

對於一個不能保證事務性的操作,一定涉及“哪個任務先做,哪個任務后做”的問題,解決這個問題的方向是:

如果出現不一致,誰先做對業務的影響較小,就誰先執行。

由於寫數據庫與淘汰緩存不能保證原子性,誰先誰后同樣要遵循上述原則。


假設先寫數據庫,再淘汰緩存:第一步寫數據庫操作成功,第二步淘汰緩存失敗,則會出現DB中是新數據,Cache中是舊數據,數據不一致。


假設先淘汰緩存,再寫數據庫:第一步淘汰緩存成功,第二步寫數據庫失敗,則只會引發一次Cache miss。

結論:數據和緩存的操作時序,結論是清楚的:先淘汰緩存,再寫數據庫。

 

1.3、緩存架構優化

上述緩存架構有一個缺點:業務方需要同時關注緩存與DB,有沒有進一步的優化空間呢?有兩種常見的方案,一種主流方案,一種非主流方案(一家之言,勿拍)。


主流優化方案是服務化:加入一個服務層,向上游提供帥氣的數據訪問接口,向上游屏蔽底層數據存儲的細節,這樣業務線不需要關注數據是來自於cache還是DB。


非主流方案是異步緩存更新:業務線所有的寫操作都走數據庫,所有的讀操作都總緩存,由一個異步的工具來做數據庫與緩存之間數據的同步,具體細節是:

(1)要有一個init cache的過程,將需要緩存的數據全量寫入cache

(2)如果DB有寫操作,異步更新程序讀取binlog,更新cache

在(1)和(2)的合作下,cache中有全部的數據,這樣:

(a)業務線讀cache,一定能夠hit(很短的時間內,可能有臟數據),無需關注數據庫

(b)業務線寫DB,cache中能得到異步更新,無需關注緩存

這樣將大大簡化業務線的調用邏輯,存在的缺點是,如果緩存的數據業務邏輯比較復雜,async-update異步更新的邏輯可能也會比較復雜。

五、其他未盡事宜

本文只討論了緩存架構設計中需要注意的幾個細節點,如果數據庫架構采用了一主多從,讀寫分離的架構,在特殊時序下,還很可能引發數據庫與緩存的不一致,這個不一致如何優化,后續的文章再討論吧。

六、結論強調

(1)淘汰緩存是一種通用的緩存處理方式

(2)先淘汰緩存,再寫數據庫的時序是毋庸置疑的

(3)服務化是向業務方屏蔽底層數據庫與緩存復雜性的一種通用方式

七、那這種問題有什么好辦法解決呢?

要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。

有一個比較巧妙的作法是,可以將這個不存在的key預先設定一個值。

比如,"key" , “&&”。

在返回這個&&值的時候,我們的應用就可以認為這是不存在的key,那我們的應用就可以決定是否繼續等待繼續訪問,還是放棄掉這次操作。如果繼續等待訪問,過一個時間輪詢點后,再次請求這個key,如果取到的值不再是&&,則可以認為這時候key有值了,從而避免了透傳到數據庫,從而把大量的類似請求擋在了緩存之中。

二、緩存並發

有時候如果網站並發訪問高,一個緩存如果失效,可能出現多個進程同時查詢DB,同時設置緩存的情況,如果並發確實很大,這也可能造成DB壓力過大,還有緩存頻繁更新的問題。

我現在的想法是對緩存查詢加鎖,如果KEY不存在,就加鎖,然后查DB入緩存,然后解鎖;其他進程如果發現有鎖就等待,然后等解鎖后返回數據或者進入DB查詢。

這種情況和剛才說的預先設定值問題有些類似,只不過利用鎖的方式,會造成部分請求等待。

三、緩存失效

引起這個問題的主要原因還是高並發的時候,平時我們設定一個緩存的過期時間時,可能有一些會設置1分鍾啊,5分鍾這些,並發很高時可能會出在某一個時間同時生成了很多的緩存,並且過期時間都一樣,這個時候就可能引發一當過期時間到后,這些緩存同時失效,請求全部轉發到DB,DB可能會壓力過重。

那如何解決這些問題呢?

其中的一個簡單方案就時講緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鍾隨機,這樣每一個緩存的過期時間的重復率就會降低,就很難引發集體失效的事件。

我們討論的第二個問題時針對同一個緩存,第三個問題時針對很多緩存。

 

總結來看:

1、緩存穿透:查詢一個必然不存在的數據。比如文章表,查詢一個不存在的id,每次都會訪問DB,如果有人惡意破壞,很可能直接對DB造成影響。

2、緩存失效:如果緩存集中在一段時間內失效,DB的壓力凸顯。這個沒有完美解決辦法,但可以分析用戶行為,盡量讓失效時間點均勻分布。

當發生大量的緩存穿透,例如對某個失效的緩存的大並發訪問就造成了緩存雪崩。

 

轉自:http://mp.weixin.qq.com/s/CY4jntpM7VNkBrz1FKRsOw


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM