天天看點

資料量大導緻寫效率低如何優化(6)【寫緩存】一、業務場景五二、寫緩存三、此方案的價值和不足

我們在上一篇文章裡面詳細讨論了緩存的架構方案,它可以減少資料庫讀操作的壓力,卻也存在着不足。比如寫操作并發量大時,這個方案并不奏效,那該怎麼辦呢?我們先來看一個具體的業務場景。

一、業務場景五

有一個這樣的場景:一場超低價預約大型線上活動,在某天9:00-9:15期間,使用者可以前往預約詳情頁半價預約搶購一款熱門商品。根據市場部門的策劃方案,這次活動營運目标是幾十萬左右的預約量。

面對如此之大的預約量,如何防止湧進來的請求壓垮資料庫?

之前,公司給系統做個一次壓測:并發量保持在8000左右時,系統響應速度最高,并發量資料要是再上升,系統響應速度就會急劇變慢。如果幾十萬使用者同時在那個期間預約商品,可以預見高峰期(特别是九點那一瞬間)的并發量肯定超過10000,到時候資料庫肯定會因為承受不住而當機。

為避免活動上線後出現此問題,我們必須提前做好預備方案。不過,在這場活動中,上司要求在架構上不要做太大調整。(說白了就是工期不能太長,也不能購買太多伺服器。)

為此,最終采用的方案是不讓預約的請求直接插入資料庫,而是先存放到性能很高的緩沖地帶,以此保證洪峰期間先沖擊洪峰地帶,之後再從緩沖地帶異步勻速搬運到資料庫中。

因使用的方案比較簡單,是以這個方案設計不到2周就上線了,且活動期間使用者體驗全程沒有卡頓,上司很滿意,績效保住了。。。

你肯定會有疑問,這個問題分庫分表不就能解決嗎?是倒是可以,不過代價太大且成本效益不高。畢竟這次僅僅是一個臨時性市場活動,且前面也說了,這次活動營運目标是幾十萬的預約量,這點數量采取分庫分表的話,未免有點得不償失。

其實,以上介紹的解決方案就是寫緩存,這也是我們接下來要講解的重點内容。

二、寫緩存

什麼是寫緩存呢?寫緩存的思路就是背景服務接收到使用者請求時,如果請求校驗沒問題,資料并不會直接落庫,而是先存儲在緩沖層中,待緩沖層中寫請求達到一定數量再進行批量落庫。這裡所說的緩沖地帶,實際上指的就是寫緩存。它的意義在于利用寫緩存比資料庫高幾個量級的吞吐能力來承受洪峰流量,再平速搬運資料到資料庫中。

設計圖如下圖:

資料量大導緻寫效率低如何優化(6)【寫緩存】一、業務場景五二、寫緩存三、此方案的價值和不足

從以上設計方案中,不難看出寫緩存可以大幅減少資料庫寫操作頻率,進而減少資料庫壓力。

上面這張圖看起來簡單,但該方案在具體實施過程中,往往需要考慮六大問題。

(一)、寫請求與批量落庫這兩個操作是同步還是異步?

在讨論這個問題前,我們先來聊聊同步與異步之間的差別。

1、同步與異步的差別

比如同步,寫請求送出資料後,寫操作的線程會等到批量落庫完成後才開始啟動。這種設計的好處是使用者預約成功後,可在我的預約頁面立即看到預約資料,壞處是使用者送出預約後,還需要在頁面上等待一段時間才能傳回結果,且這個時間不定,有可能需要等待一個完整的時間窗。

比如異步,寫請求送出資料後,會直接提示使用者送出成功。這種設計的好處是使用者能快速知道送出結果,壞處是使用者送出完成後,如果手癢前往我的預約頁面檢視,可能會出現沒有資料的情況,這時使用者就蒙圈了。

那我們到底應該使用哪種設計模式呢?先别急,我們再來讨論下這兩種設計模式的複雜度。

2、同步與異步的複雜度

同步的實作原理是寫請求送出資料時,寫請求的線程被堵塞住或者等待,待批量落庫完成後再發送信号給寫請求的線程,這個線程獲得落庫完成的信号後,最後回報預約成功給使用者。

不過,這個過程會引出一系列的問題,比如:

  • 使用者到底需要等待多久?使用者不可能無限期等待下去,此時我們還需要設定一個時間窗,比如每隔100ms批量落庫1次。
  • 如果批量落庫逾時了怎麼辦?寫請求也不可能無限期等待,此時就需要給寫請求的線程的堵塞設定一個逾時時間。
  • 如果批量落庫失敗了怎麼辦?是否需要重試?多久重試一次?
  • 如果寫請求一直堵塞在那直到重試成功再傳回?那需要重試幾次?這些邏輯其實與springcloud元件、hystrix請求合并功能(hystrix2018年已停止更新)等類似。

如果使用異步的話,上面的第二點、第四點基本不用考慮,從複雜度的角度來說,異步會比同步簡單很多,是以後面我們直接選用異步的方式,預約資料儲存到緩沖層即可傳回結果。

關于異步的使用者體驗設計,共有2種設計方案可供業務方選擇。

1、在我們的預約界面給使用者一個提示:您的預約訂單可能會有一定時間延遲。

2、使用者預約成功後,直接進入預約完成詳情頁,此頁面會定時發請求查詢背景批量落庫的狀态,如果落庫成功,則彈出成功提示,并跳轉至下一個界面。

(二)如何觸發批量落庫

關于批量落庫觸發邏輯,目前市面上共分為2種觸發方式。

1、寫請求滿足特定次數後就落庫1次,比如10個請求落一次。

按照次數批量落庫的優點是通路資料庫的次數變為1/N,從資料庫壓力上來說會小很多。不過也存在不足,如果通路資料庫的次數未湊齊N次,使用者的預約就一直無法落庫。

2、每隔1個時間視窗落庫1次,比如每隔1s落庫1次。

按照時間視窗落庫的優點是能保證使用者等待的時間不會太久,其缺點就是某個瞬時流量太大,在那個視窗落庫的資料就會很多,多到在1次資料庫通路中沒法完成所有資料的插入(比如1s内堆積了5000條資料),它們隻好通過分批次實作插入,這不就變回第1種邏輯了嗎?

那到底那種觸發邏輯好呢?我們之前方案是這兩種方式同時使用,具體實作邏輯如下:

1、每收集1次寫請求,插入預約資料到緩存中,再判斷緩存中預約數的總數是否達到一定數量,達到後直接觸發批量落庫。

2、開一個定時器,每隔1s觸發1次批量落庫。

通過以上操作,我們既可以避免資料量不足導緻的無法落庫,也避免了因瞬時流量大,待插入資料堆積太多的情況。

(三)緩沖資料存儲在哪裡?

緩沖資料不僅可以放在本地記憶體中,也可以存在分布式緩存中(比如Redis),其中最簡單的辦法是存在本地記憶體中。

你可能想問,hystrix的請求合并好像也是放在本地記憶體中?嗯,确實是,不過寫緩存與hystrix的請求合并有點不一樣,請求合并更多的是考慮讀請求的合并,不用擔心資料丢失,而寫請求需要考慮容災問題。如果伺服器出現當機,記憶體資料就會丢失,使用者的預約資料也就沒有了,後果就不用說了吧。。。。

是以我們就使用分布式緩存好了。在上一篇文章裡我們對分布式緩存技術選型做了介紹,是以我們這次就直接選用Redis了。

接下來,我們需要考慮批量落庫的設計了,批量落庫主要是把Redis中的預約資料移動到資料庫中。那麼問題就來了,當新的資料一直增加,批量落庫可能會出現多個線程同時處理的問題,此時就需要考慮并發性了。

(四)緩沖層并發操作需要注意什麼?

實際上,緩沖層并發操作邏輯與冷熱分離搬運邏輯很相似,但這裡我們來聊個不一樣的。

如果你對下面英文感興趣,可以先看下 MySQL 官方文檔中關于 Concurrent Inserts 的描述:

TheMyISAMstorage engine supports concurrent inserts to reduce contention between readers and writers for a given table: If aMyISAMtable has no holes in the data file (deleted rows in the middle), anINSERTstatement can be executed to add rows to the end of the table at the same time thatSELECTstatements are reading rows from the table.

If there are multipleINSERTstatements, they are queued and performed in sequence, concurrently with theSELECTstatements. The results of a concurrentINSERTmay not be visible immediately.

加粗地方大緻意思是:如果多個 insert 語句同時執行,它們會按根據排隊情況按順序執行,也可以與 select 語句并發執行。

這裡,我們再結合上面的場景具體說明下緩沖層并發操作時需要注意什麼。

與冷熱分離不一樣的地方在于,這次我們并不需要搬運海量資料,因為每隔 1 秒或資料量湊滿 10 條,資料就會自動搬運一次,是以 1 次 batch insert 操作就能輕松搞定這個問題,我們隻需要在并發性的設計方案中保證一次僅有一個線程批量落庫就行。這個邏輯比較簡單,我們就不贅述了。

(五)批量落庫失敗了怎麼辦?

在考慮落庫失敗這個問題之前,我們先來看下批量落庫的實作邏輯。

  • 首先,目前線程從緩存中擷取所有資料,因為每10條執行1次落庫操作,不需要擔心緩存資料量過多,也不用考慮将獲得的資料分批次操作了;
  • 其次,目前線程批量儲存資料庫;
  • 最後,目前線程從緩存中删除對應資料。(注意:不能直接清空緩存資料,因為新的預約資料可能插入到緩存中了。)

那在批量落庫的過程中,如果這個操作失敗了怎麼辦?我們自有妙招。

可能失敗的步驟 處理方案
從緩存中擷取所有的資料 如果資料未修改完無需復原<br />下次觸發的落庫線程會自動把前面未處理的資料進行處理
批量儲存資料庫 用事務包裹,如果失敗就復原,下次直接從第1步開始
從緩存中删除對應資料 失敗就失敗,不用復原到前面的步驟<br />這就會出現有些資料插入資料庫後還在緩存中,下次線程會重複将這些資料插入到資料庫的情況,是以我們需要確定資料庫儲存支援幂等(方法是使用手機号做唯一索引,這樣就會自動忽略重複插入)

我們已經知道了批量落庫每一步可能失敗的處理的解決辦法,接下來就是如何確定資料不丢失。

(六)Redis的高可用配置

在上面的業務場景裡,我們是先把使用者送出的資料儲存到緩存中,是以必須保證緩存中的資料不丢失,這就要求我們實作Redis的資料備份。

現如今,Redis共支援2種備份方式,我們一起來看下。

備份方式 描述
快照 1、Redis滿足特定條件時,會将記憶體中所有的資料儲存到硬碟。<br />2、Redis崩潰恢複後,可通過快照還原資料,但快照最後一次生成的資料會丢失。
AOF 1、AOF類似MySQL的binglog,Redis的每個操作都會記錄到AOF檔案中,Redis崩潰後可通過AOF恢複資料(比快照恢複慢一些)<br />2、AOF的fsync可以是每秒1次,也可以每個請求都fsync,前者會丢失1秒的資料,後者會影響請求性能。

另外,Redis還有一個主從的功能,這裡我們就不深度展開了。如果公司存在一個統一管理的Redis叢集方案,直接複用公司的就行,最起碼運維有保障。

而如果需要從0開始搭建,我認為最簡單的解決方案如下:

1、先使用簡單的主從模式;

2、然後在Slave Redis裡使用快照(30秒1次)+AOF(1秒1次)的配置;

3、如果master當機了,千萬别直接啟動,先把slave更新為master;

4、這時代碼裡已經有預案了,寫緩存如果失敗直接落庫;

不過這個方案有個缺點,一旦系統當機,手動恢複時大家會手忙腳亂,但資料很有保障。

三、此方案的價值和不足

寫緩存這個解決方案可以緩解寫資料請求量太大壓垮資料庫的問題,但還是存在不足。

繼續閱讀