一:業務場景---庫存系統
庫存可能會修改,每次修改都要去更新這個緩存(redis)數據; 每次庫存的數據在緩存中一旦過期,或者是被清理掉了,前端的nginx服務都會發送請求給庫存服務,去獲取相應的數據
實際上的處理流程沒有這么的簡單,這里,其實就涉及到了一個問題,數據庫與緩存雙寫,數據不一致的問題
我們的緩存模式采用cache aside pattern,所以對於修改庫存,我們采用先刪除緩存中的庫存數據+再修改DB中的庫存數據,對於讀取庫存,我們采用先讀緩存中的庫存數據,如果緩存中沒有,讀DB的中的庫存數據。
二:數據庫與緩存雙寫不一致問題分析
A請求刪除緩存成功,開始修改數據庫,這時候B請求讀取緩存,發現緩存中沒有數據,於是讀取DB,這是A請求還沒有修改完DB,這時候B請求開始回寫緩存,這就造成緩存中的數據是舊數據,DB中的數據是修改后的數據。
三:為什么高並發場景下,緩存會出現這個問題
是因為在對一個數據進行並發的讀寫的時候,才可能會出現這種問題,其實如果說你的並發量很低的話,特別是讀並發很低,每天訪問量就1萬次,那么很少的情況下,會出現剛才描述的那種不一致的場景
四:數據庫與緩存雙寫不一致問題的解決方案
對數據庫與緩存更新與讀取操作進行異步串行化,更新數據的時候,根據數據的唯一標識,將操作路由之后,發送到一個jvm內部的隊列中 讀取數據的時候,如果發現數據不在緩存中,那么將重新讀取數據+更新緩存的操作,根據唯一標識路由之后,也發送同一個jvm內部的隊列中,一個隊列對應一個工作線程 每個工作線程串行拿到對應的操作,然后一條一條的執行,這樣的話,一個數據變更的操作,先執行刪除緩存,然后再去更新數據庫,但是還沒完成更新 此時如果一個讀請求過來,讀到了空的緩存,那么可以先將緩存更新的請求發送到隊列中,此時會在隊列中積壓,然后同步等待緩存更新完成 這里有一個優化點,一個隊列中,其實多個更新緩存請求串在一起是沒意義的,因此可以做過濾,如果發現隊列中已經有一個更新緩存的請求了,那么就不用再放個更新請求操作進去了,直接等待前面的更新操作請求完成即可 待那個隊列對應的工作線程完成了上一個操作的數據庫的修改之后,才會去執行下一個操作,也就是緩存更新的操作,此時會從數據庫中讀取最新的值,然后寫入緩存中 如果請求還在等待時間范圍內,不斷輪詢發現可以取到值了,那么就直接返回; 如果請求等待的時間超過一定時長,那么這一次直接從數據庫中讀取當前的舊值
五:高並發的場景下,該解決方案要注意的問題
(1)讀請求長時阻塞
由於讀請求進行了非常輕度的異步化,所以一定要注意讀超時的問題,每個讀請求必須在超時時間范圍內返回
該解決方案,最大的風險點在於,可能數據更新很頻繁,導致隊列中積壓了大量更新操作在里面,然后讀請求會發生大量的超時,最后導致大量的請求直接走數據庫
務必通過一些模擬真實的測試,看看更新數據的頻繁是怎樣的
另外一點,因為一個隊列中,可能會積壓針對多個數據項的更新操作,因此需要根據自己的業務情況進行測試,可能需要部署多個服務,每個服務分攤一些數據的更新操作
如果一個內存隊列里居然會擠壓100個商品的庫存修改操作,每個庫存修改操作要耗費10ms完成,那么最后一個商品的讀請求,可能等待10 * 100 = 1000ms = 1s后,才能得到數據
這個時候就導致讀請求的長時阻塞
少量情況下,可能遇到讀跟數據更新沖突的情況,如上所述,那么此時更新操作如果先入隊列,之后可能會瞬間來了對這個數據大量的讀請求,但是因為做了去重的優化,所以也就一個更新緩存的操作跟在它后面
等數據更新完了,讀請求觸發的緩存更新操作也完成,然后臨時等待的讀請求全部可以讀到緩存中的數據
(2)讀請求並發量過高
這里還必須做好壓力測試,確保恰巧碰上上述情況的時候,還有一個風險,就是突然間大量讀請求會在幾十毫秒的延時hang在服務上,看服務能不能抗的住,需要多少機器才能抗住最大的極限情況的峰值
但是因為並不是所有的數據都在同一時間更新,緩存也不會同一時間失效,所以每次可能也就是少數數據的緩存失效了,然后那些數據對應的讀請求過來,並發量應該也不會特別大
(3)多服務實例部署的請求路由
可能這個服務部署了多個實例,那么必須保證說,執行數據更新操作,以及執行緩存更新操作的請求,都通過nginx服務器路由到相同的服務實例上
(4)熱點商品的路由問題,導致請求的傾斜
萬一某個商品的讀寫請求特別高,全部打到相同的機器的相同的隊列里面去了,可能造成某台機器的壓力過大
就是說,因為只有在商品數據更新的時候才會清空緩存,然后才會導致讀寫並發,所以更新頻率不是太高的話,這個問題的影響並不是特別大
但是的確可能某些機器的負載會高一些