高併發&高可用系統的常見應對策略

解耦神器:MQ

MQ是分佈式架構中的解耦神器,應用很是廣泛。有些分佈式事務也是利用MQ來作的。因爲其高吞吐量,在一些業務比較複雜的狀況,能夠先作基本的數據驗證,而後將數據放入MQ,由消費者異步去處理後續的複雜業務邏輯,這樣能夠大大提升請求響應速度,提高用戶體驗。若是消費者業務處理比較複雜,也能夠獨立集羣部署,根據實際處理能力需求部署多個節點。須要注意的是:css

  • 須要確認消息發送MQ成功

好比RabbitMQ在發送消息到MQ時,就有發送回調確認,雖然不可以徹底避免消息丟失,但也可以避免一些極端狀況下消息發送失敗的狀況了。能夠利用MQ的事務來避免更多狀況的消息丟失html

  • 消息持久化

須要注意配置消息持久化,避免MQ集羣掛掉的狀況下大量丟失消息的狀況前端

  • 消息消費的冪等性

正常來講消息是不會重複發送的,可是一些特殊狀況也可能會致使消息重複發送給消費者,通常會在消息中加一個全局惟一的流水號,經過流水號來判斷消息是否已經消費過node

  • 注意用戶體驗

使用異步處理是在提升系統吞吐量考慮下的一種設計,相對於實時快速給用戶返回結果,確定用戶體驗會更差一點,但這也是目前來講綜合考慮的一種不錯的方案了,所以在設計之初就須要評估是否須要異步處理,若是須要異步處理,那必定要考慮如何給用戶更友好的提示和引導。由於異步處理是技術實現結合實際業務狀況的一種綜合解決方案,對於產品來講是不該該關心的,須要技術人員主動儘早提出流程中異步處理的節點,在需求分析階段就考慮如何設計才能對用戶來講更加友好。若是在開發過程當中才提出,極可能就會對用戶展現界面有較大調整,從而致使需求變動、系統設計變動,然後就是甩鍋、扯皮、延期了mysql


項目管理


代碼結構和規範nginx

  • 要注意代碼結構的設計,提升代碼可重用率
  • 嚴格遵照代碼規範,代碼規範能夠下降新成員的理解難度,也能夠下降團隊成員間互相理解的難度
  • 參考:https://my.oschina.net/dengfuwei/blog/1611917

人員管理redis

  • 分工要明確,須要有隨時接收並處理問題的人員
  • 信息透明,團隊成員須要對系統有足夠的瞭解,須要讓團隊成員有獨當一面的能力
  • 知識庫,整理技術上、業務上的常見問題、經驗,方便新成員快速理解並融入
  • 分享,按期分享技術上、業務上的知識,團隊成員共同快速進步。適當的分享系統運行成果,能夠適當鼓舞團隊士氣
  • 適當與業務溝通,瞭解一線業務需求和使用狀況,以便不斷改善,也能夠在系統設計上有更長遠的考慮
  • 適當用一些項目管理工具,適當將一些工做進行量化。不適合團隊的成員也須要及時淘汰

模塊化設計

根據業務場景,將業務抽離成獨立模塊,對外經過接口提供服務,減小系統複雜度和耦合度,實現可複用,易維護,易拓展算法

項目中實踐例子:
Before:spring

在返還購 APP 裏有個【個人紅包】的功能,用戶的紅包數據來自多個業務,如:邀請新用戶註冊領取 100 元紅包,大促活動雙倍紅包,等各類活動紅包,多個活動業務都實現了一套不一樣規則的紅包領取和紅包獎勵發放的機制,致使紅包不可管理,不能複用,難維護難拓展sql

After:

  • 重構紅包業務
  • 紅包可後臺管理
  • 紅包信息管理,可添加,可編輯,可配置紅包使用的規則,可管理用戶紅包
  • 紅包獎勵發放統一處理
  • 應用業務的接入只須要專一給用戶進行紅包發放便可

設計概要


Before VS After

產品有時提出的業務需求沒有往這方面去考慮,結合場景和將來拓展須要,在需求討論的時候提出模塊化設計方案,並能夠協助產品進行設計

通用服務抽離

在項目開發中常常會遇到些相似的功能,可是不一樣的開發人員都各自實現,或者由於不能複用又從新開發一個,致使了相似功能的重複開發,因此咱們須要對可以抽離獨立服務的功能進行抽離,達到複用的效果,而且能夠不斷拓展完善,節約了後續開發成本,提升開發效率,易於維護和拓展

項目中實踐例子:
Before

在業務中常常須要對用戶進行信息通知,如:短信定時通知,APP 消息推送,微信通知,等
開發人員在接到需求中有通知功能的時候沒有考慮後續拓展,就接入第三方信息通知平臺,而後簡單封裝個信息通知方法,後續也有相似信息通知需求的時候,另外一個開發人員發現當前這個通知方法沒法知足本身的需求,而後又本身去了解第三方平臺從新封裝了通知方法,或者後續需求加了定時通知的功能,開發人員針對業務去實現了個定時通知功能,可是隻能本身業務上使用,其餘業務沒法接入,沒有人去作這塊功能的抽離,長此以往就演變成功能重複開發,且不易於維護和拓展


After
接觸到這種能夠抽離通用服務需求的時候,就會與產品確認這種需求是否後續會存在相似的須要,而後建議這把塊需求抽離成通用服務,方便後續維護和拓展

設計概要

Before VS After

Before VS After

架構設計

先後端分離
對於併發量較大的應用,能夠將先後端分離開,這樣對於前端的資源就可使用nginx等效率高的服務器,而且數據是在前端渲染,不是在服務端經過jsp、freemarker等渲染後返回前端。至關於把本來服務端處理的任務分散到用戶端瀏覽器,能夠很大程度的提升頁面響應速度。先後端分離主要考慮的應該就是跨域的問題了,對於跨域主要考慮如下場景:

  • 不跨域,建議使用這種方式。主要實現是將js、css、圖片等靜態資源放到CDN,使用nginx反向代理來區分html(或使用nodejs的服務端)和服務端數據的請求。這樣可以保證前端和後端的請求都在同一個域名之下。這樣作的好處主要是不用考慮跨域的問題,也能夠避免跨域的一些坑,以支持更多的場景(好比RESTful)。採用這種方式的麻煩點主要是涉及到CDN、nginx、應用服務器的配置,因此在版本升級的時候須要有一些自動化的工具來提升效率、避免手動出現一些錯誤,而且要有一個較好的機制來保障版本升級的兼容性,好比CDN中的資源能夠考慮在請求路徑上增長一個版本號(在 動靜分離 中也會提到)
  • 服務端跨域。主要是在服務端配置以支持跨域請求。這種主要須要考慮的是服務端的權限處理,由於跨域默認是不會將訪問的域的cookie傳到服務端的,因此須要其餘方式來傳遞一個請求的標誌,用來控制權限
  • 客戶端跨域。這種方式不太適合業務類型系統,主要適用於一些公開的服務,好比:天氣查詢、手機號歸屬地查詢等等

動靜分離

動靜分離主要也是對於性能上的優化措施,不一樣人對於動靜分離的理解不同,主要有如下兩種

  • 動態數據和靜態資源分離。主要是指將靜態資源(如:js、css、圖片等)放到CDN,這樣能夠提升靜態資源的請求速度,減小應用服務器的帶寬佔用。須要注意的是,由於使用了CDN,當版本升級的時候能夠考慮在靜態資源的訪問路徑上加一個版本號,這樣升級以後能夠避免CDN不刷新的問題,若是是APP應用,能夠避免版本不兼容的問題,因此就須要在部署環節作一些自動化的工具,避免人工操做出現失誤
  • 服務端根據動態資源生成對應的靜態資源,用戶訪問的始終是靜態資源。這種比較常見於CMS(內容管理系統)、博客等類型的應用。主要方式是提早根據動態數據生成對應的靜態資源(即html靜態頁面),這樣用戶訪問的時候就直接訪問html頁面了,能夠較大程度的提升訪問速度。這種方式主要適合數據變化不太頻繁的場景

避免過分設計

  • 避免由於少數極端狀況作過多處理
  • 避免過分拆分微服務,儘可能避免分佈式事務
  • 慎用先後端分離,好比一些內部管理型的使用量不高的應用,是不必作先後端分離的

數據預先處理
對於一些業務場景,能夠提早預處理一些數據,在使用的時候就能夠直接使用處理結果了,減小請求時的處理邏輯。如對於限制某些用戶參與資格,能夠提早將用戶打好標記,這樣在用戶請求時就能夠直接判斷是否有參與資格,若是數據量比較大,還能夠根據必定規則將數據分佈存儲,用戶請求時也根據此規則路由到對應的服務去判斷用戶參與資格,減輕單節點壓力和單服務數據量,提升總體的處理能力和響應速度


資源前置
目前不少都是分佈式微服務架構,就可能會致使調用鏈路很長,所以能夠將一些基本的判斷儘可能前置,好比用戶參與資格、前面提到的限流前置、或者一些資源直接由前端請求到目的地址,而不是經過服務端轉發;涉及機率型的高併發請求,能夠考慮在用戶訪問時即隨機一部分結果,在前端告知用戶參與失敗。總之,就是將能提早的儘可能提早,避免調用鏈路中不符合條件的節點作無用功

補償機制
對於一些業務處理失敗後須要有補償機制,例如:重試、回退等

  • 重試須要限制重試次數,避免死循環,超過次數的須要及時告警,以便人工處理或其餘處理。重試就須要保證冪等性,避免重複處理致使的不一致的問題
  • 回退。當超太重試次數或一些處理失敗後,須要回退的,須要考慮周全一些,避免出現數據不一致的狀況


冪等性
在實際處理中可能會出現各類各樣的狀況致使重複處理,就須要保證處理的冪等性,通常可使用全局惟一的流水號來進行惟一性判斷,避免重複處理的問題,主要是在MQ消息處理、接口調用等場景。全局惟一的流水號能夠參考tweeter的snowflake算法【sequence-spring-boot-starter】。具體生成的位置就須要根據實際業務場景決定了,主要是須要考慮各類極端的異常狀況

監控告警
在高併發系統中,用戶量自己就很大,一旦出現問題影響範圍就會比較大,因此監控告警就須要及時的反饋出系統問題,以便快速恢復服務。必需要創建比較完善的應對流程,建議也能夠創建對應的經驗庫,對常見問題進行記錄,一方面避免重複發生,另外一方面在發生問題時能夠及時定位問題。


自動化運維方面須要大力建設,能夠很大程度提升線上問題的響應和解決速度。而且須要有全鏈路監控機制,能夠更方便的排查線上問題並快速解決。全鏈路監控能夠考慮像pingpoint、zipkin、OpenCensus等

架構獨立服務

項目開發過程當中有些需求是與所在項目業務無關,如:收集用戶行爲習慣,收集商品曝光點擊,數據收集提供給 BI 進行統計報表輸出,公用拉新促活業務(柚子街和返還公用),相似這種需求,咱們結合應用場景,考慮服務的獨立性,以及將來的拓展須要,架構獨立項目進行維護,在服務器上獨立分佈式部署不影響現有主業務服務器資源

項目中實踐例子:
架構用戶行爲跟蹤獨立服務,在開發前預估了下這個服務的請求量,並會有相對大量的併發請求
架構方案:

  • 項目搭建選擇用 nodejs 來作服務端
  • 單進程,基於事件驅動和無阻塞 I/O,因此很是適合處理併發請求
  • 負載均衡:cluster 模塊 / PM2
  • 架構 nodejs 獨立服務
  • 提供服務接口給客戶端
  • 接口不直接 DB 操做,保證併發下的穩定性
  • 數據異步入庫
  • 經過程序把數據從:消息隊列 =>mysql
  • nodejs+express+redis(list)/mq+mysql

用戶行爲跟蹤服務的服務架構圖

高併發優化

高併發除了須要對服務器進行垂直擴展和水平擴展以外,做爲後端開發能夠經過高併發優化,保證業務在高併發的時候可以穩定的運行,避免業務停滯帶來的損失,給用戶帶來很差的體驗

緩存:

服務端緩存
內存數據庫

  • redis
  • memcache

方式

  • 優先緩存
  • 穿透 DB 問題
  • 只讀緩存
  • 更新 / 失效刪除

注意

  • 內存數據庫的分配的內存容量有限,合理規劃使用,濫用最終會致使內存空間不足
  • 緩存數據須要設置過時時間,無效 / 不使用的數據自動過時
  • 壓縮數據緩存數據,不使用字段不添加到緩存中
  • 根據業務拆分佈式部署緩存服務器

客戶端緩存

方式

  • 客戶端請求數據接口,緩存數據和數據版本號,而且每次請求帶上緩存的數據版本號
  • 服務端根據上報的數據版本號與數據當前版本號對比
  • 版本號同樣不返回數據列表,版本號不同返回最新數據和最新版本號

場景:

  • 更新頻率不高的數據

服務端緩存架構圖

場景

  • 多級緩存

雖然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接口的實現中注意這點
在緩存的使用方面,會有各類各樣複雜的狀況,建議能夠整理一下各類場景並持續完善,這樣能夠在後續使用緩存的過程當中做爲參考,也能夠避免由於考慮不周全引發的異常,對於員工的培養也是頗有好處的

異步

異步編程
方式:

  • 多線程編程
  • nodejs 異步編程

場景:

  • 參與活動成功後進行短信通知
  • 非主業務邏輯流程須要的操做,容許異步處理其餘輔助業務,等

業務異步處理

方式

  • 業務接口將客戶端上報的數據 PUSH 到消息隊列(MQ 中間件),而後就響應結果給用戶
  • 編寫獨立程序去訂閱消息隊列,異步處理業務

場景:

  • 大促活動整點搶限量紅包
  • 參與成功後委婉提示:預計 X 天后進行紅包發放
  • 併發量比較大的業務,且沒有其餘更好的優化方案,業務容許異步處理

注意:

  • 把控隊列消耗的進度
  • 保證冪等性和數據最終一致性

缺陷:

  • 犧牲用戶體驗

【業務異步處理】架構圖

【業務異步處理】除了能夠在高併發業務中使用,在上面通用服務的設計裏也是用這種架構方式

限流

在類秒殺的活動中經過限制請求量,能夠避免超賣,超領等問題
高併發的活動業務,經過前端控流,分散請求,減小併發量
更多限流方案參看對高併發流量控制的一點思考


服務端限流

  • redis 計數器
  • 如:類秒殺活動

客戶端控流

  • 經過參與活動遊戲的方式
  • 紅包雨 / 小遊戲,等方式
  1. 監控,及時擴容

應用限流後就決定了只能處理必定量的請求,對於增加期應用來講,通常仍是但願可以處理更多的用戶請求,畢竟意味着帶來更多的用戶、更多的收益。因此就須要監控應用流量,根據實際狀況及時進行擴容,提升整個系統的處理能力,以便爲更多的用戶提供服務

2.用戶體驗

當應用達到限流值時,須要給用戶更好的提示和引導,這也是須要在需求分析階段就須要考慮的

3.限流前置

在實際的系統架構中,用戶請求可能會通過多級纔會到達應用節點,好比:nginx-->gateway-->應用。若是條件容許,能夠在儘可能靠前的位置作限流設置,這樣能夠儘早的給用戶反饋,也能夠減小後續層級的資源浪費。不過畢竟在應用內增長限流配置的開發成本相對來講較低,而且可能會更靈活,因此須要根據團隊實際狀況而定了。nginx作限流設置可使用Lua+Redis配合來實現;應用內限流可使用RateLimiter來作。固然均可以經過封裝來實現動態配置限流的功能,好比【ratelimiter-spring-boot-starter】

服務降級

當服務器資源消耗已經達到必定的級別的時候,爲了保證核心業務正常運行,須要丟卒保車,棄車保帥,服務降級是最後的手段,避免服務器宕機致使業務停滯帶來的損失,以及給用戶帶來很差的體驗

業務降級

  • 從複雜服務,變成簡單服務
  • 從動態交互,變成靜態頁面

分流到 CDN

  • 從 CDN 拉取提早備好的 JSON 數據
  • 引導到 CDN 靜態頁面

中止服務

  • 中止非核心業務,並進行委婉提示

熔斷降級

在微服務架構中,會有不少的接口調用,當某些服務出現調用時間較長或沒法提供服務的時候,就可能會形成請求阻塞,從而致使響應緩慢,吞吐量下降的狀況。這時候就有必要對服務進行降級處理。當超過指定時間或服務不可用的時候,採起備用方案繼續後續流程,避免請求阻塞時間太長。好比對於機率性的請求(如抽獎),當處理時間過長時直接認爲隨機結果是無效的(如未中獎)。須要注意的是

  • 配置熔斷降級的時間須要綜合權衡一下具體配置多少,並且正常狀況下是可以快速響應的,當出現處理時間超時的狀況或服務不可用的狀況,就須要監控及時告警,以便儘快恢復服務
  • 當出現熔斷降級的時候,須要有對應的機制,好比:重試、回退。須要保證業務數據在代碼邏輯上的一致性

可使用hystrix來實現熔斷降級處理

高併發優化概要圖



防刷 / 防羊毛黨

大多數公司的產品設計和程序猿對於推廣活動業務的防刷意識不強,在活動業務設計和開發的過程當中沒有把防刷的功能加入業務中,給那些喜歡刷活動的人創造了不少的空子
等到你發現本身被刷的時候,已經產生了不小的損失,少則幾百幾千,多則幾萬
隨着利益的誘惑,如今已經浮現了一個新的職業 「刷客」,專業刷互聯網活動爲生,養了 N 臺手機 + N 個手機號碼 + N 個微信帳號,刷到的獎勵金進行提現,刷到活動商品進行低價轉手處理,開闢了一條新的灰色產業鏈
咱們要拿起武器 (代碼) 進行自個人防護,風控,加高門檻,經過校驗和限制減小風險發生的各類可能性,減小風險發生時形成的損失
這裏列出經常使用套路(具體應用結合業務場景):

校驗請求合法性

  • 請求參數合法性判斷
  • 請求頭校驗
  • user-agent
  • referer
  • ... ...
  • 簽名校驗
  • 對請求參數進行簽名
  • 設備限制
  • IP 限制
  • 微信 unionid/openid 合法性判斷
  • 驗證碼 / 手機短信驗證碼
  • 犧牲體驗
  • 自建黑名單系統過濾

業務風控

  • 限制設備 / 微信參與次數
  • 限制最多獎勵次數
  • 獎池限制
  • 根據具體業務場景設計... ...

應對角色

  • 普通用戶
  • 技術用戶
  • 專業刷客
  • 目前尚未很好的限制方式

防刷 / 防羊毛黨套路概要圖

 

附加

  • APP/H5 中籤名規則應該由客戶端童鞋開發,而後拓展 API 給前端 JS 調用,在 H5 發起接口請求的時候調用客戶端拓展的簽名,這樣能夠避免前端 JS 裏構造簽名規則而被發現破解

併發問題

多操做

  • 場景:

當 == 同用戶 == 屢次觸發點擊,或者經過模擬併發請求,就會出現多操做的問題,好比:簽到功能,一天只能簽到一次,能夠得到 1 積分,可是併發的狀況下會出現用戶能夠得到多積分的問題

  • 剖析:

簡化簽到邏輯通常是這樣的:
查詢是否有簽到記錄 --> 否 --> 添加今日簽到記錄 --> 累加用戶積分 --> 簽到成功
查詢是否有簽到記錄 --> 是 --> 今日已經簽到過
假設這個時候用戶 A 併發兩個簽到請求,這時會同時進入到 【查詢是否有簽到記錄】,而後同時返回否,就會添加兩條的簽到記錄,而且多累加積分

  • 解決方案:

最理想簡單的方案,只須要在簽到記錄表添加【簽到日期】+【用戶 ID】的組合惟一索引,當併發的時候只有會一條能夠添加成功,其餘添加操做會由於惟一約束而失敗

庫存負數

  • 場景:

當 == 多用戶 == 併發點擊參與活動,如:抽獎活動,這個時候獎品只有一個庫存了,理論上只有一個用戶能夠得到,可是併發的時候每每會出現他們都成功得到獎品,致使獎品多支出,加大了活動成本

  • 剖析:

有問題的邏輯流程通常是這樣的:
中獎 --> 查詢獎品庫存 --> 有 --> 更新獎品庫存 --> 添加中獎紀錄 --> 告知中獎
中獎 --> 查詢獎品庫存 --> 無 --> 告知無中獎
假設抽獎活動,當前獎品 A 只有最後一個庫存,而後用戶 A、B、C,同時參與活動同時中獎獎品都是 A,這個時候查詢商品庫存是存在 1 個,就會進行更新庫存,添加中獎紀錄,而後就同時中獎了

  • 解決方案:

最理想根本就不須要用多作一個庫存的 SELECT 獎品庫存操做,只須要 UPDATE 獎品庫存 - 1 WHERE 獎品庫存 >=1,UPDATE 成功後就說明是有庫存的,而後再作後續操做,併發的時候只會有一個用戶 UPDATE 成功

庫存扣減

庫存扣減的實現方式有不少種,並且涉及到扣減庫存的時候還須要結合實際業務場景來決定實現方案,除了扣減庫存,還須要記錄一些業務數據。數據庫在高併發量的應用中很容易遇到瓶頸,因此能夠考慮使用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操做沒法徹底保證一致性,因此在保證正常狀況下數據一致性的前提下,還須要相似對帳同樣來驗證扣減庫存和實際庫存的一致性。不過在這以前,我認爲須要更優先考慮限流問題,須要提早壓測出應用的性能瓶頸,根據壓測結果對請求配置限流,優先保證高併發狀況下應用不會崩潰掉,這樣才能更好的保證接收到的請求可以按正常代碼邏輯處理,減小發生庫存不一致的狀況

總結:

在開發業務接口的時候須要把 == 同用戶 == 和 == 多用戶 == 併發的場景考慮進去,這樣就能夠避免在併發的時候產生數據異常問題,致使成本多支出
可使用下面的工具進行模擬併發測試:

  • Apache JMeter
  • Charles Advanced Repeat
  • Visual Studio 性能負載

掃碼關注做者公衆號

相關文章
相關標籤/搜索