MQ是分佈式架構中的解耦神器,應用很是廣泛。有些分佈式事務也是利用MQ來作的。因爲其高吞吐量,在一些業務比較複雜的狀況,能夠先作基本的數據驗證,而後將數據放入MQ,由消費者異步去處理後續的複雜業務邏輯,這樣能夠大大提升請求響應速度,提高用戶體驗。若是消費者業務處理比較複雜,也能夠獨立集羣部署,根據實際處理能力需求部署多個節點。須要注意的是:css
好比RabbitMQ在發送消息到MQ時,就有發送回調確認,雖然不可以徹底避免消息丟失,但也可以避免一些極端狀況下消息發送失敗的狀況了。能夠利用MQ的事務來避免更多狀況的消息丟失html
須要注意配置消息持久化,避免MQ集羣掛掉的狀況下大量丟失消息的狀況前端
正常來講消息是不會重複發送的,可是一些特殊狀況也可能會致使消息重複發送給消費者,通常會在消息中加一個全局惟一的流水號,經過流水號來判斷消息是否已經消費過node
使用異步處理是在提升系統吞吐量考慮下的一種設計,相對於實時快速給用戶返回結果,確定用戶體驗會更差一點,但這也是目前來講綜合考慮的一種不錯的方案了,所以在設計之初就須要評估是否須要異步處理,若是須要異步處理,那必定要考慮如何給用戶更友好的提示和引導。由於異步處理是技術實現結合實際業務狀況的一種綜合解決方案,對於產品來講是不該該關心的,須要技術人員主動儘早提出流程中異步處理的節點,在需求分析階段就考慮如何設計才能對用戶來講更加友好。若是在開發過程當中才提出,極可能就會對用戶展現界面有較大調整,從而致使需求變動、系統設計變動,然後就是甩鍋、扯皮、延期了mysql
代碼結構和規範nginx
人員管理redis
根據業務場景,將業務抽離成獨立模塊,對外經過接口提供服務,減小系統複雜度和耦合度,實現可複用,易維護,易拓展算法
項目中實踐例子:
Before:spring
在返還購 APP 裏有個【個人紅包】的功能,用戶的紅包數據來自多個業務,如:邀請新用戶註冊領取 100 元紅包,大促活動雙倍紅包,等各類活動紅包,多個活動業務都實現了一套不一樣規則的紅包領取和紅包獎勵發放的機制,致使紅包不可管理,不能複用,難維護難拓展sql
After:
設計概要
Before VS After
產品有時提出的業務需求沒有往這方面去考慮,結合場景和將來拓展須要,在需求討論的時候提出模塊化設計方案,並能夠協助產品進行設計
在項目開發中常常會遇到些相似的功能,可是不一樣的開發人員都各自實現,或者由於不能複用又從新開發一個,致使了相似功能的重複開發,因此咱們須要對可以抽離獨立服務的功能進行抽離,達到複用的效果,而且能夠不斷拓展完善,節約了後續開發成本,提升開發效率,易於維護和拓展
項目中實踐例子:
Before
在業務中常常須要對用戶進行信息通知,如:短信定時通知,APP 消息推送,微信通知,等
開發人員在接到需求中有通知功能的時候沒有考慮後續拓展,就接入第三方信息通知平臺,而後簡單封裝個信息通知方法,後續也有相似信息通知需求的時候,另外一個開發人員發現當前這個通知方法沒法知足本身的需求,而後又本身去了解第三方平臺從新封裝了通知方法,或者後續需求加了定時通知的功能,開發人員針對業務去實現了個定時通知功能,可是隻能本身業務上使用,其餘業務沒法接入,沒有人去作這塊功能的抽離,長此以往就演變成功能重複開發,且不易於維護和拓展
After
接觸到這種能夠抽離通用服務需求的時候,就會與產品確認這種需求是否後續會存在相似的須要,而後建議這把塊需求抽離成通用服務,方便後續維護和拓展
設計概要
Before VS After
Before VS After
先後端分離
對於併發量較大的應用,能夠將先後端分離開,這樣對於前端的資源就可使用nginx等效率高的服務器,而且數據是在前端渲染,不是在服務端經過jsp、freemarker等渲染後返回前端。至關於把本來服務端處理的任務分散到用戶端瀏覽器,能夠很大程度的提升頁面響應速度。先後端分離主要考慮的應該就是跨域的問題了,對於跨域主要考慮如下場景:
動靜分離
動靜分離主要也是對於性能上的優化措施,不一樣人對於動靜分離的理解不同,主要有如下兩種
數據預先處理
對於一些業務場景,能夠提早預處理一些數據,在使用的時候就能夠直接使用處理結果了,減小請求時的處理邏輯。如對於限制某些用戶參與資格,能夠提早將用戶打好標記,這樣在用戶請求時就能夠直接判斷是否有參與資格,若是數據量比較大,還能夠根據必定規則將數據分佈存儲,用戶請求時也根據此規則路由到對應的服務去判斷用戶參與資格,減輕單節點壓力和單服務數據量,提升總體的處理能力和響應速度
資源前置
目前不少都是分佈式微服務架構,就可能會致使調用鏈路很長,所以能夠將一些基本的判斷儘可能前置,好比用戶參與資格、前面提到的限流前置、或者一些資源直接由前端請求到目的地址,而不是經過服務端轉發;涉及機率型的高併發請求,能夠考慮在用戶訪問時即隨機一部分結果,在前端告知用戶參與失敗。總之,就是將能提早的儘可能提早,避免調用鏈路中不符合條件的節點作無用功
補償機制
對於一些業務處理失敗後須要有補償機制,例如:重試、回退等
冪等性
在實際處理中可能會出現各類各樣的狀況致使重複處理,就須要保證處理的冪等性,通常可使用全局惟一的流水號來進行惟一性判斷,避免重複處理的問題,主要是在MQ消息處理、接口調用等場景。全局惟一的流水號能夠參考tweeter的snowflake算法【sequence-spring-boot-starter】。具體生成的位置就須要根據實際業務場景決定了,主要是須要考慮各類極端的異常狀況
監控告警
在高併發系統中,用戶量自己就很大,一旦出現問題影響範圍就會比較大,因此監控告警就須要及時的反饋出系統問題,以便快速恢復服務。必需要創建比較完善的應對流程,建議也能夠創建對應的經驗庫,對常見問題進行記錄,一方面避免重複發生,另外一方面在發生問題時能夠及時定位問題。
自動化運維方面須要大力建設,能夠很大程度提升線上問題的響應和解決速度。而且須要有全鏈路監控機制,能夠更方便的排查線上問題並快速解決。全鏈路監控能夠考慮像pingpoint、zipkin、OpenCensus等
架構獨立服務
項目開發過程當中有些需求是與所在項目業務無關,如:收集用戶行爲習慣,收集商品曝光點擊,數據收集提供給 BI 進行統計報表輸出,公用拉新促活業務(柚子街和返還公用),相似這種需求,咱們結合應用場景,考慮服務的獨立性,以及將來的拓展須要,架構獨立項目進行維護,在服務器上獨立分佈式部署不影響現有主業務服務器資源
項目中實踐例子:
架構用戶行爲跟蹤獨立服務,在開發前預估了下這個服務的請求量,並會有相對大量的併發請求
架構方案:
用戶行爲跟蹤服務的服務架構圖
高併發除了須要對服務器進行垂直擴展和水平擴展以外,做爲後端開發能夠經過高併發優化,保證業務在高併發的時候可以穩定的運行,避免業務停滯帶來的損失,給用戶帶來很差的體驗
服務端緩存
內存數據庫
方式
注意
方式
場景:
服務端緩存架構圖
場景
雖然Redis集羣這種緩存的性能已經很高了,可是也避免不了網絡消耗,在高併發系統中,這些消耗是可能會引發很嚴重後果的,也須要儘可能減小。能夠考慮多級緩存,將一些變動頻率很是低的數據放入應用內緩存,這樣就能夠在應用內直接處理了,相比使用集中式緩存來講,在高併發場景仍是可以提升很大效率的,能夠參考【cache-redis-caffeine-spring-boot-starter】實現兩級緩存,也能夠參考開源中國的J2Cache,支持多種兩級緩存的方式。須要注意的就是緩存失效時一級緩存的清理,由於一級緩存是在應用內,對於集羣部署的系統,應用之間是無法直接通訊的,只能藉助其餘工具來進行通知並清理一級緩存。如利用Redis的發佈訂閱功能來實現同一應用不一樣節點間的通訊
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接口的實現中注意這點
在緩存的使用方面,會有各類各樣複雜的狀況,建議能夠整理一下各類場景並持續完善,這樣能夠在後續使用緩存的過程當中做爲參考,也能夠避免由於考慮不周全引發的異常,對於員工的培養也是頗有好處的
異步編程
方式:
場景:
方式
場景:
注意:
缺陷:
【業務異步處理】架構圖
【業務異步處理】除了能夠在高併發業務中使用,在上面通用服務的設計裏也是用這種架構方式
在類秒殺的活動中經過限制請求量,能夠避免超賣,超領等問題
高併發的活動業務,經過前端控流,分散請求,減小併發量
更多限流方案參看對高併發流量控制的一點思考
服務端限流
應用限流後就決定了只能處理必定量的請求,對於增加期應用來講,通常仍是但願可以處理更多的用戶請求,畢竟意味着帶來更多的用戶、更多的收益。因此就須要監控應用流量,根據實際狀況及時進行擴容,提升整個系統的處理能力,以便爲更多的用戶提供服務
2.用戶體驗
當應用達到限流值時,須要給用戶更好的提示和引導,這也是須要在需求分析階段就須要考慮的
3.限流前置
在實際的系統架構中,用戶請求可能會通過多級纔會到達應用節點,好比:nginx-->gateway-->應用。若是條件容許,能夠在儘可能靠前的位置作限流設置,這樣能夠儘早的給用戶反饋,也能夠減小後續層級的資源浪費。不過畢竟在應用內增長限流配置的開發成本相對來講較低,而且可能會更靈活,因此須要根據團隊實際狀況而定了。nginx作限流設置可使用Lua+Redis配合來實現;應用內限流可使用RateLimiter來作。固然均可以經過封裝來實現動態配置限流的功能,好比【ratelimiter-spring-boot-starter】
當服務器資源消耗已經達到必定的級別的時候,爲了保證核心業務正常運行,須要丟卒保車,棄車保帥,服務降級是最後的手段,避免服務器宕機致使業務停滯帶來的損失,以及給用戶帶來很差的體驗
在微服務架構中,會有不少的接口調用,當某些服務出現調用時間較長或沒法提供服務的時候,就可能會形成請求阻塞,從而致使響應緩慢,吞吐量下降的狀況。這時候就有必要對服務進行降級處理。當超過指定時間或服務不可用的時候,採起備用方案繼續後續流程,避免請求阻塞時間太長。好比對於機率性的請求(如抽獎),當處理時間過長時直接認爲隨機結果是無效的(如未中獎)。須要注意的是
可使用hystrix來實現熔斷降級處理
高併發優化概要圖
大多數公司的產品設計和程序猿對於推廣活動業務的防刷意識不強,在活動業務設計和開發的過程當中沒有把防刷的功能加入業務中,給那些喜歡刷活動的人創造了不少的空子
等到你發現本身被刷的時候,已經產生了不小的損失,少則幾百幾千,多則幾萬
隨着利益的誘惑,如今已經浮現了一個新的職業 「刷客」,專業刷互聯網活動爲生,養了 N 臺手機 + N 個手機號碼 + N 個微信帳號,刷到的獎勵金進行提現,刷到活動商品進行低價轉手處理,開闢了一條新的灰色產業鏈
咱們要拿起武器 (代碼) 進行自個人防護,風控,加高門檻,經過校驗和限制減小風險發生的各類可能性,減小風險發生時形成的損失
這裏列出經常使用套路(具體應用結合業務場景):
校驗請求合法性
業務風控
應對角色
防刷 / 防羊毛黨套路概要圖
附加
多操做
當 == 同用戶 == 屢次觸發點擊,或者經過模擬併發請求,就會出現多操做的問題,好比:簽到功能,一天只能簽到一次,能夠得到 1 積分,可是併發的狀況下會出現用戶能夠得到多積分的問題
簡化簽到邏輯通常是這樣的:
查詢是否有簽到記錄 --> 否 --> 添加今日簽到記錄 --> 累加用戶積分 --> 簽到成功
查詢是否有簽到記錄 --> 是 --> 今日已經簽到過
假設這個時候用戶 A 併發兩個簽到請求,這時會同時進入到 【查詢是否有簽到記錄】,而後同時返回否,就會添加兩條的簽到記錄,而且多累加積分
最理想簡單的方案,只須要在簽到記錄表添加【簽到日期】+【用戶 ID】的組合惟一索引,當併發的時候只有會一條能夠添加成功,其餘添加操做會由於惟一約束而失敗
當 == 多用戶 == 併發點擊參與活動,如:抽獎活動,這個時候獎品只有一個庫存了,理論上只有一個用戶能夠得到,可是併發的時候每每會出現他們都成功得到獎品,致使獎品多支出,加大了活動成本
有問題的邏輯流程通常是這樣的:
中獎 --> 查詢獎品庫存 --> 有 --> 更新獎品庫存 --> 添加中獎紀錄 --> 告知中獎
中獎 --> 查詢獎品庫存 --> 無 --> 告知無中獎
假設抽獎活動,當前獎品 A 只有最後一個庫存,而後用戶 A、B、C,同時參與活動同時中獎獎品都是 A,這個時候查詢商品庫存是存在 1 個,就會進行更新庫存,添加中獎紀錄,而後就同時中獎了
最理想根本就不須要用多作一個庫存的 SELECT 獎品庫存操做,只須要 UPDATE 獎品庫存 - 1 WHERE 獎品庫存 >=1,UPDATE 成功後就說明是有庫存的,而後再作後續操做,併發的時候只會有一個用戶 UPDATE 成功
庫存扣減的實現方式有不少種,並且涉及到扣減庫存的時候還須要結合實際業務場景來決定實現方案,除了扣減庫存,還須要記錄一些業務數據。數據庫在高併發量的應用中很容易遇到瓶頸,因此能夠考慮使用Redis + MQ來作請求的處理,由MQ消費者去實現後續的業務邏輯。這樣可以較快速的響應請求,避免請求阻塞而引起更多的問題
利用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消息發送失敗須要恢復Redis中的庫存,Redis操做和MQ操做沒法徹底保證一致性,因此在保證正常狀況下數據一致性的前提下,還須要相似對帳同樣來驗證扣減庫存和實際庫存的一致性。不過在這以前,我認爲須要更優先考慮限流問題,須要提早壓測出應用的性能瓶頸,根據壓測結果對請求配置限流,優先保證高併發狀況下應用不會崩潰掉,這樣才能更好的保證接收到的請求可以按正常代碼邏輯處理,減小發生庫存不一致的狀況
在開發業務接口的時候須要把 == 同用戶 == 和 == 多用戶 == 併發的場景考慮進去,這樣就能夠避免在併發的時候產生數據異常問題,致使成本多支出
可使用下面的工具進行模擬併發測試:
掃碼關注做者公衆號