一文搞懂後臺高性能服務器設計的常見套路, BAT 高頻面試系列

微信搜索🔍「編程指北」,關注這個寫乾貨的程序員,回覆「資源」,便可獲取後臺開發學習路線和書籍

前言

金九銀十,又是一年校招季。前端

經歷過,才深知不易。最近,和做爲校招面試官的同事聊了聊,問他們是如何去考察一個學生的,我簡單歸爲如下幾點:程序員

  1. 聰明、反應快,這點自沒必要說,聰明意味着學習能力、適應力強,可以快速勝任工做。
  2. 算法不錯,代碼基本功好,這點其實考察的是算法能力和代碼是否寫得優雅。
  3. 基礎過硬,技術崗面試最核心的仍是考察「技術儲備」,包括了語言基本功,操做系統、網絡、體系結構、系統設計。
  4. 語言組織和表達能力,這點很重要,不少同窗懂得某個知識點,卻很難用簡潔準確的語言表述出來。

想必有不少同窗在刷題、刷面經,不過我想說「面經雖好,不要貪杯哦~」,面經能夠刷,看看面試官都是怎麼提問的,但不要寄但願於原題。
由於面試過程當中的問題每每是一環扣一環的,這意味着你須要有足夠的技術深度,將知識由點鏈接成面,而不是停留在相互孤立的知識點上。面試

因此仍是建議系統性的看書,若是以爲時間不夠,能夠關注書裏的重點章節。至於看哪些書?後面也會列一個個人書單和閱讀建議。在【編程指北】後臺回覆【書單】便可獲取算法

那麼回到技術面試上,除了算法和網絡、操做系統這種基礎以外,還有一類系統設計和優化的問題。這類問題須要你有一個全局的技術視野,以及熟悉一些經常使用的系統優化方法論,也就是工程上的一些 Best Practice,而不至於本身臨時拍腦殼瞎設計。數據庫

在互聯網公司,常常面臨一個「三高」問題:編程

  • 高併發
  • 高性能
  • 高可用

這篇文章將總結一下後臺服務器開發中有哪些經常使用的解決「三高」問題的方法和思想。後端

但願這些知識,可以給你一絲啓發和幫助,助力你收割 各大公司 Offer~緩存

先上本文思惟導圖:安全

如何解決三高

正文

1、緩存

什麼是緩存?看看維基百科怎麼說:服務器

In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.

在計算機中,緩存是存儲數據的硬件或軟件組件,以即可以更快地知足未來對該數據的請求。 存儲在緩存中的數據多是以前計算結果,也多是存儲在其餘位置的數據副本

緩存本質來講是使用空間換時間的思想,它在計算機世界中無處不在, 好比 CPU 就自帶 L一、L二、L3 Cache,這個通常應用開發可能關注較少。可是在一些實時系統、大規模計算模擬、圖像處理等追求極致性能的領域,就特別注重編寫緩存友好的代碼。

什麼是緩存友好?簡單來講,就是代碼在訪問數據的時候,儘可能使用緩存命中率高的方式。這個後面能夠單獨寫一篇 CPU 緩存系統以及如何編寫緩存友好代碼的文章。

1.1 緩存爲何有效?

緩存之因此可以大幅提升系統的性能,關鍵在於數據的訪問具備局部性,也就是二八定律:「百分之八十的數據訪問是集中在 20% 的數據上」。這部分數據也被叫作熱點數據。

緩存通常使用內存做爲存儲,內存讀寫速度快於磁盤,但容量有限,十分寶貴,不可能將全部數據都緩存起來。

若是應用訪問數據沒有熱點,不遵循二八定律,即大部分數據訪問並無集中在小部分數據上,那麼緩存就沒有意義,由於大部分數據尚未被再次訪問就已經被擠出緩存了。每次訪問都會回源到數據庫查詢,那麼反而會下降數據訪問效率。

1.2 緩存分類

  • 1. 本地緩存:

    使用進程內成員變量或者靜態變量,適合簡單的場景,不須要考慮緩存一致性、過時時間、清空策略等問題。

    能夠直接使用語言標準庫內的容器來作存儲。例如:

本地緩存

  • 2. 分佈式緩存:

    當緩存的數據量增大之後,單機不足以承載緩存服務時,就要考慮對緩存服務作水平擴展,引入緩存集羣。

    將數據分片後分散存儲在不一樣機器中,如何決定每一個數據分片存放在哪臺機器呢?通常是採用一致性 Hash 算法,它可以保證在緩存集羣動態調整,不斷增長或者減小機器後,客戶端訪問時依然可以根據 key 訪問到數據。

    一致性 Hash 算法也是值得用一篇文章來說的,若是暫時還不懂的話能夠去搜一下。

    經常使用的組件有 MemcacheRedis Cluster 等,第二個是在高性能內存存儲 Redis 的基礎上,提供分佈式存儲的解決方案。

1.3 緩存使用指南

1. 適合緩存的場景:

  • 讀多寫少:

    好比電商裏的商品詳情頁面,訪問頻率很高,可是通常寫入只在店家上架商品和修改信息的時候發生。若是把熱點商品的信息緩存起來,這將攔截掉不少對數據庫的訪問,提升系統總體的吞吐量。

    由於通常數據庫的 QPS 因爲有「ACID」約束、而且數據是持久化在硬盤的,因此比 Redis 這類基於內存的 NoSQL 存儲低很多。經常是一個系統的瓶頸,若是咱們把大部分的查詢都在 Redis 緩存中命中了,那麼系統總體的 QPS 也就上去了。

  • 計算耗時大,且實時性不高:
    好比王者榮耀裏的全區排行榜,通常一週更新一次,而且計算的數據量也比較大,因此計算後緩存起來,請求排行榜直接從緩存中取出,就不用實時計算了。

2. 不適合緩存的場景

  • 寫多讀少,頻繁更新。
  • 對數據一致性要求嚴格: 由於緩存會有更新策略,因此很難作到和數據庫實時同步。
  • 數據訪問徹底隨機: 由於這樣會致使緩存的命中率極低。

1.4 緩存更新的策略

如何更新緩存其實已經有總結得很是好的「最佳實踐」,咱們按照套路來,大機率不會犯錯。

主要分爲兩類 Cache-AsideCache-As-SoR。 SoR 即「System Of Record,記錄系統」,表示數據源,通常就是指數據庫。

一、Cache-Aside:

Cache-Aside架構圖

這應該是最容易想到的模式了,獲取數據時先從緩存讀,若是 cache hit 則直接返回,沒命中就從數據源獲取,而後更新緩存。

寫數據的時候則先更新數據源,而後設置緩存失效,下一次獲取數據的時候必然 cache miss,而後觸發回源

直接看僞代碼:

Cache-Aside 代碼示範

能夠看到這種方式對於緩存的使用者是不透明的,須要使用者手動維護緩存。

二、Cache-As-SoR:

Cache-As-SoR架構圖

從字面上來看,就是把 Cache 看成 SoR,也就是數據源,因此一切讀寫操做都是針對 Cache 的,由 Cache 內部本身維護和數據源的一致性。

這樣對於使用者來講就和直接操做 SoR 沒有區別了,徹底感知不到 Cache 的存在。

CPU 內部的 L一、L二、L3 Cache 就是這種方式,做爲數據的使用方應用程序,是徹底感知不到在內存和咱們之間還存在幾層的 Cache,可是咱們以前又提到編寫 「緩存友好」的代碼,不是透明的嗎?這是否是衝突呢?

其實否則,緩存友好是指咱們經過學習瞭解緩存內部實現、更新策略以後,經過調整數據訪問順序提升緩存的命中率。

Cache-As-SoR 又分爲如下三種方式:

  • Read Through:這種方式和 Cache-Aside 很是類似,都是在查詢時發生 cache miss 去更新緩存,可是區別在於 Cache-Aside 須要調用方手動更新緩存,而 Cache-As-SoR 則是由緩存內部實現本身負責,對應用層透明。
  • Write Through: 直寫式,就是在將數據寫入緩存的同時,緩存也去更新後面的數據源,而且必須等到數據源被更新成功後纔可返回。這樣保證了緩存和數據庫裏的數據一致性
  • Write Back:回寫式,數據寫入緩存便可返回,緩存內部會異步的去更新數據源,這樣好處是寫操做特別快,由於只須要更新緩存。而且緩存內部能夠合併對相同數據項的屢次更新,可是帶來的問題就是數據不一致,可能發生寫丟失。

2、預處理和延後處理

預先延後,這實際上是一個事物的兩面,無論是預先仍是延後核心思想都是將原本該在實時鏈路上處理的事情剝離,要麼提早要麼延後處理。下降實時鏈路的路徑長度, 這樣能有效提升系統性能。

2.1 預處理

舉個咱們團隊實際中遇到的問題:

前兩個月支付寶聯合杭州市政府發放消費劵,可是要求只有杭州市常駐居民才能領取,那麼須要在搶卷請求進入後臺的時候就判斷一下用戶是不是杭州常駐居民。

而判斷用戶是不是常駐居民這個是另一個微服務接口,若是直接實時的去調用那個接口,短時的高併發頗有可能把這個服務也拖掛,最終致使整個系統不可用,而且 RPC 自己也是比較耗時的,因此就考慮在這裏進行優化。

那麼該怎麼作呢?很簡單的一個思路,提早將杭州全部常駐居民的 user_id 存到緩存中, 好比能夠直接存到 Redis。大概就是千萬量級,這樣,當請求到來的時候咱們直接經過緩存能夠快速判斷是否來自杭州常駐居民。若是不是則直接在這裏返回前端。

這裏經過預先處理減小了實時鏈路上的 RPC 調用,既減小了系統的外部依賴,也極大的提升了系統的吞吐量。

預處理在 CPU 和操做系統中也普遍使用,好比 CPU 基於歷史訪存信息,將內存中的指令和數據預取到 Cache 中,這樣能夠大大提升Cache 命中率。 還好比在 Linux 文件系統中,預讀算法會預測即將訪問的 page,而後批量加載比當前讀請求更多的數據緩存在 page cache 中,這樣當下次讀請求到來時能夠直接從 cache 中返回,大大減小了訪問磁盤的時間。

2.2 延後處理

仍是支付寶,上栗子:

集五福活動

這是支付寶春節集五福活動開獎當晚,不過,做爲非酋的我通常是不屑於參與這種活動的。

你們發現沒有,這類活動中獎獎金通常會顯示 「稍後到帳」,爲何呢?那固然是到帳這個操做不簡單!

到帳即轉帳,A 帳戶給 B 帳戶轉錢,A 減錢, B 就必需要同時加上錢,也就是說不能 A 減了錢但 B 沒有加上,這就會致使資金損失。資金安全是支付業務的生命線,這可不行。

這兩個動做必須一塊兒成功或是一塊兒都不成功,不能只成功一半,這是保證數據一致性。 保證兩個操做同時成功或者失敗就須要用到事務

若是去實時的作到帳,那麼大機率數據庫的 TPS(每秒處理的事務數) 會是瓶頸。經過產品提示,將到帳操做延後處理,解決了數據庫 TPS 瓶頸。

延後處理還有一個很是著名的例子,COW(Copy On Write,寫時複製)。 Linux 建立進程的系統調用 fork,fork 產生的子進程只會建立虛擬地址空間,而不會分配真正的物理內存,子進程共享父進程的物理空間,只有當某個進程須要寫入的時候,纔會真正分配物理頁,拷貝該物理頁,經過 COW 減小了不少沒必要要的數據拷貝。

3、池化

後臺開發過程當中你必定離不開各類 「池子」: 內存池、鏈接池、線程池、對象池......

內存、鏈接、線程這些都是資源,建立線程、分配內存、數據庫鏈接這些操做都有一個特徵, 那就是建立和銷燬過程都會涉及到不少系統調用或者網絡 IO。 每次都在請求中去申請建立這些資源,就會增長請求處理耗時,可是若是咱們用一個 容器(池) 把它們保存起來,下次須要的時候,直接拿出來使用,避免重複建立和銷燬浪費的時間。

3.1 內存池

在 C/C++ 中,常用 malloc、new 等 API 動態申請內存。因爲申請的內存塊大小不一,若是頻繁的申請、釋放會致使大量的內存碎片,而且這些 API 底層依賴系統調用,會有額外的開銷。

內存池就是在使用內存前,先向系統申請一塊空間留作備用,使用者須要內池時向內存池申請,用完後還回來。

內存池的思想很是簡單,實現卻不簡單,難點在於如下幾點:

  • 如何快速分配內存
  • 下降內存碎片率
  • 維護內存池所需的額外空間儘可能少

若是不考慮效率,咱們徹底能夠將內存分爲不一樣大小的塊,而後用鏈表鏈接起來,分配的時候找到大小最合適的返回,釋放的時候直接添加進鏈表。如:

空閒鏈表

固然這只是玩具級別的實現,業界有性能很是好的實現了,咱們能夠直接拿來學習和使用。

好比 Google 的 「tcmalloc」 和 Facebook 的 「jemalloc」。

限於篇幅咱們不在這裏詳細講解它們的實現原理,若是感興趣能夠搜來看看,也推薦去看看被譽爲神書的 CSAPP(《深刻理解計算機系統》)第 10 章,那裏也講到了動態內存分配算法。

3.2 線程池

線程是幹嗎的?線程就是咱們程序執行的實體。在服務器開發領域,咱們常常會爲每一個請求分配一個線程去處理,可是線程的建立銷燬、調度都會帶來額外的開銷,線程太多也會致使系統總體性能降低。在這種場景下,咱們一般會提早建立若干個線程,經過線程池來進行管理。當請求到來時,只需從線程池選一個線程去執行處理任務便可。

線程池經常和隊列一塊兒使用來實現任務調度,主線程收到請求後將建立對應的任務,而後放到隊列裏,線程池中的工做線程等待隊列裏的任務。

線程池實現上通常有四個核心組成部分:

  • 管理器(Manager): 用於建立並管理線程池。
  • 工做線程(Worker): 執行任務的線程。
  • 任務接口(Task): 每一個具體的任務必須實現任務接口,工做線程將調用該接口來完成具體的任務。
  • 任務隊列(TaskQueue): 存放還未執行的任務。

線程池模型

線程池在 C、C++ 中沒有具體的實現,須要應用開發者手動實現上訴幾個部分。

在 Java 中 「ThreadPoolExecutor」 類就是線程池的實現。後續我也會寫文章分析 C++ 如何寫一個簡單的線程池以及 Java 中線程池是如何實現的。

3.3 鏈接池

顧名思義,鏈接池是建立和管理鏈接的。

你們最熟悉的莫過於數據庫鏈接池,這裏咱們簡單分析下若是不用數據庫鏈接池,一次 SQL 查詢請求會通過哪些步驟:

  1. 和 MySQL server 創建 TCP 鏈接:

    • 三次握手
  2. MySQL 權限認證:

    • Server 向 Client 發送 密鑰
    • Client 使用密鑰加密用戶名、密碼等信息,將加密後的報文發送給 Server
    • Server 根據 Client 請求包,驗證是不是合法用戶,而後給 Client 發送認證結果
  3. Client 發送 SQL 語句
  4. Server 返回語句執行結果
  5. MySQL 關閉
  6. TCP 鏈接斷開

    • 四次揮手

能夠看出不使用鏈接池的話,爲了執行一條 SQL,會花不少時間在安全認證、網絡IO上。

若是使用鏈接池,執行一條 SQL 就省去了創建鏈接和斷開鏈接所需的額外開銷。

還能想起哪裏用到了鏈接池的思想嗎?我認爲 HTTP 長連接也算一個變相的連接池,雖然它本質上只有一個鏈接,可是思想卻和鏈接池不謀而合,都是爲了複用同一個鏈接發送多個 HTTP 請求,避免創建和斷開鏈接的開銷。

池化其實是預處理和延後處理的一種應用場景,經過池子將各種資源的建立提早和銷燬延後。

4、同步變異步

對於處理耗時的任務,若是採用同步的方式,那麼會增長任務耗時,下降系統併發度。

能夠經過將同步任務變爲異步進行優化。

舉個例子,好比咱們去 KFC 點餐,遇到排隊的人不少,當點完餐後,大多狀況下咱們會隔幾分鐘就去問好了沒,反覆去問了好幾回纔拿到,在這期間咱們也無法幹活了,這時候咱們是這樣的:

同步寫法

這個就叫同步輪訓, 這樣效率顯然過低了。

服務員被問煩了,就在點完餐後給咱們一個號碼牌,每次準備好了就會在服務檯叫號,這樣咱們就能夠在被叫到的時候再去取餐,中途能夠繼續幹本身的事。

這就叫異步,在不少編程語言中有異步編程的庫,好比 C++ std::future、Python asyncio 等,可是異步編程每每須要回調函數(Callback function),若是回調函數的層級太深,這就是回調地獄(Callback hell)。回調地獄如何優化又是一個龐大的話題。。。。

這個例子至關於函數調用的異步化,還有的是狀況是處理流程異步化,這個會在接下來消息隊列中講到。

5、消息隊列

消息隊列示意圖

這是一個很是簡化的消息隊列模型,上游生產者將消息經過隊列發送給下游消費者。在這之間,消息隊列能夠發揮不少做用,好比:

5.1 服務解耦

有些服務被其它不少服務依賴,好比一個論壇網站,當用戶成功發佈一條帖子有一系列的流程要作,有積分服務計算積分,推送服務向發佈者的粉絲推送一條消息..... 對於這類需求,常見的實現方式是直接調用:

直接調用

這樣若是須要新增一個數據分析的服務,那麼又得改動發佈服務,這違背了依賴倒置原則即上層服務不該該依賴下層服務,那麼怎麼辦呢?

發佈訂閱模式

引入消息隊列做爲中間層,當帖子發佈完成後,發送一個事件到消息隊列裏,而關心帖子發佈成功這件事的下游服務就能夠訂閱這個事件,這樣即便後續繼續增長新的下游服務,只須要訂閱該事件便可,徹底不用改動發佈服務,完成系統解耦。

5.2 異步處理

有些業務涉及到的處理流程很是多,可是不少步驟並不要求實時性。那麼咱們就能夠經過消息隊列異步處理。好比淘寶下單,通常包括了風控、鎖庫存、生成訂單、短信/郵件通知等步驟。可是核心的就風控和鎖庫存, 只要風控和扣減庫存成功,那麼就能夠返回結果通知用戶成功下單了。後續的生成訂單,短信通知均可以經過消息隊列發送給下游服務異步處理。大大提升了系統響應速度。

這就是處理流程異步化。

5.3 流量削峯

通常像秒殺、抽獎、搶卷這種活動都伴隨着短期海量的請求, 通常超事後端的處理能力,那麼咱們就能夠在接入層將請求放到消息隊列裏,後端根據本身的處理能力不斷從隊列裏取出請求進行業務處理。

就像最近長江汛期,上游短期大量的洪水匯聚直奔下游,可是經過三峽大壩將這些水緩存起來,而後勻速的向下遊釋放,起到了很好的削峯做用。

起到了平均流量的做用。

5.4 總結

消息隊列的核心思想就是把同步的操做變成異步處理,異步處理會帶來相應的好處,好比:

  • 服務解耦
  • 提升系統的併發度,將非核心操做異步處理,不會阻塞住主流程

可是軟件開發沒有銀彈,全部的方案選擇都是一種 trade-off。 一樣,異步處理也不全是好處,也會致使一些問題:

  • 下降了數據一致性,從強一致性變爲最終一致性
  • 有消息丟失的風險,好比宕機,須要有容災機制

6、批量處理

在涉及到網絡鏈接、IO等狀況時,將操做批量進行處理可以有效提升系統的傳輸速率和吞吐量。

在先後端通訊中,經過合併一些頻繁請求的小資源能夠得到更快的加載速度。

好比咱們後臺 RPC 框架,常常有更新數據的需求,而有的數據更新的接口每每只接受一項,這個時候咱們每每會優化下更新接口,

使其可以接受批量更新的請求,這樣能夠將批量的數據一次性發送,大大縮短網絡 RPC 調用耗時。

7、數據庫

咱們常把後臺開發調侃爲「CRUD」,數據庫在整個應用開發過程當中的重要性不言而喻。

並且不少時候系統的瓶頸也每每處在數據庫這裏,慢的緣由也有不少,好比多是沒用索引、沒用對索引、讀寫鎖衝突等等。

那麼如何使用數據才能又快又好呢?下面這幾點須要重點關注:

7.1 索引

索引多是咱們平時在使用數據庫過程當中接觸得最多的優化方式。索引比如圖書館裏的書籍索引號,想象一下,若是我讓你去一個沒有書籍索引號的圖書館找《人生》這本書,你是什麼樣的感覺?固然是懷疑人生,同理,你應該能夠理解當你查詢數據,卻不用索引的時候數據庫該有多崩潰了吧。

數據庫表的索引就像圖書館裏的書籍索引號同樣,能夠提升咱們檢索數據的效率。索引能提升查找效率,但是你有沒有想過爲何呢?這是由於索引通常而言是一個排序列表,排序意味着能夠基於二分思想進行查找,將查詢時間複雜度作到 O(log(N)),快速的支持等值查詢和範圍查詢。

二叉搜索樹查詢效率無疑是最高的,由於平均來講每次比較都能縮小一半的搜索範圍,可是通常在數據庫索引的實現上卻會選擇 B 樹或 B+ 樹而不用二叉搜索樹,爲何呢?

這就涉及到數據庫的存儲介質了,數據庫的數據和索引都是存放在磁盤,而且是 InnoDB 引擎是以頁爲基本單位管理磁盤的,一頁通常爲 16 KB。AVL 或紅黑樹搜索效率雖然很是高,可是一樣數據項,它也會比 B、B+ 樹更高,高就意味着平均來講會訪問更多的節點,即磁盤IO次數!

根據 Google 工程師 Jeff Dean 的統計,訪問內存數據耗時大概在 100 ns,訪問磁盤則是 10,000,000 ns。

因此表面上來看咱們使用 B、B+ 樹沒有 二叉查找樹效率高,可是實際上因爲 B、B+ 樹下降了樹高,減小了磁盤 IO 次數,反而大大提高了速度。

這也告訴咱們,沒有絕對的快和慢,系統分析要抓主要矛盾,先分析出決定系統瓶頸的究竟是什麼,而後纔是針對瓶頸的優化。

其實關於索引想寫的也還有不少,但仍是受限於篇幅,之後再單獨寫。

先把我認爲索引必知必會的知識列出來,你們能夠查漏補缺:

  • 主鍵索引和普通索引,以及它們之間的區別
  • 最左前綴匹配原則
  • 索引下推
  • 覆蓋索引、聯合索引

7.2 讀寫分離

通常業務剛上線的時候,直接使用單機數據庫就夠了,可是隨着用戶量上來以後,系統就面臨着大量的寫操做和讀操做,單機數據庫處理能力有限,容易成爲系統瓶頸。

因爲存在讀寫鎖衝突,而且不少大型互聯網業務每每讀多寫少,讀操做會首先成爲數據庫瓶頸,咱們但願消除讀寫鎖衝突從而提高數據庫總體的讀寫能力。

那麼就須要採用讀寫分離的數據庫集羣方式,一主多從,主庫會同步數據到從庫。寫操做都到主庫,讀操做都去從庫。

讀寫分離

讀寫分離到以後就避免了讀寫鎖爭用,這裏解釋一下,什麼叫讀寫鎖爭用:

MySQL 中有兩種鎖:

  • 排它鎖( X 鎖): 事務 T 對數據 A 加上 X 鎖時,只容許事務 T 讀取和修改數據 A。
  • 共享鎖( S 鎖): 事務 T 對數據 A 加上 S 鎖時,其餘事務只能再對數據 A 加 S 鎖,而不能加 X 鎖,直到 T 釋放 A 上的 S 鎖。

讀寫分離解決問題的同時也會帶來新問題,好比主庫和從庫數據不一致

MySQL 的主從同步依賴於 binlog,binlog(二進制日誌)是 MySQL Server 層維護的一種二進制日誌,是獨立於具體的存儲引擎。它主要存儲對數據庫更新(insert、delete、update)的 SQL 語句,因爲記錄了完整的 SQL 更新信息,因此 binlog 是能夠用來數據恢復和主從同步複製的。

從庫從主庫拉取 binlog 而後依次執行其中的 SQL 便可達到複製主庫的目的,因爲從庫拉取 binlog 存在網絡延遲等,因此主從數據存在延遲問題。

那麼這裏就要看業務是否容許短期內的數據不一致,若是不能容忍,那麼能夠經過若是讀從庫沒獲取到數據就去主庫讀一次來解決。

7.3 分庫分表

若是用戶愈來愈多,寫請求暴漲,對於上面的單 Master 節點確定扛不住,那麼該怎麼辦呢?多加幾個 Master?不行,這樣會帶來更多的數據不一致的問題,增長系統的複雜度。那該怎麼辦?就只能對庫表進行拆分了。

常見的拆分類型有垂直拆分和水平拆分。

考慮拼夕夕電商系統,通常有 訂單表、用戶表、支付表、商品表、商家表等, 最初這些表都在一個數據庫裏。
後來隨着砍一刀帶來的海量用戶,拼夕夕後臺扛不住了! 因而緊急從阿狸粑粑那裏挖來了幾個 P八、P9 大佬對系統進行重構。

  1. P9 大佬第一步先對數據庫進行垂直分庫,

根據業務關聯性強弱,將它們分到不一樣的數據庫, 好比訂單庫,商家庫、支付庫、用戶庫。

  1. 第二步是對一些大表進行垂直分表,將一個表按照字段分紅多表,每一個表存儲其中一部分字段。 好比商品詳情表可能最初包含了幾十個字段,可是每每最多訪問的是商品名稱、價格、產地、圖片、介紹等信息,因此咱們將不常訪問的字段單獨拆成一個表。
  • 因爲垂直分庫已經按照業務關聯切分到了最小粒度,數據量任然很是大,P9 大佬開始水平分庫,好比能夠把訂單庫分爲訂單1庫、訂單2庫、訂單3庫...... 那麼如何決定某個訂單放在哪一個訂單庫呢?能夠考慮對主鍵經過哈希算法計算放在哪一個庫。
  • 分完庫,單表數據量任然很大,查詢起來很是慢,P9 大佬決定按日或者按月將訂單分表,叫作日表、月表。

分庫分表同時會帶來一些問題,好比平時單庫單表使用的主鍵自增特性將做廢,由於某個分區庫表生成的主鍵沒法保證全局惟一,這就須要引入全局 UUID 服務了。

通過一番大刀闊斧的重構,拼夕夕恢復了往日的活力,你們又能夠愉快的在上面互相砍一刀了。

(分庫分表會引入不少問題,並無一一介紹,這裏只是爲了講解什麼是分庫分表)

8、具體技法

8.1 零拷貝

高性能的服務器應當避免沒必要要數據複製,特別是在用戶空間和內核空間之間的數據複製。 好比 HTTP 靜態服務器發送靜態文件的時候,通常咱們會這樣寫:

發送文件

若是瞭解 Linux IO 的話就知道這個過程包含了內核空間和用戶空間之間的屢次拷貝:

IO示意圖

內核空間和用戶空間之間數據拷貝須要 CPU 親自完成,可是對於這類數據不須要在用戶空間進行處理的程序來講,這樣的兩次拷貝顯然是浪費。什麼叫 「不須要在用戶空間進行處理」?

好比 FTP 或者 HTTP 靜態服務器,它們的做用只是將文件從磁盤發送到網絡,不須要在中途對數據進行編解碼之類的計算操做。

若是可以直接將數據在內核緩存之間移動,那麼除了減小拷貝次數之外,還能避免內核態和用戶態之間的上下文切換。

而這正是零拷貝(Zero copy)乾的事,主要就是利用各類零拷貝技術,減小沒必要要的數據拷貝,將 CPU 從數據拷貝這樣簡單的任務解脫出來,讓 CPU 專一於別的任務。

經常使用的零拷貝技術:

  1. mmap

    mmap 經過內存映射,將文件映射到內核緩衝區,同時,用戶空間能夠共享內核空間的數據。這樣,在進行網絡傳輸時,就能夠減小內核空間到用戶空間的拷貝次數。

mmap

  1. sendfile

    sendfile 是 Linux2.1 版本提供的,數據不通過用戶態,直接從頁緩存拷貝到 socket 緩存,同時因爲和用戶態徹底無關,就減小了一次上下文切換。

    在 Linux 2.4 版本,對 sendfile 進行了優化,直接經過 DMA 將磁盤文件數據讀取到 socket 緩存,真正實現了 」0」 拷貝。前面 mmap 和 2.1 版本的 sendfile 實際上只是消除了用戶空間和內核空間之間拷貝,而頁緩存和 socket 緩存之間的拷貝依然存在。

8.2 無鎖化

在多線程環境下,爲了不 競態條件(race condition), 咱們一般會採用加鎖來進行併發控制,鎖的代價也是比較高的,鎖會致使上線文切換,甚至被掛起直到鎖被釋放。

基於硬件提供的原子操做 CAS(Compare And Swap) 實現一些高性能無鎖的數據結構,好比無鎖隊列,能夠在保證併發安全的狀況下,提供更高的性能。

首先須要理解什麼是 CAS,CAS 有三個操做數,內存裏當前值M,預期值 E,修改的新值 N,CAS 的語義就是:

若是當前值等於預期值,則將內存修改成新值,不然不作任何操做

用 C 語言來表達就是:

CAS

注意,上面 CAS 函數其實是一條原子指令,那麼是如何用的呢?

假設我須要實現這樣一個功能:

對一個全局變量 global 在兩個不一樣線程分別對它加 100 次,這裏多線程訪問一個全局變量存在 race condition,因此咱們須要採用線程同步操做,下面我分別用鎖和CAS的方法來實現這個功能。

CAS和鎖示範

經過使用原子操做大大下降了鎖衝突的可能性,提升了程序的性能。

除了 CAS,還有一些硬件原子指令:

  • Fetch-And-Add,對變量原子性 + 1
  • Test-And-Set,這是各類鎖算法的核心,在 AT&T/GNU 彙編語法下,叫 xchg 指令,我會單獨寫一篇如何使用 xchg 實現各類鎖。

8.3 序列化與反序列化

先看看維基百科怎麼定義的序列化:

In computing, serialization (US spelling) or serialisation (UK spelling) is the process of translating a data structure or object state into a format that can be stored (for example, in a file or memory data buffer) or transmitted (for example, across a computer network) and reconstructed later (possibly in a different computer environment). When the resulting series of bits is reread according to the serialization format, it can be used to create a semantically identical clone of the original object. For many complex objects, such as those that make extensive use of references, this process is not straightforward. Serialization of object-oriented objects does not include any of their associated methods with which they were previously linked.

我相信你大機率沒有看完上面的英文描述,其實我也不愛看英文資料,總以爲很慢,可是計算機領域一手的學習資料都是美帝那邊的,因此沒辦法,必須逼本身去試着讀一些英文的資料。

實際上也沒有那麼難,熟悉經常使用的幾百個專業名詞,句子都是很是簡單的一些從句。沒看的話,再倒回去看看?

這裏我就不作翻譯了,主要是水平過低,估計作到「信達雅」的信都很難。

扯遠了,仍是回到序列化來。

全部的編程必定是圍繞數據展開的,而數據呈現形式每每是結構化的,好比結構體(Struct)、類(Class)。 可是當咱們 經過網絡、磁盤等傳輸、存儲數據的時候卻要求是二進制流。 好比 TCP 鏈接,它提供給上層應用的是面向鏈接的可靠字節流服務。那麼如何將這些結構體和類轉化爲可存儲和可傳輸的字節流呢?這就是序列化要乾的事情,反之,從字節流如何恢復爲結構化的數據就是反序列化。

序列化解決了對象持久化和跨網絡數據交換的問題。

序列化通常按照序列化後的結果是否可讀,可分爲如下兩類:

  • 文本類型:

    如 JSON、XML,這些類型可讀性很是好,是自解釋的。也經常用在先後端數據交互上,由於接口調試,可讀性高很是方便。可是缺點就是信息密度低,序列化後佔用空間大。

  • 二進制類型

    如 Protocol Buffer、Thrift等,這些類型採用二進制編碼,數據組織得更加緊湊,信息密度高,佔用空間小,可是帶來的問題就是基本不可讀。

還有 Java 、Go 這類語言內置了序列化方式,好比在 Java 裏實現了 Serializable 接口即表示該對象可序列化。

說到這讓我想起了大一寫的的兩個程序,一個是用剛 C 語言寫的公交管理系統,當時須要將公交線路、站點信息持久化保存,當時的方案就是每一個公交線路寫在一行,用 "|"分割信息,好比:

5|6:00-22:00|大學城|南山站|北京站
123|6:30-23:00|南湖大道|茶山劉|世界

第一列就是線路編號、第二項是發車時間、後面就是途徑的站點。是否是很是原始?實際上這也是一種序列化方式,只是效率很低,也不通用。並且存在一個問題就是若是信息中包含 「|」怎麼辦?固然是用轉義。

第二個程序是用 Java 寫的網絡五子棋,當時須要經過網絡傳輸表示棋子位置的對象,查了一圈最後發現只須要實現 Serializable 接口,本身什麼都不用幹,就能本身完成對象的序列化,而後經過網絡傳輸後反序列化。當時哪懂得這就叫序列化,只以爲牛逼、神奇!

最後完成了一個能夠網絡五子棋,拉着隔壁室友一塊兒玩。。。真的是成就感滿滿哈哈哈。

說來在編程方面,已經好久沒有這樣的成就感了。

總結

這篇文章主要是粗淺的介紹了一些系統設計、系統優化的套路和最佳實踐。

不知道你發現沒有,從緩存到消息隊列、CAS......,不少看起來很牛逼的架構設計其實都來源於操做系統、體系結構。

因此我很是熱衷學習一些底層的基礎知識,這些看似古老的技術是通過時間洗禮留下來的好東西。如今不少的新技術、框架看似很是厲害,實則很多都是新瓶裝舊酒,每幾年又會被淘汰一批。

最後說一句(求關注)

這篇文章寫了挺久的,從寫文章、畫圖,調格式每一步都很花時間。若是以爲對你有幫助的話,能夠點個關注或者在看鼓勵下~

文章持續更新,微信搜索「 編程指北 」第一時間獲取,回覆【資料】有我準備的一線 BAT 大廠面試資料和簡歷模板。

期待你的關注~
有任何問題,歡迎留言~

相關文章
相關標籤/搜索