解耦神器:MQ
MQ是分布式架構(gòu)中的解耦神器,應(yīng)用非常普遍。有些分布式事務(wù)也是利用MQ來做的劈愚。由于其高吞吐量,在一些業(yè)務(wù)比較復(fù)雜的情況闻妓,可以先做基本的數(shù)據(jù)驗證菌羽,然后將數(shù)據(jù)放入MQ,由消費者異步去處理后續(xù)的復(fù)雜業(yè)務(wù)邏輯由缆,這樣可以大大提高請求響應(yīng)速度注祖,提升用戶體驗。如果消費者業(yè)務(wù)處理比較復(fù)雜均唉,也可以獨立集群部署是晨,根據(jù)實際處理能力需求部署多個節(jié)點。需要注意的是:
- 需要確認(rèn)消息發(fā)送MQ成功
比如RabbitMQ在發(fā)送消息到MQ時舔箭,就有發(fā)送回調(diào)確認(rèn)罩缴,雖然不能夠完全避免消息丟失,但也能夠避免一些極端情況下消息發(fā)送失敗的情況了层扶◇镎拢可以利用MQ的事務(wù)來避免更多情況的消息丟失
- 消息持久化
需要注意配置消息持久化,避免MQ集群掛掉的情況下大量丟失消息的情況
- 消息消費的冪等性
正常來說消息是不會重復(fù)發(fā)送的怒医,但是一些特殊情況也可能會導(dǎo)致消息重復(fù)發(fā)送給消費者炉抒,一般會在消息中加一個全局唯一的流水號,通過流水號來判斷消息是否已經(jīng)消費過
- 注意用戶體驗
使用異步處理是在提高系統(tǒng)吞吐量考慮下的一種設(shè)計稚叹,相對于實時快速給用戶返回結(jié)果焰薄,肯定用戶體驗會更差一點,但這也是目前來說綜合考慮的一種不錯的方案了扒袖,因此在設(shè)計之初就需要評估是否需要異步處理塞茅,如果需要異步處理,那一定要考慮如何給用戶更友好的提示和引導(dǎo)季率。因為異步處理是技術(shù)實現(xiàn)結(jié)合實際業(yè)務(wù)情況的一種綜合解決方案野瘦,對于產(chǎn)品來說是不應(yīng)該關(guān)心的,需要技術(shù)人員主動盡早提出流程中異步處理的節(jié)點,在需求分析階段就考慮如何設(shè)計才能對用戶來說更加友好鞭光。如果在開發(fā)過程中才提出吏廉,很可能就會對用戶展示界面有較大調(diào)整,從而導(dǎo)致需求變更惰许、系統(tǒng)設(shè)計變更席覆,而后就是甩鍋、扯皮汹买、延期了
項目管理
代碼結(jié)構(gòu)和規(guī)范
- 要注意代碼結(jié)構(gòu)的設(shè)計佩伤,提高代碼可重用率
- 嚴(yán)格遵守代碼規(guī)范,代碼規(guī)范可以降低新成員的理解難度晦毙,也可以降低團(tuán)隊成員間互相理解的難度
- 參考:https://my.oschina.net/dengfuwei/blog/1611917
人員管理
- 分工要明確生巡,需要有隨時接收并處理問題的人員
- 信息透明,團(tuán)隊成員需要對系統(tǒng)有足夠的了解见妒,需要讓團(tuán)隊成員有獨當(dāng)一面的能力
- 知識庫孤荣,整理技術(shù)上、業(yè)務(wù)上的常見問題徐鹤、經(jīng)驗垃环,方便新成員快速理解并融入
- 分享邀层,定期分享技術(shù)上返敬、業(yè)務(wù)上的知識,團(tuán)隊成員共同快速進(jìn)步寥院。適當(dāng)?shù)姆窒硐到y(tǒng)運(yùn)行成果劲赠,可以適當(dāng)鼓舞團(tuán)隊士氣
- 適當(dāng)與業(yè)務(wù)溝通,了解一線業(yè)務(wù)需求和使用情況秸谢,以便不斷改善凛澎,也可以在系統(tǒng)設(shè)計上有更長遠(yuǎn)的考慮
- 適當(dāng)用一些項目管理工具,適當(dāng)將一些工作進(jìn)行量化估蹄。不適合團(tuán)隊的成員也需要及時淘汰
模塊化設(shè)計
根據(jù)業(yè)務(wù)場景塑煎,將業(yè)務(wù)抽離成獨立模塊,對外通過接口提供服務(wù)臭蚁,減少系統(tǒng)復(fù)雜度和耦合度最铁,實現(xiàn)可復(fù)用,易維護(hù)垮兑,易拓展
項目中實踐例子:
Before:
在返還購 APP 里有個【我的紅包】的功能冷尉,用戶的紅包數(shù)據(jù)來自多個業(yè)務(wù),如:邀請新用戶注冊領(lǐng)取 100 元紅包系枪,大促活動雙倍紅包雀哨,等各種活動紅包,多個活動業(yè)務(wù)都實現(xiàn)了一套不同規(guī)則的紅包領(lǐng)取和紅包獎勵發(fā)放的機(jī)制,導(dǎo)致紅包不可管理雾棺,不能復(fù)用膊夹,難維護(hù)難拓展
After:
重構(gòu)紅包業(yè)務(wù)
紅包可后臺管理
紅包信息管理,可添加捌浩,可編輯割疾,可配置紅包使用的規(guī)則,可管理用戶紅包
紅包獎勵發(fā)放統(tǒng)一處理
應(yīng)用業(yè)務(wù)的接入只需要專注給用戶進(jìn)行紅包發(fā)放即可
設(shè)計概要
Before VS After
產(chǎn)品有時提出的業(yè)務(wù)需求沒有往這方面去考慮嘉栓,結(jié)合場景和未來拓展需要宏榕,在需求討論的時候提出模塊化設(shè)計方案,并可以協(xié)助產(chǎn)品進(jìn)行設(shè)計
通用服務(wù)抽離
在項目開發(fā)中經(jīng)常會遇到些類似的功能侵佃,但是不同的開發(fā)人員都各自實現(xiàn)麻昼,或者因為不能復(fù)用又重新開發(fā)一個,導(dǎo)致了類似功能的重復(fù)開發(fā)馋辈,所以我們需要對能夠抽離獨立服務(wù)的功能進(jìn)行抽離抚芦,達(dá)到復(fù)用的效果,并且可以不斷拓展完善迈螟,節(jié)約了后續(xù)開發(fā)成本叉抡,提高開發(fā)效率,易于維護(hù)和拓展
項目中實踐例子:
Before
在業(yè)務(wù)中經(jīng)常需要對用戶進(jìn)行信息通知答毫,如:短信定時通知褥民,APP 消息推送,微信通知洗搂,等
開發(fā)人員在接到需求中有通知功能的時候沒有考慮后續(xù)拓展消返,就接入第三方信息通知平臺,然后簡單封裝個信息通知方法耘拇,后續(xù)也有類似信息通知需求的時候撵颊,另一個開發(fā)人員發(fā)現(xiàn)當(dāng)前這個通知方法無法滿足自己的需求,然后又自己去了解第三方平臺重新封裝了通知方法惫叛,或者后續(xù)需求加了定時通知的功能倡勇,開發(fā)人員針對業(yè)務(wù)去實現(xiàn)了個定時通知功能,但是只能自己業(yè)務(wù)上使用嘉涌,其他業(yè)務(wù)無法接入妻熊,沒有人去做這塊功能的抽離,久而久之就演變成功能重復(fù)開發(fā)洛心,且不易于維護(hù)和拓展
After
接觸到這種可以抽離通用服務(wù)需求的時候固耘,就會與產(chǎn)品確認(rèn)這種需求是否后續(xù)會存在類似的需要,然后建議這把塊需求抽離成通用服務(wù)词身,方便后續(xù)維護(hù)和拓展
設(shè)計概要
Before VS After
架構(gòu)設(shè)計
前后端分離
對于并發(fā)量較大的應(yīng)用厅目,可以將前后端分離開,這樣對于前端的資源就可以使用nginx等效率高的服務(wù)器,并且數(shù)據(jù)是在前端渲染损敷,不是在服務(wù)端通過jsp葫笼、freemarker等渲染后返回前端。相當(dāng)于把原本服務(wù)端處理的任務(wù)分散到用戶端瀏覽器拗馒,可以很大程度的提高頁面響應(yīng)速度路星。前后端分離主要考慮的應(yīng)該就是跨域的問題了,對于跨域主要考慮以下場景:
- 不跨域诱桂,建議使用這種方式洋丐。主要實現(xiàn)是將js、css挥等、圖片等靜態(tài)資源放到CDN友绝,使用nginx反向代理來區(qū)分html(或使用nodejs的服務(wù)端)和服務(wù)端數(shù)據(jù)的請求度迂。這樣能夠保證前端和后端的請求都在同一個域名之下扮念。這樣做的好處主要是不用考慮跨域的問題,也可以避免跨域的一些坑摔敛,以支持更多的場景(比如RESTful)辞槐。采用這種方式的麻煩點主要是涉及到CDN掷漱、nginx、應(yīng)用服務(wù)器的配置榄檬,所以在版本升級的時候需要有一些自動化的工具來提高效率卜范、避免手動出現(xiàn)一些錯誤,并且要有一個較好的機(jī)制來保障版本升級的兼容性丙号,比如CDN中的資源可以考慮在請求路徑上增加一個版本號(在 動靜分離 中也會提到)
- 服務(wù)端跨域先朦。主要是在服務(wù)端配置以支持跨域請求。這種主要需要考慮的是服務(wù)端的權(quán)限處理犬缨,因為跨域默認(rèn)是不會將訪問的域的cookie傳到服務(wù)端的,所以需要其他方式來傳遞一個請求的標(biāo)志棉浸,用來控制權(quán)限
- 客戶端跨域怀薛。這種方式不太適合業(yè)務(wù)類型系統(tǒng),主要適用于一些公開的服務(wù)迷郑,比如:天氣查詢枝恋、手機(jī)號歸屬地查詢等等
動靜分離
動靜分離主要也是對于性能上的優(yōu)化措施,不同人對于動靜分離的理解不一樣嗡害,主要有以下兩種
- 動態(tài)數(shù)據(jù)和靜態(tài)資源分離焚碌。主要是指將靜態(tài)資源(如:js、css霸妹、圖片等)放到CDN十电,這樣可以提高靜態(tài)資源的請求速度,減少應(yīng)用服務(wù)器的帶寬占用。需要注意的是鹃骂,因為使用了CDN台盯,當(dāng)版本升級的時候可以考慮在靜態(tài)資源的訪問路徑上加一個版本號,這樣升級之后可以避免CDN不刷新的問題畏线,如果是APP應(yīng)用静盅,可以避免版本不兼容的問題,所以就需要在部署環(huán)節(jié)做一些自動化的工具寝殴,避免人工操作出現(xiàn)失誤
- 服務(wù)端根據(jù)動態(tài)資源生成對應(yīng)的靜態(tài)資源蒿叠,用戶訪問的始終是靜態(tài)資源。這種比較常見于CMS(內(nèi)容管理系統(tǒng))蚣常、博客等類型的應(yīng)用栈虚。主要方式是提前根據(jù)動態(tài)數(shù)據(jù)生成對應(yīng)的靜態(tài)資源(即html靜態(tài)頁面),這樣用戶訪問的時候就直接訪問html頁面了史隆,可以較大程度的提高訪問速度魂务。這種方式主要適合數(shù)據(jù)變化不太頻繁的場景
避免過度設(shè)計
避免因為少數(shù)極端情況做過多處理
避免過度拆分微服務(wù),盡量避免分布式事務(wù)
慎用前后端分離泌射,比如一些內(nèi)部管理型的使用量不高的應(yīng)用粘姜,是沒必要做前后端分離的
數(shù)據(jù)預(yù)先處理
對于一些業(yè)務(wù)場景,可以提前預(yù)處理一些數(shù)據(jù)熔酷,在使用的時候就可以直接使用處理結(jié)果了孤紧,減少請求時的處理邏輯。如對于限制某些用戶參與資格拒秘,可以提前將用戶打好標(biāo)記号显,這樣在用戶請求時就可以直接判斷是否有參與資格,如果數(shù)據(jù)量比較大躺酒,還可以根據(jù)一定規(guī)則將數(shù)據(jù)分布存儲押蚤,用戶請求時也根據(jù)此規(guī)則路由到對應(yīng)的服務(wù)去判斷用戶參與資格,減輕單節(jié)點壓力和單服務(wù)數(shù)據(jù)量羹应,提高整體的處理能力和響應(yīng)速度
資源前置
目前很多都是分布式微服務(wù)架構(gòu)揽碘,就可能會導(dǎo)致調(diào)用鏈路很長,因此可以將一些基本的判斷盡量前置园匹,比如用戶參與資格雳刺、前面提到的限流前置、或者一些資源直接由前端請求到目的地址裸违,而不是通過服務(wù)端轉(zhuǎn)發(fā)掖桦;涉及概率型的高并發(fā)請求,可以考慮在用戶訪問時即隨機(jī)一部分結(jié)果供汛,在前端告知用戶參與失敗枪汪∮磕拢總之,就是將能提前的盡量提前料饥,避免調(diào)用鏈路中不符合條件的節(jié)點做無用功
補(bǔ)償機(jī)制
對于一些業(yè)務(wù)處理失敗后需要有補(bǔ)償機(jī)制蒲犬,例如:重試、回退等
- 重試需要限制重試次數(shù)岸啡,避免死循環(huán)原叮,超過次數(shù)的需要及時告警,以便人工處理或其他處理巡蘸。重試就需要保證冪等性奋隶,避免重復(fù)處理導(dǎo)致的不一致的問題
- 回退。當(dāng)超過重試次數(shù)或一些處理失敗后悦荒,需要回退的唯欣,需要考慮周全一些,避免出現(xiàn)數(shù)據(jù)不一致的情況
冪等性
在實際處理中可能會出現(xiàn)各種各樣的情況導(dǎo)致重復(fù)處理搬味,就需要保證處理的冪等性境氢,一般可以使用全局唯一的流水號來進(jìn)行唯一性判斷,避免重復(fù)處理的問題碰纬,主要是在MQ消息處理萍聊、接口調(diào)用等場景。全局唯一的流水號可以參考tweeter的snowflake算法【sequence-spring-boot-starter】悦析。具體生成的位置就需要根據(jù)實際業(yè)務(wù)場景決定了寿桨,主要是需要考慮各種極端的異常情況
監(jiān)控告警
在高并發(fā)系統(tǒng)中,用戶量本身就很大强戴,一旦出現(xiàn)問題影響范圍就會比較大亭螟,所以監(jiān)控告警就需要及時的反饋出系統(tǒng)問題,以便快速恢復(fù)服務(wù)骑歹。必須要建立比較完善的應(yīng)對流程预烙,建議也可以建立對應(yīng)的經(jīng)驗庫,對常見問題進(jìn)行記錄陵刹,一方面避免重復(fù)發(fā)生默伍,另一方面在發(fā)生問題時可以及時定位問題。
自動化運(yùn)維方面需要大力建設(shè)衰琐,可以很大程度提高線上問題的響應(yīng)和解決速度。并且需要有全鏈路監(jiān)控機(jī)制炼蹦,可以更方便的排查線上問題并快速解決羡宙。全鏈路監(jiān)控可以考慮像pingpoint、zipkin掐隐、OpenCensus等
架構(gòu)獨立服務(wù)
項目開發(fā)過程中有些需求是與所在項目業(yè)務(wù)無關(guān)狗热,如:收集用戶行為習(xí)慣钞馁,收集商品曝光點擊,數(shù)據(jù)收集提供給 BI 進(jìn)行統(tǒng)計報表輸出匿刮,公用拉新促活業(yè)務(wù)(柚子街和返還公用)僧凰,類似這種需求,我們結(jié)合應(yīng)用場景熟丸,考慮服務(wù)的獨立性训措,以及未來的拓展需要,架構(gòu)獨立項目進(jìn)行維護(hù)光羞,在服務(wù)器上獨立分布式部署不影響現(xiàn)有主業(yè)務(wù)服務(wù)器資源
項目中實踐例子:
架構(gòu)用戶行為跟蹤獨立服務(wù)绩鸣,在開發(fā)前預(yù)估了下這個服務(wù)的請求量,并會有相對大量的并發(fā)請求
架構(gòu)方案:
項目搭建選擇用 nodejs 來做服務(wù)端
單進(jìn)程纱兑,基于事件驅(qū)動和無阻塞 I/O呀闻,所以非常適合處理并發(fā)請求
負(fù)載均衡:cluster 模塊 / PM2
架構(gòu) nodejs 獨立服務(wù)
提供服務(wù)接口給客戶端
接口不直接 DB 操作,保證并發(fā)下的穩(wěn)定性
數(shù)據(jù)異步入庫
通過程序把數(shù)據(jù)從:消息隊列 =>mysql
nodejs+express+redis(list)/mq+mysql
用戶行為跟蹤服務(wù)的服務(wù)架構(gòu)圖
高并發(fā)優(yōu)化
高并發(fā)除了需要對服務(wù)器進(jìn)行垂直擴(kuò)展和水平擴(kuò)展之外潜慎,作為后端開發(fā)可以通過高并發(fā)優(yōu)化捡多,保證業(yè)務(wù)在高并發(fā)的時候能夠穩(wěn)定的運(yùn)行,避免業(yè)務(wù)停滯帶來的損失铐炫,給用戶帶來不好的體驗
緩存:
服務(wù)端緩存
內(nèi)存數(shù)據(jù)庫
- redis
- memcache
方式
- 優(yōu)先緩存
- 穿透 DB 問題
- 只讀緩存
- 更新 / 失效刪除
注意
- 內(nèi)存數(shù)據(jù)庫的分配的內(nèi)存容量有限垒手,合理規(guī)劃使用,濫用最終會導(dǎo)致內(nèi)存空間不足
- 緩存數(shù)據(jù)需要設(shè)置過期時間驳遵,無效 / 不使用的數(shù)據(jù)自動過期
- 壓縮數(shù)據(jù)緩存數(shù)據(jù)淫奔,不使用字段不添加到緩存中
- 根據(jù)業(yè)務(wù)拆分布式部署緩存服務(wù)器
客戶端緩存
方式
- 客戶端請求數(shù)據(jù)接口,緩存數(shù)據(jù)和數(shù)據(jù)版本號堤结,并且每次請求帶上緩存的數(shù)據(jù)版本號
- 服務(wù)端根據(jù)上報的數(shù)據(jù)版本號與數(shù)據(jù)當(dāng)前版本號對比
- 版本號一樣不返回數(shù)據(jù)列表唆迁,版本號不一樣返回最新數(shù)據(jù)和最新版本號
場景:
- 更新頻率不高的數(shù)據(jù)
服務(wù)端緩存架構(gòu)圖
場景
- 多級緩存
雖然Redis集群這種緩存的性能已經(jīng)很高了,但是也避免不了網(wǎng)絡(luò)消耗竞穷,在高并發(fā)系統(tǒng)中唐责,這些消耗是可能會引起很嚴(yán)重后果的,也需要盡量減少瘾带∈蟾纾可以考慮多級緩存,將一些變更頻率非常低的數(shù)據(jù)放入應(yīng)用內(nèi)緩存看政,這樣就可以在應(yīng)用內(nèi)直接處理了朴恳,相比使用集中式緩存來說,在高并發(fā)場景還是能夠提高很大效率的允蚣,可以參考【cache-redis-caffeine-spring-boot-starter】實現(xiàn)兩級緩存于颖,也可以參考開源中國的J2Cache,支持多種兩級緩存的方式嚷兔。需要注意的就是緩存失效時一級緩存的清理森渐,因為一級緩存是在應(yīng)用內(nèi)做入,對于集群部署的系統(tǒng),應(yīng)用之間是沒法直接通信的同衣,只能借助其他工具來進(jìn)行通知并清理一級緩存竟块。如利用Redis的發(fā)布訂閱功能來實現(xiàn)同一應(yīng)用不同節(jié)點間的通信
- CDN
CDN也是一種緩存,只是主要適用于一些靜態(tài)資源耐齐,比如:css浪秘、js、png圖片等蚪缀,前端會使用的較多秫逝。在一些場景下,可以結(jié)合動靜分離询枚、前后端分離违帆,將前端資源全部放入CDN中,能夠很大程度提高訪問效率金蜀。需要注意的是前端靜態(tài)資源是可能會更新的刷后,當(dāng)有更新的時候需要刷新CDN緩存≡ǔ或者另一種策略是在靜態(tài)資源的地址上增加一個類似版本號的標(biāo)志尝胆,這樣每次修改后的路徑就會不一樣,上線后CDN就會直接回源到自己應(yīng)用內(nèi)獲取最新的文件并緩存在CDN中护桦。使用CDN就需要一套比較完善的自動化部署的工具了含衔,不然每次修改后上線就會比較麻煩
- 前端緩存
前端html中可以配置靜態(tài)資源在前端的緩存,配置后瀏覽器會緩存一些資源二庵,當(dāng)用戶刷新頁面時贪染,只要不是強(qiáng)制刷新,就可以不用再通過網(wǎng)絡(luò)請求獲取靜態(tài)資源催享,也能夠一定程度提高頁面的響應(yīng)速度
- 緩存穿透
當(dāng)使用緩存的時候杭隙,如果緩存中查詢不到數(shù)據(jù),就會回源到數(shù)據(jù)庫中查詢因妙。但是如果某些數(shù)據(jù)在數(shù)據(jù)庫中也沒有痰憎,如果不做處理,那么每次請求都會回源到數(shù)據(jù)庫查詢數(shù)據(jù)攀涵。如果有人惡意利用這種不存在的數(shù)據(jù)大量請求系統(tǒng)铣耘,那么就會導(dǎo)致大量請求到數(shù)據(jù)庫中執(zhí)行查詢操作。這種情況就叫做緩存穿透以故。在高并發(fā)場景下更需要防止這種情況的發(fā)生
防止:如果數(shù)據(jù)庫中查詢不到數(shù)據(jù)涡拘,可以往緩存里放一個指定的值,從緩存中取值時先判斷一下据德,如果是這個指定的值就直接返回空鳄乏,這樣就可以都從緩存中獲取數(shù)據(jù)了,從而避免緩存穿透的問題棘利。也可以根據(jù)緩存對象的實際情況橱野,采用兩級緩存的方式,這樣也可以減少緩存設(shè)備的請求量善玫。redis是常用的緩存水援,但是不能存儲null,因此spring cache模塊中定義了一個NullValue對象茅郎,用來代表空值蜗元。spring boot中Redis方式實現(xiàn)spring cache是有一些缺陷的(spring boot 1.5.x版本),具體參考[https://my.oschina.net/dengfuwei/blog/1616221]中提到的#RedisCache實現(xiàn)中的缺陷#
- 緩存雪崩
緩存雪崩主要是指由于緩存原因系冗,大量請求到達(dá)了數(shù)據(jù)庫奕扣,導(dǎo)致數(shù)據(jù)庫壓力過大而崩潰。除了上面提到的緩存穿透的原因掌敬,還有可能是緩存過期的瞬間有大量的請求需要處理惯豆,從緩存中判斷無數(shù)據(jù),然后就直接查詢數(shù)據(jù)庫了奔害。這也是在高并發(fā)場景下比較容易出現(xiàn)的問題
防止:當(dāng)緩存過期時楷兽,回源到數(shù)據(jù)庫查詢的時候需要做下處理,如:加互斥鎖华临。這樣就能夠避免在某個時間點有大量請求到達(dá)數(shù)據(jù)庫了芯杀,當(dāng)然也可以對方法級別做限流處理,比如:hystrix雅潭、RateLimiter揭厚。也可以通過封裝實現(xiàn)緩存在過期前的某個時間點自動刷新緩存。spring cache的注解中有一個sync屬性寻馏,主要是用來表示回源到數(shù)據(jù)查詢時是否需要保持同步棋弥,由于spring cache只是定義標(biāo)準(zhǔn),沒有具體緩存實現(xiàn)诚欠,所以只是根據(jù)sync的值調(diào)用了不同的Cache接口的方法顽染,所以需要在Cache接口的實現(xiàn)中注意這點
在緩存的使用方面,會有各種各樣復(fù)雜的情況轰绵,建議可以整理一下各種場景并持續(xù)完善粉寞,這樣可以在后續(xù)使用緩存的過程中作為參考,也可以避免因為考慮不周全引起的異常左腔,對于員工的培養(yǎng)也是很有好處的
異步
異步編程
方式:
- 多線程編程
- nodejs 異步編程
場景:
- 參與活動成功后進(jìn)行短信通知
- 非主業(yè)務(wù)邏輯流程需要的操作唧垦,允許異步處理其他輔助業(yè)務(wù),等
業(yè)務(wù)異步處理
方式
- 業(yè)務(wù)接口將客戶端上報的數(shù)據(jù) PUSH 到消息隊列(MQ 中間件)液样,然后就響應(yīng)結(jié)果給用戶
- 編寫?yīng)毩⒊绦蛉ビ嗛喯㈥犃姓窳粒惒教幚順I(yè)務(wù)
場景:
大促活動整點搶限量紅包
參與成功后委婉提示:預(yù)計 X 天后進(jìn)行紅包發(fā)放
并發(fā)量比較大的業(yè)務(wù)巧还,且沒有其他更好的優(yōu)化方案,業(yè)務(wù)允許異步處理
注意:
- 把控隊列消耗的進(jìn)度
- 保證冪等性和數(shù)據(jù)最終一致性
缺陷:
- 犧牲用戶體驗
【業(yè)務(wù)異步處理】架構(gòu)圖
【業(yè)務(wù)異步處理】除了可以在高并發(fā)業(yè)務(wù)中使用坊秸,在上面通用服務(wù)的設(shè)計里也是用這種架構(gòu)方式
限流
在類秒殺的活動中通過限制請求量麸祷,可以避免超賣,超領(lǐng)等問題
高并發(fā)的活動業(yè)務(wù)褒搔,通過前端控流阶牍,分散請求,減少并發(fā)量
更多限流方案參看對高并發(fā)流量控制的一點思考
服務(wù)端限流
- redis 計數(shù)器
- 如:類秒殺活動
客戶端控流
通過參與活動游戲的方式
紅包雨 / 小游戲星瘾,等方式
- 監(jiān)控走孽,及時擴(kuò)容
應(yīng)用限流后就決定了只能處理一定量的請求,對于增長期應(yīng)用來說琳状,一般還是希望能夠處理更多的用戶請求磕瓷,畢竟意味著帶來更多的用戶、更多的收益算撮。所以就需要監(jiān)控應(yīng)用流量生宛,根據(jù)實際情況及時進(jìn)行擴(kuò)容,提高整個系統(tǒng)的處理能力肮柜,以便為更多的用戶提供服務(wù)
- 用戶體驗
當(dāng)應(yīng)用達(dá)到限流值時陷舅,需要給用戶更好的提示和引導(dǎo),這也是需要在需求分析階段就需要考慮的
- 限流前置
在實際的系統(tǒng)架構(gòu)中审洞,用戶請求可能會經(jīng)過多級才會到達(dá)應(yīng)用節(jié)點莱睁,比如:nginx-->gateway-->應(yīng)用。如果條件允許芒澜,可以在盡量靠前的位置做限流設(shè)置仰剿,這樣可以盡早的給用戶反饋,也可以減少后續(xù)層級的資源浪費痴晦。不過畢竟在應(yīng)用內(nèi)增加限流配置的開發(fā)成本相對來說較低南吮,并且可能會更靈活,所以需要根據(jù)團(tuán)隊實際情況而定了誊酌。nginx做限流設(shè)置可以使用Lua+Redis配合來實現(xiàn)部凑;應(yīng)用內(nèi)限流可以使用RateLimiter來做。當(dāng)然都可以通過封裝來實現(xiàn)動態(tài)配置限流的功能碧浊,比如【ratelimiter-spring-boot-starter】
服務(wù)降級
當(dāng)服務(wù)器資源消耗已經(jīng)達(dá)到一定的級別的時候涂邀,為了保證核心業(yè)務(wù)正常運(yùn)行,需要丟卒保車箱锐,棄車保帥比勉,服務(wù)降級是最后的手段,避免服務(wù)器宕機(jī)導(dǎo)致業(yè)務(wù)停滯帶來的損失,以及給用戶帶來不好的體驗
業(yè)務(wù)降級
- 從復(fù)雜服務(wù)浩聋,變成簡單服務(wù)
- 從動態(tài)交互观蜗,變成靜態(tài)頁面
分流到 CDN
- 從 CDN 拉取提前備好的 JSON 數(shù)據(jù)
- 引導(dǎo)到 CDN 靜態(tài)頁面
停止服務(wù)
- 停止非核心業(yè)務(wù),并進(jìn)行委婉提示
熔斷降級
在微服務(wù)架構(gòu)中赡勘,會有很多的接口調(diào)用嫂便,當(dāng)某些服務(wù)出現(xiàn)調(diào)用時間較長或無法提供服務(wù)的時候,就可能會造成請求阻塞闸与,從而導(dǎo)致響應(yīng)緩慢,吞吐量降低的情況岸售。這時候就有必要對服務(wù)進(jìn)行降級處理践樱。當(dāng)超過指定時間或服務(wù)不可用的時候,采取備用方案繼續(xù)后續(xù)流程凸丸,避免請求阻塞時間太長拷邢。比如對于概率性的請求(如抽獎),當(dāng)處理時間過長時直接認(rèn)為隨機(jī)結(jié)果是無效的(如未中獎)屎慢。需要注意的是
- 配置熔斷降級的時間需要綜合權(quán)衡一下具體配置多少瞭稼,而且正常情況下是能夠快速響應(yīng)的,當(dāng)出現(xiàn)處理時間超時的情況或服務(wù)不可用的情況腻惠,就需要監(jiān)控及時告警环肘,以便盡快恢復(fù)服務(wù)
- 當(dāng)出現(xiàn)熔斷降級的時候,需要有對應(yīng)的機(jī)制集灌,比如:重試悔雹、回退。需要保證業(yè)務(wù)數(shù)據(jù)在代碼邏輯上的一致性
可以使用hystrix來實現(xiàn)熔斷降級處理
高并發(fā)優(yōu)化概要圖
防刷 / 防羊毛黨
大多數(shù)公司的產(chǎn)品設(shè)計和程序猿對于推廣活動業(yè)務(wù)的防刷意識不強(qiáng)欣喧,在活動業(yè)務(wù)設(shè)計和開發(fā)的過程中沒有把防刷的功能加入業(yè)務(wù)中腌零,給那些喜歡刷活動的人創(chuàng)造了很多的空子
等到你發(fā)現(xiàn)自己被刷的時候,已經(jīng)產(chǎn)生了不小的損失唆阿,少則幾百幾千益涧,多則幾萬
隨著利益的誘惑,現(xiàn)在已經(jīng)浮現(xiàn)了一個新的職業(yè) “刷客”驯鳖,專業(yè)刷互聯(lián)網(wǎng)活動為生闲询,養(yǎng)了 N 臺手機(jī) + N 個手機(jī)號碼 + N 個微信賬號,刷到的獎勵金進(jìn)行提現(xiàn)臼隔,刷到活動商品進(jìn)行低價轉(zhuǎn)手處理嘹裂,開辟了一條新的灰色產(chǎn)業(yè)鏈
我們要拿起武器 (代碼) 進(jìn)行自我的防御,風(fēng)控摔握,加高門檻寄狼,通過校驗和限制減少風(fēng)險發(fā)生的各種可能性,減少風(fēng)險發(fā)生時造成的損失
這里列出常用套路(具體應(yīng)用結(jié)合業(yè)務(wù)場景):
校驗請求合法性
請求參數(shù)合法性判斷
請求頭校驗
user-agent
referer
... ...
簽名校驗
對請求參數(shù)進(jìn)行簽名
設(shè)備限制
IP 限制
微信 unionid/openid 合法性判斷
驗證碼 / 手機(jī)短信驗證碼
犧牲體驗
自建黑名單系統(tǒng)過濾
業(yè)務(wù)風(fēng)控
- 限制設(shè)備 / 微信參與次數(shù)
- 限制最多獎勵次數(shù)
- 獎池限制
- 根據(jù)具體業(yè)務(wù)場景設(shè)計... ...
應(yīng)對角色
普通用戶
技術(shù)用戶
專業(yè)刷客
目前還沒有很好的限制方式
防刷 / 防羊毛黨套路概要圖
附加
- APP/H5 中簽名規(guī)則應(yīng)該由客戶端童鞋開發(fā),然后拓展 API 給前端 JS 調(diào)用泊愧,在 H5 發(fā)起接口請求的時候調(diào)用客戶端拓展的簽名伊磺,這樣可以避免前端 JS 里構(gòu)造簽名規(guī)則而被發(fā)現(xiàn)破解
并發(fā)問題
多操作
- 場景:
當(dāng) == 同用戶 == 多次觸發(fā)點擊,或者通過模擬并發(fā)請求删咱,就會出現(xiàn)多操作的問題屑埋,比如:簽到功能,一天只能簽到一次痰滋,可以獲得 1 積分摘能,但是并發(fā)的情況下會出現(xiàn)用戶可以獲得多積分的問題
- 剖析:
簡化簽到邏輯一般是這樣的:
查詢是否有簽到記錄 --> 否 --> 添加今日簽到記錄 --> 累加用戶積分 --> 簽到成功
查詢是否有簽到記錄 --> 是 --> 今日已經(jīng)簽到過
假設(shè)這個時候用戶 A 并發(fā)兩個簽到請求,這時會同時進(jìn)入到 【查詢是否有簽到記錄】敲街,然后同時返回否团搞,就會添加兩條的簽到記錄,并且多累加積分
- 解決方案:
最理想簡單的方案多艇,只需要在簽到記錄表添加【簽到日期】+【用戶 ID】的組合唯一索引逻恐,當(dāng)并發(fā)的時候只有會一條可以添加成功,其他添加操作會因為唯一約束而失敗
庫存負(fù)數(shù)
- 場景:
當(dāng) == 多用戶 == 并發(fā)點擊參與活動峻黍,如:抽獎活動复隆,這個時候獎品只有一個庫存了,理論上只有一個用戶可以獲得姆涩,但是并發(fā)的時候往往會出現(xiàn)他們都成功獲得獎品挽拂,導(dǎo)致獎品多支出,加大了活動成本
- 剖析:
有問題的邏輯流程一般是這樣的:
中獎 --> 查詢獎品庫存 --> 有 --> 更新獎品庫存 --> 添加中獎紀(jì)錄 --> 告知中獎
中獎 --> 查詢獎品庫存 --> 無 --> 告知無中獎
假設(shè)抽獎活動阵面,當(dāng)前獎品 A 只有最后一個庫存轻局,然后用戶 A、B样刷、C仑扑,同時參與活動同時中獎獎品都是 A,這個時候查詢商品庫存是存在 1 個置鼻,就會進(jìn)行更新庫存镇饮,添加中獎紀(jì)錄,然后就同時中獎了
- 解決方案:
最理想根本就不需要用多做一個庫存的 SELECT 獎品庫存操作箕母,只需要 UPDATE 獎品庫存 - 1 WHERE 獎品庫存 >=1储藐,UPDATE 成功后就說明是有庫存的,然后再做后續(xù)操作嘶是,并發(fā)的時候只會有一個用戶 UPDATE 成功
庫存扣減
庫存扣減的實現(xiàn)方式有很多種钙勃,而且涉及到扣減庫存的時候還需要結(jié)合實際業(yè)務(wù)場景來決定實現(xiàn)方案,除了扣減庫存聂喇,還需要記錄一些業(yè)務(wù)數(shù)據(jù)辖源。數(shù)據(jù)庫在高并發(fā)量的應(yīng)用中很容易遇到瓶頸蔚携,所以可以考慮使用Redis + MQ來做請求的處理,由MQ消費者去實現(xiàn)后續(xù)的業(yè)務(wù)邏輯克饶。這樣能夠較快速的響應(yīng)請求酝蜒,避免請求阻塞而引發(fā)更多的問題
- 使用Redis來做庫存扣減
利用Redis中的incr命令來實現(xiàn)庫存扣減的操作。Redis從2.6.0版本開始內(nèi)置了Lua解釋器矾湃,并且對Lua腳本的執(zhí)行是具有原子性的亡脑,所以可以利用此特性來做庫存的扣減,具體實現(xiàn)可以參考【stock-spring-boot-starter】邀跃,starter中主要實現(xiàn)了初始化/重置庫存霉咨、扣減庫存、恢復(fù)庫存
Redis集群的效率已經(jīng)非常高了坞嘀,能夠支撐一定量的并發(fā)扣減庫存躯护,并且由于Redis執(zhí)行Lua腳本的原子性可以避免超扣的問題。如果一個Redis集群還滿足不了業(yè)務(wù)需要丽涩,可以考慮將庫存進(jìn)行拆分。即將庫存拆成多份裁蚁,分別放到不同的Redis集群當(dāng)中矢渊,多個Redis集群采用輪詢策略,基本能夠在大體上保證各個Redis集群的剩余庫存量不會相差太大枉证。不過也不能絕對的保證數(shù)量均勻矮男,所以在扣減庫存操作返回庫存不足時,還是需要一定的策略去解決這個問題室谚,比如扣減庫存返回庫存不足時毡鉴,繼續(xù)輪詢到下一個Redis集群,當(dāng)所有Redis集群都返回庫存不足時秒赤,可以在應(yīng)用節(jié)點內(nèi)或某個統(tǒng)一的地方打個標(biāo)記表示已沒有庫存猪瞬,避免每個請求都輪詢?nèi)康腞edis集群。
- 扣減庫存的冪等性
由于利用Redis的incr命令來扣減庫存入篮,沒法存儲請求源的信息陈瘦,所以扣減庫存的冪等性由應(yīng)用來保證,可以利用客戶端token或流水號之類的來做
- MQ異步處理業(yè)務(wù)數(shù)據(jù)
扣減庫存都會伴隨一些業(yè)務(wù)數(shù)據(jù)需要記錄潮售,如果實時記錄到數(shù)據(jù)庫痊项,仍然很容易達(dá)到瓶頸,所以可以利用MQ酥诽,將相關(guān)信息放入MQ鞍泉,然后由MQ消費者去異步處理后續(xù)的業(yè)務(wù)邏輯。當(dāng)然如果MQ消息發(fā)送失敗需要恢復(fù)Redis中的庫存肮帐,Redis操作和MQ操作無法完全保證一致性咖驮,所以在保證正常情況下數(shù)據(jù)一致性的前提下,還需要類似對賬一樣來驗證扣減庫存和實際庫存的一致性。不過在這之前游沿,我認(rèn)為需要更優(yōu)先考慮限流問題饰抒,需要提前壓測出應(yīng)用的性能瓶頸,根據(jù)壓測結(jié)果對請求配置限流诀黍,優(yōu)先保證高并發(fā)情況下應(yīng)用不會崩潰掉袋坑,這樣才能更好的保證接收到的請求能夠按正常代碼邏輯處理,減少發(fā)生庫存不一致的情況
總結(jié):
在開發(fā)業(yè)務(wù)接口的時候需要把 == 同用戶 == 和 == 多用戶 == 并發(fā)的場景考慮進(jìn)去眯勾,這樣就可以避免在并發(fā)的時候產(chǎn)生數(shù)據(jù)異常問題枣宫,導(dǎo)致成本多支出
可以使用下面的工具進(jìn)行模擬并發(fā)測試:
- Apache JMeter
- Charles Advanced Repeat
- Visual Studio 性能負(fù)載