14年雙11大促緩存方案,今天有點閒暇時間,回顧一下當時的思路。前端
大促活動下,對於某些產品進行整點秒殺活動。預計流量是平時峯值5+倍
。redis
商品計算邏輯比較複雜:某個最終展現的商品屬性和價格,可能須要上億次動態條件計算得到,動態條件每時每刻都在變化,而且商品的庫存屬性屬於行業共有庫存,每時每刻都在變化。後端
計算模型:前端機併發去後端獲取實時計算數據,而後合併結果,根據用戶信息
給商品打屬性,排序。緩存
針對這種場景,有不少方案能夠嘗試,不過總結起來,大概倆個方向:擴容
和緩存
。併發
擴容是最容易想到的方式,並且每一年大促,根據壓測和運營活動預期,均可能有相應擴容。擴容從某種程度上說,也是最簡單的方式,若是應用規劃足夠好,沒有狀態,那麼基本不用開發介入就能完成。運維
可是若是應用涉及狀態信息,那麼擴容就沒有說的那麼輕巧,擴容涉及到增長集羣狀態;活動結束後,機器下線涉及集羣減小狀態,這一增一減,增長了運維的成本和系統穩定性。異步
擴容還有一個很差的地方就是活動結束後,系統水位降低,閒下來4倍的機器,比較浪費。分佈式
相對擴容,緩存是一種從應用角度出發,優化系統的方案。緩存的方案可細分不一樣粒度,分別適用不一樣場景。優化
靜態化能最大限度下降最大限度下降後端壓力,通常靜態內容能夠定時或者經過數據更像觸發生成,而後推送到CDN節點。靜態化適用於1)讀多寫少的數據
,或者2)可以容忍數據變化延遲
的場景。對於本文介紹的場景,並不適合,緣由在於商品不知足前面說的兩點,而且每一個登錄用戶看到的產品價格和屬性是不一樣的。spa
靜態化這種」一刀切」模式,不能知足針對每一個用戶的個性化展現需求,若是把每一個用戶看到的數據都靜態化,那緩存的命中率有會很低,基本每一個用戶請求一兩次就不會再來了。並且緩存數據量巨大。
由此想到把緩存粒度縮小,把緩存從展現層後推到前端機
上。由於前端機負責彙總後端結算結果,並根據用戶信息給商品打屬性,排序。
通過頭腦風暴,最終肯定採用緩存中間結果
方式。接下來討論一下方案細節。
若是緩存有數據,取緩存數據,若是沒有,請求後端並把結果更新到緩存。
這是一種最簡單的緩存模式,但不幸的是不適合秒殺場景,由於秒殺開始的時候,緩存很能沒有數據,請求會穿透
到後端。
實時請求數據來自緩存,緩存數據定時異步更新。
粗看起來,這個方案不存在緩存穿透
的狀況,由於數據不會實時從後端計算獲取,而是從緩存獲取,若是緩存數據存在,直接獲取便可。緩存更新能夠把用戶請求彙總後去重,定時更新。
上面討論的兩種方式都一個共性問題:第一批請求
問題:若是第一批請求緩存沒有數據怎麼辦?
簡單粗暴
的方式會讓這樣的請求穿透緩存
,後端去處理並更新緩存。這樣會給後端計算帶來壓力,秒殺開始那一剎那,極可能支撐不住。
而實時緩存
的犧牲了這樣的請求,由於這些請求根本看不到數據,因此請求失敗。這兩種方式在本文的應用場景都不合適。
的確,上面兩個方案若是能在第一批請求
到達前初始化好緩存,那基本上能夠知足本文的應用場景的。並且看起來也很容易作到,提早模擬一次請求
或者提早往緩存放一份數據
不就能夠了嗎?
不幸的是,本文場景由於涉及數據範圍巨大,不能在較短期內遍歷緩存key,初始化好緩存,即便採起併發方式。並且,初始化緩存請求過多,也將給後端機器形成壓力。
根據需求,兩種方案的緩存不會永久有效
,若是緩存失敗了怎麼辦?
對於簡單粗暴
方式,若是緩存失效,又會遇到第一批請求
問題,一批請求發現緩存失效,怎麼辦?看來即便解決了緩存初始化問題,還有可能致使緩存穿透。
實時緩存
模式也有相似問題,若是異步更新前數據已經失效了,那麼將犧牲一批數據失效後到更新前這批用戶。由於沒有人去更新數據。
無論哪一種方式,分佈式緩存更新都存在併發問題,尤爲在整點秒殺場景更爲突出。對於簡單粗暴
方式,能夠採用分佈式鎖解決:若是緩存穿透的一批請求只有一個會真正打到後端是否是就能夠解決了?
實時緩存
也有一樣的問題,只不過異步請求能夠把一段時間內的重複請求合併成一個,從側面避免了併發問題。
把上面的討論結合,能夠獲得一種更優雅的緩存方案,既不犧牲第一批請求,也不存在緩存穿透問題,同時避免併發更新問題。
想象有這樣一個哨兵線程,只有它能去後端請求實時數據,並更新緩存。
第一批請求場景:
第一批請求中,選取最先的那個請求爲哨兵,這個線程不會去讀緩存,直接去後端獲取計算結果並更新緩存。其餘普通線程則自旋+sleep等待,直到哨兵更新緩存後,能拿到數據爲止。
緩存失效場景:
哨兵的做用是讓緩存永不失效。哨兵線程提早甦醒
,去後端獲取計算數據並更新緩存。這樣,其餘普通線程根本不會感知到緩存已經失效,他們能一直拿到最新的緩存。
例如,某個key的緩存失效時間的12:00:00
,那麼哨兵可能在11:59:55
的時候甦醒,請求後端並於11:59:57
的時候完成緩存數據更新,後續請求線程感知不到數據的更新,一直能取到非過時的數據。
哨兵:其實哨兵也是一個普通請求。能夠用原子計數器(redis或tair)實現,一個數據有兩個key:原子計數器key和數據緩存key,兩者緩存時間一致,可是計數器key失效時間比數據key的要早(至少提早一個後端請求RT時間,這樣能保證哨兵更新緩存後,不被其餘線程感知到)。當請求線程發現緩存沒有數據的時候,每一個線程去更新計數器,更新後,獲得計數器爲1的線程,被設置成哨兵線程,其餘線程則等待哨兵。
普通請求沒有獲取到數據的時候,自旋+sleep應該有個超時時間,防止意外狀況。若是超時了,根據業務場景選擇請求後端數據仍是處理失敗。