前段時間接了一個雙11的活動,業務邏輯:**用戶購買某一類商品後,活動期間的4個整點在活動頁點擊button領取紅包,每一個時段獎品數量有限,先到先得。**聽着很簡單,但是活動開始時,異常火熱,流量超過了咱們的預估,原本是分時段領取獎品,結果演變爲了秒殺。前端
當時每一個整點的QPS瞬間飆高,響應時間RT短期內居高不下,可是整個Check下來應用所有機器的負載都很是正常,後來全面查找緣由,才找到問題的根源, 是因爲每一個時段更新數據庫同一條獎品致使超時!mysql
整點開搶後瞬時巨量的請求同時涌入,即便咱們Apache端作過初步限流,應用也作了信號量的控制,並且加上分佈式緩存的使用,減緩了至關大的壓力,整個業務邏輯校驗階段運做良好,可是系統的瓶頸就轉移到其餘環節:減獎品庫存!由於咱們每一個時段只有一個獎品A,每次減庫存都是update獎品A中的獎品餘額字段!大量符合發獎要求的用戶請求瞬時涌入數據庫去更新此條記錄,update鎖行,致使後面的請求所有排隊等待,等前面一個update完成釋放行鎖後才能處理下一個請求,大量請求等待,佔用了數據庫的鏈接!一旦數據庫同一時間片內的鏈接數被打滿,就會致使這個時間片內其餘後來的所有請求因拿不到鏈接而超時,致使訪問此數據庫的其餘環節也出現問題!因此RT就會異常飆高!nginx
根據木桶理論,咱們後續確定必須得優化這個最短板,將這個瓶頸解決!針對這樣的狀況,咱們這邊出了兩套方案:一、強依賴分佈式緩存達到減庫存的目的;二、熱點/單點數據拆分,弱依賴分佈式緩存,採用分散熱點的方式減庫存.下面請容許我詳細分解下這兩套方案,也但願你們提各類建設性意見!sql
應用中使用分佈式緩存來存儲當前時間段的獎品餘額,有用戶中獎則將此緩存中的餘額減一,不須要查詢和實時更新數據庫,而是每隔自定義的一段時間將緩存中的餘額異步更新至數據庫中。數據庫
優勢:這種方式徹底依賴於緩存,讀寫速度快,不須要實時更新數據庫,下降了數據庫至關大的壓力;數組
缺點:緩存不是100%穩定,很容易丟,即便採用持久化的緩存,在高併發下有時也會出問題; 一旦丟失數據,這樣就致使數據庫記錄的獎品餘額比實際真實存在的獎品餘額要多,這個時候讀數據庫,就會致使獎品多發,也就是所謂的超賣!緩存
某時段的一個獎品拆分爲多條後,如何能保證先到先得的業務需求將獎品準確發完,這裏就引入分佈式緩存做爲輔助,緩存不徹底穩定不要緊,只是藉助其在多條獎品中進行準確分發,當數據庫全部獎品都有餘額的狀況時,能減小查詢操做!只有當某一條獎品餘額爲0時緩存中的數據纔會失效,這時才須要查詢一次數據庫!服務器
步驟: 數組M:獎品的總數。 數字P:沒有獎品的行。 數字R:有獎品的行。 若是數字R中有,可是數據庫中沒有,更新緩存。 一、 同個時段的獎品拆爲多份(好比10份),加行號N區分(1~10),獎1~10的數值存入數組M中; 二、 根據行號1~10 ,查詢分佈式緩存中是否存在各行獎品對應的記錄(緩存中存放沒餘額的獎品行); 是: 將存在的行號存入數組P; 否: 數組P值爲NULL; 三、 數組M-數組P=數組R; 四、 判斷數組R是否爲空 ; 是: 沒有獎品餘額,返回未中獎! 否: 在數組R中隨機一個行號L; 五、 更新數據庫表,將L行獎品餘額減一; 更新成功: 減獎品庫存成功,直接返回發獎成功! 更新失敗: 極大可能緣由是因爲沒有獎品致使 5.一、 查詢數據庫中這10個獎品List(全量list); 5.二、 將List中沒有獎品餘額的行同步至對應的緩存(第2 步),並判斷List中是否全部獎品行餘額全爲0; 是: 無獎品,返回未中獎; 否: List中選擇一個有餘額的獎品,最好是餘額最多的,將行號存入L,執行第5 步;
優勢:此方案不會發生獎品多發的狀況,將單行數據分拆爲多行,分散了熱點,一樣能夠減輕數據庫更新時超負荷長鏈等待致使的鏈接被等待用戶佔用然後續請求超時,能夠經過拆分爲適量的行來解決單點熱點數據帶來的性能問題!架構
缺點:此方案須要作業務拆解,增長了業務的複雜性!獎品拆分爲多條,數據量太大時,不是很便捷,可能會帶來數據庫性能問題,但這個能夠經過分庫分表,舊數據遷移備份的方式解決!在獎品快被抽完的那麼幾微秒的用戶可能存在誤殺!併發
這就是目前針對數據庫的單點熱點問題,我我的的一些看法,也只是初步構想,尚未進入徹底的實踐中,還但願各位大神多指點一二!
二 解決方案 從上面的背景分析,解決熱點數據併發更新須要注意核心問題: 減小直接對db層數據熱點的併發更新,本文從業務和數據庫的設計層面來規劃.同時也但願你們提更好的解決思路。 1 前端層面 前端是整個流量的入口, 正常業務訪問時系統表現平穩,可是當有人惡意請求時,須要加上流控措施,好比常見的 a 須要用戶回答問題,填寫驗證碼,移動圖像等等,防止或者減小有機器人來惡意請求。 b 頁面上採用防止機器人的判斷 兩秒之內的成功請求一概拒絕。 c 經過設置nginx ,對同一個ip源的請求次數作限制,防止機器人來申請。 優勢 有效減小或者防止有人利用機器人惡意請求 缺點 存在必定的誤殺率,錯殺了正常的請求。 2 應用層 應用程序接收前端前端請求,進行一系列的數據庫操做,在咱們規避了惡意請求以後若是仍是有大量的數據庫寫訪問請求,咱們須要 a 對業務作降級 限制接口的調用次數,下降對數據庫的請求壓力。 選擇不更新請求次數,弱化該商品申請次數的展示。相似於閱讀次數,申請次數 ,與金額,庫存無關的功能點。 b 經過異步更新來避免直接寫數據庫 。 應用使用分佈式緩存(好比tair)來存儲某項商品的申請次數或者某人的申請次數,以商品id/user_id 或者將where 條件做爲key,申請試用人數爲value/符合某項具體條件的 count結果爲value, 有用戶申請成功則更新申請試用人數。不須要查詢和實時寫數據庫,每隔必定時間/次數將結果寫入數據庫。 優勢:該方法徹底依賴於緩存,讀寫速度快,不須要實時更新數據庫,減輕數據庫併發寫的壓力; 缺點:緩存不是100%穩定,很容易丟,即便採用持久化的緩存,在高併發下有時也可能會出現異常,穿透緩存到db ,致使前端業務展示問題。 3 數據庫層 a 將熱點數據拆分,分在不一樣的庫不一樣的表中,分散熱點數據,減輕數據庫併發更新熱點帶來的RT升高和應用鏈接等待時能保證業務可以正常訪問其餘商品表,損失局部可用性。 優勢:實時讀寫數據庫,前端展現數據的準確性。 缺點:業務邏輯稍顯複雜。 b 限流補丁 針對某些特定的sql語句 從MySQL 層面加以限制,當系統thread_running達到必定值或者某個sql執行時間超過必定閾值則拒絕該sql的執行。(阿里內部已經實現限流版本) c 使用MySQL的 thread pool 功能。在併發較大時,one to one的模式會引發Innodb的mutex鎖爭用。當前解決方法是經過innodb_thread_concurrency參數,可是該參數自身也存在鎖爭用,一樣影響了MySQL的性能。 優勢:thread pool主要從四個方面考慮:減小SQL併發,使得有足夠的資源:使用線程組,獨立管理:使用優先級隊列,限制併發執行的事務:避免死鎖。 缺點:在測試過程當中發現,會有大量的鏈接等待kernel mutex鎖,可是持續的壓力會致使MySQL的thread running飆高,最終致使MySQL不可用。
## 1、緩存式的Web應用程序架構:
在Web層和db層之間加一層cache層,主要目的:減小數據庫讀取負擔,提升數據讀取速度。cache存取的媒介是內存,能夠考慮採用分佈式的cache層,這樣更容易破除內存容量的限制,同時增長了靈活性。
操做步驟:
一般狀況下在PHP中MySQL查詢是串行的,若是能實現MySQL查詢的異步化,就能實現多條SQL語句同時執行,這樣就能大大地縮短MySQL查詢的耗時,提升數據庫查詢的效率。目前MySQL的異步查詢只在MySQLi擴展提供,查詢方法分別是:
一、使用MYSQLI_ASYNC模式執行mysqli::query
二、獲取異步查詢結果:mysqli::reap_async_query
使用mysql異步查詢,須要使用mysqlnd做爲PHP的MySQL數據庫驅動。
使用MySQL異步查詢,由於須要給全部查詢都建立一個新的鏈接,而MySQL服務端會爲每一個鏈接建立一個單獨的線程進行處理,若是建立的線程過多,則會形成線程切換引發系統負載太高。Swoole中的異步MySQL其原理是經過MYSQLI_ASYNC模式查詢,而後獲取mysql鏈接的socket,加入到epoll事件循環中,當數據庫返回結果時會回調指定函數,這個過程是徹底異步非阻塞的。
當數據庫的寫壓力增長,cache層(如Memcached)只能緩解數據庫的讀取壓力。讀寫集中在一個數據庫上讓數據庫不堪重負。使用主從複製技術(master-slave模式)來達到讀寫分離,以提升讀寫性能和讀庫的可擴展性。讀寫分離就是隻在主服務器上寫,只在從服務器上讀,基本原理是讓主數據庫處理事務性查詢,而從數據庫處理select查詢,數據庫複製被用於把事務性查詢(增刪改)致使的改變動新同步到集羣中的從數據庫。
MySQL讀寫分離提高系統性能:
一、主從只負責各自的讀和寫,極大程度緩解X鎖和S鎖爭用。
二、slave能夠配置MyISAM引擎,提高查詢性能以及節約系統開銷。
三、master直接寫是併發的,slave經過主庫發送來的binlog恢復數據是異步的。
四、slave能夠單獨設置一些參數來提高其讀的性能。
五、增長冗餘,提升可用性。
實現主從分離可使用MySQL中間件如:Atlas
在cache層的高速緩存,MySQL的主從複製,讀寫分離的基礎上,這時MySQL主庫的寫壓力開始出現瓶頸,而數據量的持續猛增,因爲MyISAM使用表鎖,在高併發下會出現嚴重的鎖問題,大量的高併發MySQL應用開始使用InnoDB引擎代替MyISAM。採用Master-Slave複製模式的MySQL架構,只能對數據庫的讀進行擴展,而對數據的寫操做仍是集中在Master上。這時須要對數據庫的吞吐能力進一步地擴展,以知足高併發訪問與海量數據存儲的需求。
對於訪問極爲頻繁且數據量巨大的單表來講,首先要作的是減小單表的記錄條數,以便減小數據查詢所需的時間,提升數據庫的吞吐,這就是所謂的分表【水平拆分】。在分表以前,首先須要選擇適當的分表策略,使得數據可以較爲均衡地分佈到多張表中,而且不影響正常的查詢。
分表可以解決單表數據量過大帶來的查詢效率降低的問題,可是卻沒法給數據庫的併發處理能力帶來質的提高。面對高併發的讀寫訪問,當數據庫master服務器沒法承載寫操做壓力時,無論如何擴展Slave服務器都是沒有意義的,對數據庫進行拆分,從而提升數據庫寫入能力,即分庫【垂直拆分】。
分庫分表的理由策略以下:
一、中間變量=user_id % ( 庫數量 * 每一個庫的表數量 )
二、庫=取整(中間變量 / 每一個庫的表數量)
三、表=中間變量 % 每一個庫的表數量
數據庫通過業務拆分及分庫分表,雖然查詢性能和併發處理能力提升了。可是本來跨表的事務上升爲分佈式事務;因爲記錄被切分到不一樣的庫和不一樣的表中,難以進行多表關聯查詢,而且不能不指定路由字段對數據進行查詢。且分庫分表後須要進一步對系統進行擴容(路由策略變動)將變得很是不方便,須要從新進行數據遷移。