大型網站架構常用解決方案

每個大型網站都是由小變大的,在變大的過程中,幾乎都需要經歷單機架構、集羣架構到分佈式架構的演變。而伴隨着業務系統架構一同演變的,還有各種外圍系統和存儲系統,比如關係型數據庫的分庫分表改造、從本地緩存到分佈式緩存的過渡等。

在業務架構逐漸複雜的同時,保證系統的高性能、高可用、易擴展、可伸縮,使框架能有效地滿足業務需要,是一個長遠而艱鉅的任務。本文介紹了五種相關的技術:分佈式服務化架構、大流量的限流和削峯、分佈式配置管理服務、熱點數據的讀寫優化和數據庫的分庫分表。

值得注意的是,技術並不是越複雜越好,技術是爲了更好地服務業務,只要能達到業務的需求,就是好的技術。簡單說就是,即使你有實現複雜技術的能力,沒有用戶量和利潤爲基礎,也難以落地實施。所以雖然下文中提到了一些框架,但是並不是每一種框架都需要你去親自實踐。很多時候,只是給你提供一個新的思路,一種新的方法,而至於是不是值得被實踐,還需要得到業務和用戶的考驗。


分佈式服務化架構

集羣和分佈式

首先介紹一下集羣和分佈式,這裏有很多人不知道它們的區別,我找了網上的一張圖片,非常形象地闡釋了單機,集羣和分佈式的區別:

一言以蔽之,分佈式是多個系統完成一個任務以縮短單個任務的執行時間,而集羣是多個系統分攤相同的任務以提高單位時間內執行的任務數。

詳細來說,集羣就是當一臺服務器的處理能力接近或已超出其容量上限時,與其更換一臺性能更強勁的服務器,通過增加新的服務器來分散併發訪問流量更加有效。這叫做服務器的橫向擴容,可以實現可伸縮性和高可用性架構。而分佈式則拆分了系統,即業務垂直化,就是根據系統業務功能拆分出多個業務模塊,分而治之,獨立部署,由此可以降低業務邏輯之間的耦合性。

下面舉兩個例子,方便更好的理解:

  1. 集羣用例:當用戶量增大時,尤其是用戶跨網絡,跨地域訪問網站時,可以將系統中的一些靜態資源數據,如圖片、HTML網頁等,緩存在CDN節點上,這樣用戶的請求會直接被距離用戶最近的ISP處理,從而大幅提升系統整體響應的速度。
  2. 分佈式用例:一般的大型電商網站都會拆分出首頁、用戶、搜索、廣告、購物、訂單、商品、收益結算等子系統,由不同的團隊分別負責開發,部署。

值得注意的是,分佈式和集羣並不是相互對立的兩種系統架構,而是解決問題的不同思路,甚至大部分情況下,還可以結合起來,共築一個高性能、高可用的軟件系統。例如,分佈式將系統拆分成了若干業務模塊,而每個業務模塊都對應着集羣來進行響應和處理。所以分佈式可以看做集羣之上的系統架構。

服務化架構,微服務和RPC

雖然將業務系統以功能爲維度拆分出多個子系統,可以更清晰地規劃和體現出每個子系統的職責,降低系統業務的耦合,但是系統中一定會存在較多的共享業務,這些共享業務被重複建設,產生冗餘代碼。而且數據庫連接等底層資源必然會限制業務系統所允許橫向擴展的節點的數量。爲了解決諸等問題,產生了服務化架構(Service Oriented Architecture, SOA)改造。服務化可以說是分佈式的更高層演化。

業務系統實施服務化改造後,原本共享的業務被拆分,形成可複用的服務,可以在最大程度上避免共享業務的重複建設、資源連接瓶頸等問題的出現。

而最近特別火的微服務,實際上也是服務化架構,只不過是細化了服務拆分過程中的粒度。

之前介紹了什麼是服務化,但是說到底服務化只是一個抽象的概念,而實現服務調用的關鍵,就是RPC。

RPC由客戶端(服務調用方)和服務端(服務提供方)兩部分構成,與在同一個進程空間內執行本地方法調用相比,RPC調用需要服務調用方根據服務提供方提供的方法參數,以網絡的形式遠程調用指定的服務方法,執行完成後再將執行結果響應給服務調用方。

一次RPC調用主要需要經歷三個步驟:

  1. 底層的網絡通信協議處理;
  2. 解決尋址問題;
  3. 請求/響應過程中參數的序列化和反序列化工作。

這裏的底層網絡通信協議可以採用TCP;尋址問題可以通過使用服務註冊中心來解決;而序列化,可以採用JSON/XML等文本格式,也可以採用二進制的協議,如protobuf,thrift。

雖然RPC協議屏蔽了底層複雜的細節處理,但是隨着服務規模的擴大,使用一個RPC框架能帶來很多開發和管理上的收益。目前比較流行的RPC框架有阿里的Dubbo,還有Motan,JsonRPC等,這裏對其具體的使用方法不再贅述。如果想要了解的話,可以參考:

使用RPC時需要注意的一點是,防止因超時和重試引起的系統雪崩。一般的策略是RPC調用失敗則自動failover到其他服務節點上,重試兩次,服務調用超時就意味着失敗,需要進行重試。但是如果超時時間設置地不合理,小於服務的實際執行時間,或由於網絡抖動導致服務超時,在大流量的場景下,由於failover引起蝴蝶效應,請求會變爲正常請求的三倍,影響到後端存儲系統,導致資源連接被耗盡,從而引發系統出現雪崩。

服務化架構的組成

瞭解了服務化之後,我們來看看服務化架構的組成。

服務化架構的核心就是RPC,所以服務化架構的組成中也包含了RPC,就是服務的提供方和調用方,即Provider和Consumer。另外,基於RPC尋址的需求,還需要一個地方存儲Provider的信息,便於Consumer調用,這就是註冊中心,即Registry。另外,鑑於分佈式系統的複雜性和狀態多樣性,我們需要對Provider、Consumer、Registry進行管理,比如服務依賴管理、權限管理、配置管理、版本管理、流量控制、服務上下線等,那麼還需要一個管理端,即Administrator。

所以綜上所述,一個完整的分佈式服務化架構,應該包含4部分:

  1. 註冊中心,Registry
  2. 服務提供端,Provider
  3. 服務消費端,Consumer
  4. 管理端,Administrator

其中,Provider將自己能夠提供的服務信息登記到註冊中心,同時通過心跳的方式定時更新自己的狀態。Consumer從Registry獲取可用服務信息後,直接與Provider建立連接進行交互。

另外,一些基於REST的服務框架(比如Spring Cloud)會增加服務路由的組件,也就是說Consumer需要經過服務路由才能請求Provider,這種方式下,其架構就是由5部分組成:

  1. 註冊中心,Registry
  2. 服務提供端,Provider
  3. 服務消費端,Consumer
  4. 管理端,Administrator
  5. 服務路由,Router

REST – Representational State Transfer,全稱是 Resource Representational State Transfer,通俗來講就是:資源在網絡中以某種表現形式進行狀態轉移。分解開來,Resource:資源,即數據;Representational:某種表現形式,比如JSON,XML,JPEG等;State Transfer:狀態變化,通過HTTP動詞實現。簡單說就是用URL定位資源,用HTTP描述操作。也可以描述爲看Url就知道要什麼,看http method就知道幹什麼,看http status code就知道結果如何。

服務的橫向拆分

之前提到的SOA服務化拆分,也叫做垂直拆分,不同的業務和功能被拆分到不同的服務中。但是當業務規模繼續上升,個別服務出現了存儲的瓶頸,於是需要進行存儲的橫向拆分設計,下面這張圖片表示了服務橫向拆分的四個階段,其中計算就是指我們的業務代碼,存儲指數據庫或者緩存:

  1. 存儲沒有達到存儲容量以及性能的瓶頸,仍舊是單實例。
  2. 存儲容量到達了瓶頸,或者單庫的TPS超過了極限,由或者緩存會達到性能極限:
    • 對於數據庫來說,可以按照某個業務維度拆庫拆表,擴容數據庫實例來承載更多的TPS。
    • 對於緩存來說,可以像數據庫一樣擴容更多的實例,並通過業務維度實現數據打散。
    • 無論如何,這個階段計算層需要根據業務維度路由,找到數據所在的存儲節點。
  3. 通過一個大中間件來解決所有的存儲層擴展性問題。
    • 對於數據庫,增加一層db proxy作爲代理,幫計算層透明的完成數據路由,同時也實現數據庫連接的複用。
    • 對於緩存層拋棄多實例部署方式,而是選擇一款分佈式緩存,比如redis clusters。
  4. 拋棄中間件,通過給每個存儲分片分配獨立的計算層,實現故障隔離。騰訊給這個架構方式起名叫做:set化,用業界專業術語叫做:bulkheads隔艙模式。

雖然說set方案隔離性最好,但是實施成本和改造成本都比較高,可以僅僅對核心業務做了set隔離,來儘量減小故障損失的影響面。

服務治理方案

服務拆分帶來了很多的好處,但是相應的也帶來了很多問題:

  • 拆得越細,系統越複雜;
  • 系統之間的依賴關係也更復雜;
  • 運維複雜度提升;
  • 監控更加複雜;
  • 出問題時定位問題更難。

基於以上問題,建立一個分佈式調用跟蹤系統就顯得非常重要。現在的分佈式調用跟蹤系統大都脫胎於Google的論文 Dapper, A Large Scale Distributed Systems Tracing Infrastructure.

論文中提到了分佈式調用跟蹤系統的四個關鍵設計目標:

  1. 服務性能低損耗
  2. 業務代碼低侵入
  3. 監控界面可視化
  4. 數據分析準實時

想了解分佈式調用跟蹤系統的具體實現過程,可以去看那篇論文,這裏就不再展開了。但是需要注意的是,如果將採集到的所有數據信息都直接寫入數據庫中將給數據庫造成較大的負載壓力,因此可以將信息優先寫入消息隊列中,當消費端消費到信息後,再寫入數據庫,以達到削峯的效果。並且底層存儲除了使用關係型數據庫,還可以嘗試使用HBase等NoSQL數據庫來替代。而且,由於監控數據的時效性較高,長期保存的意義不大,所以定期清理數據庫中的歷史數據也是非常必要的。最後,如果跟蹤系統對核心業務的性能影響比較大,那麼我們可以考慮關停它,因爲跟蹤系統所帶來的收益一定要大於損耗服務性能的缺陷,在更一般的情況下可以結合採樣率在最大程度上控制損耗。

總結

以上介紹了分佈式服務系統的架構。在此還需再強調一下,如果用戶規模以及業務需求的複雜度還沒有到量,那麼最後保持現有架構不變,畢竟構建一個高性能、高可用、易擴展、可伸縮的分佈式系統絕非一件簡單的事情,需要解決的技術難題太多。而且如果業務沒有起色,一昧的追求大型網站架構並無任何意義。

這裏更多是給你提供了一個解決問題的方案,你是否會遇到這些問題,以及是否會選擇這個解決方案,都不是現在能確定了的。


大流量的限流和削峯

分佈式系統爲什麼要進行流量管制

大型互聯網電商網站的主要技術挑戰來自於龐大的用戶規模帶來的大流量和高併發,如果不對流量進行和合理管制,肆意放任大流量衝擊系統,那麼將導致一系列的問題出現,比如一些可用的連接資源被耗盡、分佈式緩存的容量被撐爆、數據庫吞吐量降低,最終必然會導致系統產生雪崩效應。

雖然限流可能會影響用戶的體驗,但是犧牲一點個人時間換來整體的井然有序是值得的。需要明確的是,流量管制的目的是保護系統,讓系統的負載處於一個比較均衡的水位,而不是刻意得爲了限流而限流,這樣造成的用戶體驗的缺失毫無意義。

一般來說,大型互聯網站通常採用的做法是通過擴容、動靜分離、緩存、服務降級及限流五種常規手段來保護系統的穩定運行。下面簡單介紹一下這五種常規手段:

  • 擴容:當一臺服務器的處理能力接近或已超出其容量上限時,採用集羣技術對服務器進行擴容。
  • 動靜分離:將動態數據的靜態數據分而治之,用戶對靜態數據的訪問在CDN中獲取,避免請求直接落到企業的數據中心。
  • 緩存:緩存的讀寫效率遠勝於任何關係型數據庫,合理使用緩存技術,系統可以應對大流量、高併發下的熱點數據的讀寫問題。
  • 服務降級:當系統容量支撐核心業務都捉襟見肘時,犧牲部分功能換來系統的核心服務不受影響是非常有必要的。
  • 限流:寫服務很難通過緩存和服務降級來優化,需要採用合理且有效的限流手段對系統做好保護。

合理地運用以上五種常規手段,可以使用戶流量像漏斗模型一樣逐層減少,讓流量始終保持在系統可處理的容量範圍之內:

限流方案

  1. 計數器算法
    • 池化資源技術(數據庫連接池、線程池、對象池等)。確保在併發環境下連接數不會超過資源閾值。
  2. 令牌桶算法
    • 以均勻的速度向桶中放入令牌,限制流量的平均流入速率,並且可以允許出現一定程度上的突發流量。
  3. 漏桶算法
    • 以固定的速度從桶中流出流量,限制流量的流出速率,不允許出現突發流量。

以下介紹幾種可選的限流實現方案

  • Guava.RateLimiter 實現基於令牌桶算法的平均速率限流。
  • Nginx 實現接入層限流。
  • 使用計數器算法實現限流:Redis集中限流/本地限流

削峯方案

之前介紹過了如何通過技術手段進行流量管制,其實也可以在業務上進行調整,對峯值流量進行分散處理,即削峯。避免在同一時間段內產生較大的用戶流量衝擊系統,從而降低系統的負載壓力。

針對削峯的策略,可將削峯方案分爲基於時間分片的削峯方案,和基於異步調用的削峯方案。

基於時間分片的削峯方案

對於基於時間分片的削峯方案,以下提供兩種可選的削峯方案:

  1. 活動分時段進行實現削峯
    • 將整點的促銷活動調整到多個時段進行,這樣同一時間聚集的用戶流量將會被有效分散,大大降低系統的負載壓力。
  2. 通過答題驗證實現削峯
    • 在用戶下單前增加答題驗證環節,那麼峯值的下單請求必然會被拉長,並且靠後的請求會因爲沒有庫存而無法順利下單,因此同一時間對系統進行併發寫的流量將會非常有限。

基於異步調用的削峯方案

對於基於異步調用的削峯方案,異步調用是指通過創建線程來實現方法的異步調用,將程序中原本的串行化執行流程變爲併發/並行執行。一般情況下,在分佈式環境下解決系統之間耦合以及大流量削峯的手段,是使用消息中間件來實現異步調用,即MQ消息隊列。

一般而言,使用MQ進行流量削峯的經典場景,是控制併發寫流量從而降低後端存儲系統的負載壓力。比如數據庫或者分佈式緩存系統,如果併發寫的流量過大,容量容易瞬間撐爆導致資源連接耗盡等悲劇發生,所以需要一種削峯方案來對併發寫流量進行排隊處理。

通過使用MQ,我們可以先將消息寫入消息隊列中,若使用PULL模式,可以由消費者按照自己的處理能力獲取消息來進行寫操作;若使用PUSH模式,可以控制消費者的數量在合理的範圍之內。

除了使用MQ來實現削峯之外,還可以使用MQ來實現系統之間的解耦。在某些情況下,不同的業務子系統之間的服務調用,即RPC,不一定是必需的,這時可以使用消息傳遞來替代RPC調用。例如,註冊賬號時調用郵件系統發送賬號激活郵件,對於用戶系統而言,這個調用並不是必需的,可以將消息寫入消息隊列中,待消費者消費後,異步完成激活郵件的發送。

總之,那些非必需的依賴,都可以通過消息傳遞來進行替代,從而保證進程功能的單一性。

下面介紹幾個MQ的框架:

  1. ActiveMQ
    • 遵循JMS規範
  2. RocketMQ(分佈式消息中間件)
    • 不遵循JMS規範,支持順序消息,事務消息,支持PULL和PUSH。

JMS規範:由JMS Provider、Provider和Consumer三個角色構成。其中JMS Provider負責消息路由和消息傳遞,Provider負責向消息隊列寫入消息,Consumer負責訂閱消息。JMS的消息模型有兩種:Point-to-Point(P2P,點對點)PULL模型,Publish/Subscribe(pub/sub,發佈/訂閱)PUSH模型。

並不是任何情況下都適合使用MQ來進行系統之間的解耦和流量削峯等操作,對於需要同步等待調用結果的業務場景而言,使用異步化必然會對業務流程造成嚴重影響,甚至還會影響用戶體驗。


分佈式配置管理服務

在實際的開發過程中,有很多地方都需要用到配置信息,在大部分情況下,我們會選擇將相關配置信息配置在配置文件中,但是在集羣中,維護每一個節點的配置文件十分困難。因此需要一種集中式資源配置的形式,以讓所有的集羣節點共享一分配置信息。這就是適用於大規模分佈式場景的集中式資源配置。除此之外,在某些特殊的業務場景下,我們希望配置信息是可以在運行時發送變更的。

整理一下集中式資源管理平臺有一下四個優點:

  1. 配置信息統一管理;
  2. 動態獲取/更新配置信息;
  3. 降低運維人員的維護成本;
  4. 降低配置出錯率。

其實簡單來看,分佈式配置管理服務就是典型的發佈/訂閱模式,獲取配置信息的一方爲訂閱方,發佈配置信息的一方爲推送方。

實現分佈式配置管理服務的經典框架有ZooKeeper,Dubbo就是基於此實現註冊中心來進行服務的動態註冊和發現。除此之外,ZooKeeper還提供了配置管理、分佈式協調/通知、分佈式鎖及統一命名等服務。但是Zookeeper並沒有實現配置信息管理頁面和客戶端的容災機制,還可以選用Diamond和Disconf來提供分佈式配置管理服務。


熱點數據的讀寫優化

雖然我們可以將熱點數據緩存在分佈式緩存中,但是緩存系統的單點容量還是存在上限的。除此之外,由於熱點數據的寫操作無法直接在緩存中完成,因此併發寫引起的InnoDB行鎖的競爭可能會引發系統出現雪崩。下文中針對熱點數據的讀寫優化提出了一些解決的思路和方案。

緩存技術

首先先介紹一下緩存技術。簡而言之,緩存指的是將被頻繁訪問的熱點數據存儲在距離計算最近的地方,以方便系統快速做出響應。例如,靜態數據可以緩存到CDN上,也可以緩存在代理服務器上,從數據庫等存儲系統中獲取的數據信息也可以進行緩存。

針對開源本地緩存,可以試用Ehcache。但是應用程序本身的緩存就很緊張,而本地緩存是同一個進程內的緩存技術,如果緩存數據所佔的內存比例比較大,肯定會影響應用程序的運行。所以在實際的開發過程中,更多采用分佈式緩存,但是分佈式緩存在特殊的應用場景下可能會存在單點瓶頸,所以一個很好的方案是將本地緩存與分佈式緩存結合。常用的高性能分佈式緩存有Redis和Memcached。

下面針對熱點數據的訪問,緩存可以在其中起到的優化作用,進行一下介紹。主要有熱賣商品的高併發讀和高併發寫。

熱賣商品的高併發讀

分佈式緩存的原理是不同的Key落到不同的緩存節點上,但是對於限時搶購的熱賣商品來說,這時同一個Key必然會落到同一個緩存節點上,因此分佈式緩存在這種情況下一定會出現單點瓶頸。

針對分佈式緩存可能存在的單點瓶頸,以下提出了兩種解決方案:

  1. 基於Redis集羣的多寫多讀方案;
  2. 本地緩存結合Redis集羣的多級Cache方案。

基於Redis集羣的多寫多讀方案

默認情況下一個熱賣商品只有一個Key,但是我們可以給它指定N個Key,在N個緩存節點上實現了冗餘存儲,這樣在併發環境下,客戶端可以針對這些Key以輪詢或隨機等方式實現數據訪問,從而降低單個節點的負載壓力。

此方案有兩個問題,一是在多寫情況下如何保證數據的一致性;二是由於N個Key都是提前準備好的,而不是Redis計算生成的,所以每次Redis集羣發生變更時,所有的Key都需要重新計算。

針對如何保障多寫時數據的一致性,有以下的解決方案:

使用Zookeeper來配置統一熱賣商品的Key,當一個節點的數據發生修改時,全量更新所有的Key,這樣一旦在某一個節點處寫入失敗,就可以直接從Zookeeper中移除該Key,從而避免數據出現髒讀。當失敗的節點正常後,再將其Key加到Zookeeper中即可。

本地緩存結合Redis集羣的多級Cache方案

根據二八定律,限時搶購場景下的讀操作比例一定會遠遠大於寫操作比例。而本地緩存又和進程共享內存空間,所以可以將訪問熱度不高的商品存在分佈式緩存,而本地緩存中存儲訪問熱度較高的熱賣商品。對於商品數據而言,需要配置本地緩存的更新策略,對於那些靜態資源,圖片、視頻等都緩存在CDN上,而本地緩存只需要緩存商品詳情和商品庫存,並定時對分佈式緩存進行輪詢,更新本地緩存。這樣雖然本地緩存和分佈式緩存會有一定時間窗口下的數據不一致,但是對於讀操作來說,我們允許在一定程度上出現髒讀,等到最終扣減庫存的時候再提示用戶已售空即可。

在這裏要注意的是,如果爲本地緩存設置了TTL策略,那麼當本地緩存的TTL過期,而用戶流量又過大時,大量請求無法在本地緩存命中,會對分佈式緩存頻繁訪問,導致程序的吞吐量下降。

另外由於本地緩存是通過輪詢的方式從分佈式緩存中拉取最新數據的,所以會存在兩個緩存不一致的窗口期,如果想要縮短這個窗口期,可以引入消息隊列,當分佈式緩存中的商品數據修改後,再把消息寫入到消息隊列中,所有訂閱了此Topic的本地緩存可以消費到推送的商品數據。但是這一方案設計複雜,並且增加了外圍系統宕機的風險。

實時熱點自動發現方案

以上方案都是基於熱點Key已經確定了的情況,那麼如何確定熱點Key呢。有一些熱點Key可以在活動開始前就提前分析出來,但是那些沒有被發現並突然成爲熱點的數據,以及被熱點數據瞬間帶起來的流量就成了漏網之魚。所以對於這種在運行時突然形成的熱點,我們需要引入一種實時熱點自動發現機制來進行熱點保護。

具體的實施方案爲,在上游系統中對相關數據進行埋點上報並異步寫入到日誌系統中,然後通過實時熱點自動發現平臺對收集到的日誌數據做調用次數統計和熱點分析,一旦數據符合熱點條件,就立即通知系統做好熱點保護。

熱賣商品的高併發寫

針對熱點數據,除了高併發的讀需求,併發扣減同一熱賣商品庫存的寫需求是一件更加棘手的事情。

因爲商品的真實庫存需要存儲在關係型數據庫中,但是大量的併發更新熱點數據都是針對同一行的,若是Mysql,這一操作必然會引起大量的線程競爭InnoDB的行鎖,嚴重影響數據庫的TPS,導致RT上升,最終可能引發系統出現雪崩。

爲了避免數據庫淪爲瓶頸,我們可以將熱賣商品庫存的扣減操作轉移至關係型數據外或者合理控制併發寫的流量。針對如何解決熱點數據的併發寫問題,以下給出了幾種解決方案。

關係型數據庫避免超賣

直接在關係型數據庫中扣減庫存時,如何避免商品超賣呢?可以使用樂觀鎖來避免這個問題。出於性能上的考慮,我們不建議在查詢操作中加排它鎖,即for update,而是增加一個version字段,當一個用戶成功扣減庫存後,需要將version加一,這樣第二個用戶扣減庫存時由於version不匹配,需要進行重試。

除了使用樂觀鎖,我們還可以使用"實際庫存數≥扣減庫存數"作爲條件來替代version匹配,例如:

update item set stock=stock-1 where item_id = 1 and stock > 1;

但是這並沒有解決線程競爭InnoDB行鎖時所引起的一系列問題,所以下面給出一些在關係型數據庫外扣減庫存的方案。

在Redis中扣減熱賣商品庫存

由於Redis的讀/寫能力遠勝過任何關係型數據庫,所以在Redis中實現庫存扣減是一個很不錯的替代方案。這樣在Redis中存儲的商品庫存爲實時庫存,在數據庫中存儲的庫存爲實際庫存。

使用Redis實現扣減庫存時,需要引入分佈式鎖來避免超賣。分佈式鎖自身需要滿足以下三點要求:

  1. 在任何情況下分佈式鎖都不能淪爲系統瓶頸;
  2. 不能產生死鎖;
  3. 支持鎖重入。

至於分佈式鎖的實現方式,常見的有基於Zookeeper和Redis實現的分佈式鎖,這裏不再贅述。

當系統獲取到分佈式鎖併成功扣減Redis中的實時庫存後,可以將消息寫入到消息隊列中,由消費者負責實際庫存的扣減。

爲了不使分佈式鎖淪爲系統瓶頸,可以使用tryLock而是不lock來獲取分佈式鎖,儘管這會影響商品庫存的扣減成功率。

熱賣商品庫存扣減優化方案

以上爲了解決商品超賣,無論是在數據庫中扣減庫存,還是在Redis中扣減庫存,都必須依賴於串行化和鎖機制。爲了減少鎖的獲取次數,可以使用批量提交扣減庫存的方式。

簡單說就是,先對前端發起的庫存扣減請求進行收集,達到閾值後再對這些請求做合併處理,獲取到鎖後就一次性進行庫存扣減,將串行化操作變成批處理操作,大大提升了系統整體的TPS。

也可以直接使用AliSQL數據庫來提升秒殺場景的性能,AliSQL針對秒殺場景做了特殊的優化。


數據庫的分庫分表

重要的業務數據,都是需要落盤到關係型數據庫中的,如何提升關係型數據庫的並行能力和檢索效率就成了架構設計的關鍵問題。下面首先介紹一個關係型數據庫架構上的演變。

關係型數據庫的架構演變

關係型數據庫常見的性能瓶頸主要有兩個:

  1. 大量的併發讀/寫操作,導致單庫出現難以承受的負載壓力。
  2. 單表存儲數據量過大,導致檢索效率低下。

爲了提高關係型數據庫的性能,關係型數據庫的架構演化趨勢爲:讀寫分離->垂直分庫->水平分庫分表,下面我分別介紹一下這幾種架構。

讀寫分離:根據二八法則,80%的數據庫操作都是讀操作,因此讀寫分離可以大大降低單庫的負載壓力。一般採用一主多從的形式,由Master負責寫操作,而Salve作爲備庫只開放讀操作,主從直接數據保持同步。需要注意的是,如果Master存在TPS比較高的情況,Master與Salve數據庫之間的數據同步是會存在一定延遲的,因此在寫入Master之前最好將同一份數據落到緩存中,以避免高併發情況下,從Salve中獲取不到指定數據的情況發生。

垂直分庫:以關係型數據庫MySQL爲例,當單表數據超過500萬行時,讀操作就會成爲瓶頸,而寫操作由於是順序寫則不會有影響。因此可以根據自身業務的垂直劃分,將單庫中的數據表拆分到不同的業務庫中,實現分而治之的數據管理和讀/寫操作。

水平分庫分表:解決關係型數據庫應對高併發、單表數據量過大的最終解決方案就是水平分庫分表。水平分表就是將單庫中的單個業務表拆分成n個邏輯相關的業務子表,不同的業務子表各自負責存儲不同區間的數據,對外形成一個整體,這也是常說的Sharding。水平分表後的業務子表可以包含在單庫中,如果單庫TPS過高,也可以對單庫進行水平化,將業務子表分散到n個邏輯相關的業務子庫中。

Sharding中間件

以上水平分庫分表將單個業務表拆分成了多個數據庫中的多張業務表,需要考慮兩個問題。一是明確Shard Key(路由條件),路由條件決定了數據的落盤位置;二是根據所定義的Shard Key進行數據路由,還需要定義一套特定的路由算法和規則。

例如,將1024個表均勻分佈在32個數據庫中,每個數據庫中有32個表,這樣根據Shard Key獲取數據庫和數據表的路由算法爲:

db=shardKey%1024/32
tb=shardKey%1024%32

一般情況下,我們使用成熟的Sharding中間件來完成數據的路由工作。Sharding中間件的架構主要有基於Proxy的架構和應用集成架構兩種,基於Proxy的架構的Sharding中間件更加靈活,而應用集成架構的Sharding中間件讀/寫性能更高。常用的Proxy Sharding中間件有Cobar,MyCat,應用集成中間件有Shark,這裏就其使用方法不再贅述。

分庫分錶帶來的影響

雖然分庫分表能很大程度上提高數據庫的性能,但是也帶來了一些問題,主要體現在邏輯代碼的實現上。下面簡述了幾個常見的問題,並附帶了解決問題的思路和方法。

  1. 多表之間的關聯查詢和外鍵約束無法保證

在大型的互聯網企業,一旦數據庫分庫分表之後,對於SQL語句的編寫都傾向於簡單化、輕量化,而將複雜的邏輯運算上移到應用層,避免數據庫成爲系統的瓶頸。而外鍵約束也要儘可能得避免使用,使數據庫的職能更加的單一,不進行額外的計算操作。所以多表聯合查詢都會被拆分成多條單表查詢語句,而單表查詢語句也有一些優勢,查詢語句簡單,易於理解、維護和擴展;緩存利用率高等。

  1. 無法使用數據庫自帶的方案生成全局唯一的ID

無論是Oracle的Sequence,或是MySQL的AUTO_INCREMENT,這類生成唯一ID的方式都是面向單點的,而在分庫分表的架構下,我們需要一個多機的SequenceID解決方案。可以使用Java的UUID,但是它和格式爲8-4-4-4-12,作爲一個ID來說太長了。還可以考慮一個獨立的外圍單點系統來負責生成一個兼顧唯一性和連續性ID。有些Sharding中間件也提供了生成SequenceID的API,例如Shark。

  1. 多維度的複雜條件查詢

由於數據以什麼樣的維度分表,就會以什麼樣的維度落盤,最後也只能通過這種維度進行查詢,想要實現滿足多維度的複雜條件的查詢需求就很難。可以使用Solr來完成多維度的複雜條件的查詢,同時它比直接使用like進行模糊查詢的效率也要高很多。

  1. 分佈式事務

在分佈式的環境下,本地事務已經無法保證數據的一致性了,但是引入分佈式事務所帶來的問題往往很複雜。常見的分佈式系統中的一致性協議有:兩階段提交協議、三階段提交協議、Paxos協議。這裏不建議使用分佈式事務,如果一定要保證一致性,也不要刻意去追求強一致性,刻意考慮採用基於消息中間件保證數據最終一致性的方案。

數據庫的HA方案

HA在廣義上是指系統所具備的高可用性。無論是數據庫主從模式,還是應用程序集羣,都不會因爲單一節點的故障而影響系統整體服務的不可用。但是數據庫搭建HA,還需要一種機制能保證主從的正常切換,目前有三種成熟的主從切換方案:

  1. 基於配置中心實現主從切換;
    • 監控系統告警後,運維人員手動修改配置中心的數據源信息。
  2. 基於Keepalived實現主從切換;
    • Master和Slave上的Keepalived程序會相互發送心跳信號,Master故障後Slave檢測不到心跳就會接管寫入請求。
  3. 基於MHA實現主從切換。
    • 通過保存二進制日誌,最大程度的保證數據的不丟失。

訂單業務冗餘表需求

針對訂單業務,有買家ID和賣家ID,分庫分表時如果只是以其中一個維度進行數據落盤,那麼最終能夠查詢出訂單數據的只是賣家或買家中的一方。針對這種特殊的業務需求,常見的做法是對同一份訂單數據進行冗餘存儲,即同時維護賣家訂單表和買家訂單表。

冗餘表的實現方式有數據同步寫入和數據異步寫入兩種,一般情況下考慮到系統的TPS,都是採用數據異步寫入方案,並結合實際的訂單業務,優先將數據寫入買家表中。

爲了保障冗餘表的數據一致性,可以使用分佈式事務或最終一致性方案。這裏分佈式事務具有複雜性和低效性,故此只介紹最終一致性方案的具體實施:

在訂單寫入買家表後,將消息寫入消息隊列中,寫入買家表後也將消息寫入消息隊列中,這樣當消費者消費到第一條消息的指定時間內沒有消費到第二條消息,就可以認爲數據出現了不一致,需要執行數據補償操作。但是也有可能是網絡原因導致了第二條消息的延遲,因此在數據補償前需要優先執行冪等操作。而至於數據補償,可以使用線上補償,循環對比買家表和賣家表;也可以使用線下補償,增量地對比兩個表的日誌。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章