說明:
僅僅是本身的一些觀點和思考,若有問題歡迎指正
文中【】內提到的spring boot starter是本身封裝的,地址:https://gitee.com/itopener/springbootcss
解耦神器:MQhtml
MQ 是分佈式架構中的解耦神器,應用很是廣泛。有些分佈式事務也是利用 MQ 來作的。因爲其高吞吐量,在一些業務比較複雜的狀況,能夠先作基本的數據驗證,而後將數據放入MQ,由消費者異步去處理後續的複雜業務邏輯,這樣能夠大大提升請求響應速度,提高用戶體驗。若是消費者業務處理比較複雜,也能夠獨立集羣部署,根據實際處理能力需求部署多個節點。須要注意的是:前端
須要確認消息發送MQ成功
好比RabbitMQ在發送消息到MQ時,就有發送回調確認,雖然不可以徹底避免消息丟失,但也可以避免一些極端狀況下消息發送失敗的狀況了。能夠利用MQ的事務來避免更多狀況的消息丟失。nginx
消息持久化
須要注意配置消息持久化,避免MQ集羣掛掉的狀況下大量丟失消息的狀況git
消息消費的冪等性
正常來講消息是不會重複發送的,可是一些特殊狀況也可能會致使消息重複發送給消費者,通常會在消息中加一個全局惟一的流水號,經過流水號來判斷消息是否已經消費過redis
注意用戶體驗
使用異步處理是在提升系統吞吐量考慮下的一種設計,相對於實時快速給用戶返回結果,確定用戶體驗會更差一點,但這也是目前來講綜合考慮的一種不錯的方案了,所以在設計之初就須要評估是否須要異步處理,若是須要異步處理,那必定要考慮如何給用戶更友好的提示和引導。由於異步處理是技術實現結合實際業務狀況的一種綜合解決方案,對於產品來講是不該該關心的,須要技術人員主動儘早提出流程中異步處理的節點,在需求分析階段就考慮如何設計才能對用戶來講更加友好。若是在開發過程當中才提出,極可能就會對用戶展現界面有較大調整,從而致使需求變動、系統設計變動,然後就是甩鍋、扯皮、延期了。算法
庫存扣減spring
庫存扣減的實現方式有不少種,並且涉及到扣減庫存的時候還須要結合實際業務場景來決定實現方案,除了扣減庫存,還須要記錄一些業務數據。數據庫在高併發量的應用中很容易遇到瓶頸,因此能夠考慮使用Redis + MQ來作請求的處理,由MQ消費者去實現後續的業務邏輯。這樣可以較快速的響應請求,避免請求阻塞而引起更多的問題:數據庫
使用 Redis 來作庫存扣減
利用Redis中的incr命令來實現庫存扣減的操做。Redis從2.6.0版本開始內置了Lua解釋器,而且對Lua腳本的執行是具備原子性的,因此能夠利用此特性來作庫存的扣減,具體實現能夠參考【stock-spring-boot-starter】,starter中主要實現了初始化/重置庫存、扣減庫存、恢復庫存。後端
Redis集羣的效率已經很是高了,可以支撐必定量的併發扣減庫存,而且因爲Redis執行Lua腳本的原子性能夠避免超扣的問題。若是一個Redis集羣還知足不了業務須要,能夠考慮將庫存進行拆分。即將庫存拆成多份,分別放到不一樣的Redis集羣當中,多個Redis集羣採用輪詢策略,基本可以在大致上保證各個Redis集羣的剩餘庫存量不會相差太大。不過也不能絕對的保證數量均勻,因此在扣減庫存操做返回庫存不足時,仍是須要必定的策略去解決這個問題,好比扣減庫存返回庫存不足時,繼續輪詢到下一個Redis集羣,當全部Redis集羣都返回庫存不足時,能夠在應用節點內或某個統一的地方打個標記表示已沒有庫存,避免每一個請求都輪詢所有的Redis集羣。
扣減庫存的冪等性
因爲利用Redis的incr命令來扣減庫存,無法存儲請求源的信息,因此扣減庫存的冪等性由應用來保證,能夠利用客戶端token或流水號之類的來作。
MQ 異步處理業務數據
扣減庫存都會伴隨一些業務數據須要記錄,若是實時記錄到數據庫,仍然很容易達到瓶頸,因此能夠利用MQ,將相關信息放入MQ,而後由MQ消費者去異步處理後續的業務邏輯。固然若是MQ消息發送失敗須要恢復Redis中的庫存,Redis操做和MQ操做沒法徹底保證一致性,因此在保證正常狀況下數據一致性的前提下,還須要相似對帳同樣來驗證扣減庫存和實際庫存的一致性。不過在這以前,我認爲須要更優先考慮限流問題,須要提早壓測出應用的性能瓶頸,根據壓測結果對請求配置限流,優先保證高併發狀況下應用不會崩潰掉,這樣才能更好的保證接收到的請求可以按正常代碼邏輯處理,減小發生庫存不一致的狀況。
限流
相信不少猿都遇到過併發量猛增致使系統崩潰的狀況,因此建議提早壓測出系統性能瓶頸,包含各個應用接口、數據庫、緩存、MQ等的瓶頸,而後根據壓測結果配置對應的限流值,這樣能夠很大程度避免應用由於大量請求而掛掉。固然這也會帶來其餘的問題,好比如下兩個方面:
監控,及時擴容
應用限流後就決定了只能處理必定量的請求,對於增加期應用來講,通常仍是但願可以處理更多的用戶請求,畢竟意味着帶來更多的用戶、更多的收益。因此就須要監控應用流量,根據實際狀況及時進行擴容,提升整個系統的處理能力,以便爲更多的用戶提供服務。
用戶體驗
當應用達到限流值時,須要給用戶更好的提示和引導,這也是須要在需求分析階段就須要考慮的。
限流前置
在實際的系統架構中,用戶請求可能會通過多級纔會到達應用節點,好比:nginx-->gateway-->應用。若是條件容許,能夠在儘可能靠前的位置作限流設置,這樣能夠儘早的給用戶反饋,也能夠減小後續層級的資源浪費。不過畢竟在應用內增長限流配置的開發成本相對來講較低,而且可能會更靈活,因此須要根據團隊實際狀況而定了。nginx作限流設置可使用Lua+Redis配合來實現;應用內限流可使用RateLimiter來作。固然均可以經過封裝來實現動態配置限流的功能,好比【ratelimiter-spring-boot-starter】
緩存
在高併發應用中,確定避免不了數據的頻繁讀寫,這時候緩存就可以起到很大做用了,通常會使用像Redis集羣這樣的高性能緩存,減小數據庫的頻繁讀取,以提升數據的查詢效率,這裏主要提下如下場景:
多級緩存
雖然Redis集羣這種緩存的性能已經很高了,可是也避免不了網絡消耗,在高併發系統中,這些消耗是可能會引發很嚴重後果的,也須要儘可能減小。能夠考慮多級緩存,將一些變動頻率很是低的數據放入應用內緩存,這樣就能夠在應用內直接處理了,相比使用集中式緩存來講,在高併發場景仍是可以提升很大效率的,能夠參考【cache-redis-caffeine-spring-boot-starter】實現兩級緩存,也能夠參考開源中國的J2Cache,支持多種兩級緩存的方式。須要注意的就是緩存失效時一級緩存的清理,由於一級緩存是在應用內,對於集羣部署的系統,應用之間是無法直接通訊的,只能藉助其餘工具來進行通知並清理一級緩存。如利用Redis的發佈訂閱功能來實現同一應用不一樣節點間的通訊。
CDN
CDN也是一種緩存,只是主要適用於一些靜態資源,好比:css、js、png圖片等,前端會使用的較多。在一些場景下,能夠結合動靜分離、先後端分離,將前端資源所有放入CDN中,可以很大程度提升訪問效率。須要注意的是前端靜態資源是可能會更新的,當有更新的時候須要刷新CDN緩存。或者另外一種策略是在靜態資源的地址上增長一個相似版本號的標誌,這樣每次修改後的路徑就會不同,上線後CDN就會直接回源到本身應用內獲取最新的文件並緩存在CDN中。使用CDN就須要一套比較完善的自動化部署的工具了,否則每次修改後上線就會比較麻煩。
前端緩存
前端html中能夠配置靜態資源在前端的緩存,配置後瀏覽器會緩存一些資源,當用戶刷新頁面時,只要不是強制刷新,就能夠不用再經過網絡請求獲取靜態資源,也可以必定程度提升頁面的響應速度。
緩存穿透
若是不作處理,那麼每次請求都會回源到數據庫查詢數據。若是有人惡意利用這種不存在的數據大量請求系統,那麼就會致使大量請求到數據庫中執行查詢操做。這種狀況就叫作緩存穿透。在高併發場景下更須要防止這種狀況的發生。
防止:若是數據庫中查詢不到數據,能夠往緩存裏放一個指定的值,從緩存中取值時先判斷一下,若是是這個指定的值就直接返回空,這樣就能夠都從緩存中獲取數據了,從而避免緩存穿透的問題。也能夠根據緩存對象的實際狀況,採用兩級緩存的方式,這樣也能夠減小緩存設備的請求量。redis是經常使用的緩存,可是不能存儲null,所以spring cache模塊中定義了一個NullValue對象,用來表明空值。spring boot中Redis方式實現spring cache是有一些缺陷的(spring boot 1.5.x版本),具體參考[https://my.oschina.net/dengfuwei/blog/1616221]中提到的#RedisCache實現中的缺陷#
緩存雪崩
緩存雪崩主要是指因爲緩存緣由,大量請求到達了數據庫,致使數據庫壓力過大而崩潰。除了上面提到的緩存穿透的緣由,還有多是緩存過時的瞬間有大量的請求須要處理,從緩存中判斷無數據,而後就直接查詢數據庫了。這也是在高併發場景下比較容易出現的問題。
防止:當緩存過時時,回源到數據庫查詢的時候須要作下處理,如:加互斥鎖。這樣就可以避免在某個時間點有大量請求到達數據庫了,固然也能夠對方法級別作限流處理,好比:hystrix、RateLimiter。也能夠經過封裝實現緩存在過時前的某個時間點自動刷新緩存。spring cache的註解中有一個sync屬性,主要是用來表示回源到數據查詢時是否須要保持同步,因爲spring cache只是定義標準,沒有具體緩存實現,因此只是根據sync的值調用了不一樣的Cache接口的方法,因此須要在Cache接口的實現中注意這點。
在緩存的使用方面,會有各類各樣複雜的狀況,建議能夠整理一下各類場景並持續完善,這樣能夠在後續使用緩存的過程當中做爲參考,也能夠避免由於考慮不周全引發的異常,對於員工的培養也是頗有好處的。
數據預先處理
對於一些業務場景,能夠提早預處理一些數據,在使用的時候就能夠直接使用處理結果了,減小請求時的處理邏輯。如對於限制某些用戶參與資格,能夠提早將用戶打好標記,這樣在用戶請求時就能夠直接判斷是否有參與資格,若是數據量比較大,還能夠根據必定規則將數據分佈存儲,用戶請求時也根據此規則路由到對應的服務去判斷用戶參與資格,減輕單節點壓力和單服務數據量,提升總體的處理能力和響應速度。
資源前置
目前不少都是分佈式微服務架構,就可能會致使調用鏈路很長,所以能夠將一些基本的判斷儘可能前置,好比用戶參與資格、前面提到的限流前置、或者一些資源直接由前端請求到目的地址,而不是經過服務端轉發;涉及機率型的高併發請求,能夠考慮在用戶訪問時即隨機一部分結果,在前端告知用戶參與失敗。總之,就是將能提早的儘可能提早,避免調用鏈路中不符合條件的節點作無用功。
熔斷降級
在微服務架構中,會有不少的接口調用,當某些服務出現調用時間較長或沒法提供服務的時候,就可能會形成請求阻塞,從而致使響應緩慢,吞吐量下降的狀況。這時候就有必要對服務進行降級處理。當超過指定時間或服務不可用的時候,採起備用方案繼續後續流程,避免請求阻塞時間太長。好比對於機率性的請求(如抽獎),當處理時間過長時直接認爲隨機結果是無效的(如未中獎)。須要注意的是:
· 配置熔斷降級的時間須要綜合權衡一下具體配置多少,並且正常狀況下是可以快速響應的,當出現處理時間超時的狀況或服務不可用的狀況,就須要監控及時告警,以便儘快恢復服務。
· 當出現熔斷降級的時候,須要有對應的機制,好比:重試、回退。須要保證業務數據在代碼邏輯上的一致性。
可使用hystrix來實現熔斷降級處理
補償機制
對於一些業務處理失敗後須要有補償機制,例如:重試、回退等。
· 重試須要限制重試次數,避免死循環,超過次數的須要及時告警,以便人工處理或其餘處理。重試就須要保證冪等性,避免重複處理致使的不一致的問題。
· 回退。當超太重試次數或一些處理失敗後,須要回退的,須要考慮周全一些,避免出現數據不一致的狀況。
冪等性
在實際處理中可能會出現各類各樣的狀況致使重複處理,就須要保證處理的冪等性,通常可使用全局惟一的流水號來進行惟一性判斷,避免重複處理的問題,主要是在MQ消息處理、接口調用等場景。全局惟一的流水號能夠參考tweeter的snowflake算法【sequence-spring-boot-starter】。具體生成的位置就須要根據實際業務場景決定了,主要是須要考慮各類極端的異常狀況。
監控告警
在高併發系統中,用戶量自己就很大,一旦出現問題影響範圍就會比較大,因此監控告警就須要及時的反饋出系統問題,以便快速恢復服務。必需要創建比較完善的應對流程,建議也能夠創建對應的經驗庫,對常見問題進行記錄,一方面避免重複發生,另外一方面在發生問題時能夠及時定位問題。
自動化運維方面須要大力建設,能夠很大程度提升線上問題的響應和解決速度。而且須要有全鏈路監控機制,能夠更方便的排查線上問題並快速解決。全鏈路監控能夠考慮像pingpoint、zipkin、OpenCensus等。
人員管理
· 分工要明確,須要有隨時接收並處理問題的人員;
· 信息透明,團隊成員須要對系統有足夠的瞭解,須要讓團隊成員有獨當一面的能力;
· 知識庫,整理技術上、業務上的常見問題、經驗,方便新成員快速理解並融入;
· 分享,按期分享技術上、業務上的知識,團隊成員共同快速進步。適當的分享系統運行成果,能夠適當鼓舞團隊士氣;
· 適當與業務溝通,瞭解一線業務需求和使用狀況,以便不斷改善,也能夠在系統設計上有更長遠的考慮;
· 適當用一些項目管理工具,適當將一些工做進行量化。不適合團隊的成員也須要及時淘汰。
避免過分設計
· 避免由於少數極端狀況作過多處理;
· 避免過分拆分微服務,儘可能避免分佈式事務。
代碼結構和規範
· 要注意代碼結構的設計,提升代碼可重用率;· 避免嚴格遵照代碼規範,代碼規範能夠下降新成員的理解難度,也能夠下降團隊成員間互相理解的難度。