Java開發高併發使用及分佈式系統設計與原理分析

NoSQL Java 編程語言 CPU 圖靈學院 圖靈學院 2017-09-28

引言

高併發就是可以使用多個線程或者多個進程,同時處理(就是併發)不同的的操作。比如說一個網站,同時訪問的數量很多,就是高併發。想要實現併發就有多看這方面的資料了。知道了這個,高併發就知道了唄。

分佈式系統(distributed system)是建立在網絡之上的軟件系統。正是因為軟件的特性,所以分佈式系統具有高度的內聚性和透明性。因此,網絡和分佈式系統之間的區別更多的在於高層軟件(特別是操作系統),而不是硬件。內聚性是指每一個數據庫分佈節點高度自治,有本地的數據庫管理系統。透明性是指每一個數據庫分佈節點對用戶的應用來說都是透明的,看不出是本地還是遠程。在分佈式數據庫系統中,用戶感覺不到數據是分佈的,即用戶不須知道關係是否分割、有無副本、數據存於哪個站點以及事務在哪個站點上執行等。

一丶使用緩存服務器

使用Redis作為緩存服務器的,剛開始的時候會滿足需要,隨著項目的增大緩存數據的增多就會查詢和插入更慢這時就要考慮Redis集群方案了

使用Redis分佈式要保證數據都能能夠平均的緩存到每一臺機器,首先想到的做法是對數據進行分片,因為Redis是key-value存儲的,首先想到的是Hash分片,可能的做法是對key進行哈希運算,得到一個long值對分佈式的數量取模會得到一個一個對應數據庫的一個映射,沒有讀取就可以定位到這臺數據庫,那麼速度但然會提升了。

但是取模的hash算法是有問題的如果集群數量不變的話沒有什麼問題,一旦增加一臺機器或者一臺機器掛掉,導致機器數量變化,就會導致計算的出的數據庫映射亂掉,不能正確存取數據了。

因為這個問題引入我們說的一致性哈希算法,這個哈希算法具有的特徵

1.均衡性:也有人把它定義為平衡性,是指哈希的結果能夠儘可能分佈到所有的節點中去,這樣可以有效的利用每個節點上的資源。

2.單調性:對於單調性有很多翻譯讓我非常的不解,而我想要的是當節點數量變化時哈希的結果應儘可能的保護已分配的內容不會被重新分派到新的節點。

3.分散性和負載:這兩個其實是差不多的意思,就是要求一致性哈希算法對 key 哈希應儘可能的避免重複。

一致性哈希就數據結構是創建一個排序的環形數據結構,有許多個區域,先讓每一臺服務器都分佈環上,取每一個服務器的特效做哈希運行,得到的值放進環中,進行排序這樣就能根據哈希特徵找到對應的真是服務器,能夠讓把服務器平均的分佈到環上。

第一個特徵均衡性:就是儘量的讓數據平均的分部到每一個服務器,不讓某臺機器壓力特別打,或者乾脆沒活幹,因為這個原因,我們的每一個服務器都添加幾個虛擬服務器,比如真是服務器叫node1那麼第一個服務器的虛擬服務器就叫node1-1,node1-2...,根據這些特徵進行哈希運算也分佈到環中,這樣就能把服務器平均的分佈到環中。

第二個特徵單調性:因為服務器都在環中,數據的key進行哈希運算得到一個值,跟環中的服務器的哈希值進行比較,取離當前值最接近的哈希值對象的服務器,這樣就是獲取服務器的原理了,我們是做了一個偷懶的工作,服務器哈希進行排序,以順時針方式得到一個剛好大於key哈希的服務器。

單調性是在不管添加節點還是刪除節點,原來對應的服務器不變,因為這個環很大,服務器是零星分佈的,這樣增加或者刪除一個節點只有受影響的都是當前節點,但是key對應的數據庫是不變的,也不能說不變,是把變化變得儘可能的小。

第三個特徵分散性和負載:指服務器在環中儘可能的分散,儘可能的讓數據平均分佈到不同的服務器,我們就是使用虛擬節點的方式解決的。

其實在Linux中(我實在不知道Windows,我猜應該差不多)進程和線程是同一個數據結構——task_struct,對於內核(kernel)來說並沒有進程和線程的區別,只有進程——kernel稱之為task。所以 在Linux中進程和線程並沒有父子關係而是平行的結構 ,表示進程的數據結構填充的數據多一些,包括了打開文件,共享內存之類的,這個被稱為主線程;其他線程的數據結構這些項目則為空,並且有一個“父進程”的指針,指向了“主線程”。

明白了這一點我們就清楚了, 操作系統調度的最小對象其實是——線程 ,但是名字叫task,教科書上叫進程。。。。有點混亂了嗎?所以我們引入一個新的術語—— 併發實體 。所有CPU調度的最小單位我們統稱為併發實體,無論是進程還是線程或者是其他的什麼“怪胎”。(沒錯,我會在下一次介紹這些怪胎。)

二丶為什麼要線程?

勇敢提出這個問題的人要受到表揚,他冒著被無情嘲諷的危險提出了一個很白痴的問題。(我覺得回答不出來這個問題的人,才是真正的白痴)回答這個問題要回答另一個基本的問題——為什麼要併發

我們想象一下,一個Web服務器,可能是下面的代碼

while (true){request = next_http_request()request_work(request)}

程序循環獲取新請求(next_http_request),執行請求(request_work),然後繼續下一次循環。request_work會從硬盤讀取文件,然後發送給客戶端作為HTTP的響應,而硬盤I/O是一個阻塞操作,也就是說request_work會一直等待讀取完數據之後才能釋放CPU的控制權,然後下一個請求才有機會被執行。

這就是併發要解決的問題,當request_work發起I/O之後CPU是完全空閒下來的,而可憐的新請求(next_http_request)必須等待I/O完成之後才可以獲取CPU的控制權。所以CPU的利用率非常低, 併發要解決的問題就是提高CPU的利用率 。明白這一點我們也就清楚了,併發只對非CPU密集型程序管用,如果CPU利用率非常高,更多的併發只會讓情況更加糟糕

那麼併發為什麼一定是多線程而不是多進程呢?其實在Linux下進程和線程的創建成本沒有什麼區別(都是task_struct),但是進程之間可以共享數據的方式只能通過非常複雜的IPC來實現, 線程之間代碼都是共享的,地址空間也是共享的,所以共享數據的方式更加高效。 (進程要考慮隔離,一個進程沒有辦法直接訪問另一個進程;線程不用隔離,線程之間共享內存)

我們修改成多線程版本的Web服務器

while (true){request = next_http_request()request_work_in_thread(request)}

request_work_in_thread方法會啟動一個線程(work線程),然後CPU開始執行next_http_request獲得下一個請求。

request對象是在主線程創建的,可以直接傳遞給request_work_in_thread中的work線程使用。

我們提高CPU利用率所以需要並行,我們要提高併發實體之間共享數據的效率所以選擇了線程作為併發實體的實現

三丶Java的線程

好了,回到了Java。在Java中啟動一個線程非常簡單——只要new Thread就搞定了。JVM會把它變成操作系統的API,如果是Linux則會生成一個task_struct的結構。至於Runable之類的東西其實最後還是Thread,即便是Java併發包最後也還是用Thread。

所以至此,我們成功把《操作系統原理》中進程、線程和Java的線程“融匯貫通了”。下面開始另一個東西——PV操作。

競爭條件,臨界區、PV信號量

恩,你這一部分估計也已經還給老師了。沒關係,我們一起回憶一下。

舉個例子:

public void plus(int value){count = count + value;}

當多線程同時調用plus的時候程序的邏輯是錯誤的。count+value並不是一個原子操作,它會被變成三個CPU指令

  • 獲取count的數據到寄存器(還記得嗎?CPU只訪問寄存器)

  • 寄存器+value,並且寫回寄存器

  • 寄存器寫回到內存

    如果T1,T2兩個線程同時執行(count=0)

  • T1 獲取count的數據到寄存器

  • T1 將寄存器的值加10

  • T2 獲取count的數據到寄存器

  • T2 將寄存器的值加30

  • T2 寄存器寫回到內存(count=30)

  • T1 寄存器寫回到內存(count=10)

    我們期望的可能是40,但是實際情況是10,因為T2訪問數據的時候T1還沒有來得及寫回到內存中。當兩個線程訪問同一個數據,最後的結果依賴於線程的順序這個就叫競爭條件。避免競爭條件的方法就是——通過臨界區把一組動作“原子化”。例子中就是把:count = count + value,原子化。(原子化是指一次做完;其他人排隊等候。)

就像你去買咖啡,收銀員是所有人共享的(競爭條件),如果他經歷:問杯型、種類、口味;收費;給你發票,這三個過程不能被打斷否則會亂掉的。所以大家需要依次排隊。(臨界區)

如何實現臨界區?答案是PV信號量(也叫PV操作,PV原語)。它是著名“河南籍”計算機科學家——E.W.Dijkstra設計的一套算法,老爺子這套嚴密的理論是現代併發的基礎。簡單來說他定義了兩個操作

設一個計數器s

  • P s-1,如果s小於0則休眠否則繼續執行

  • V s+1,如果s<=0則喚醒等待進程否則繼續執行

P操作相當於使用資源,執行這個操作相當於:這個桌子我承包了,你們等我用過之後再用。(如果桌子有人那就只能乖乖等著了)

V操作相當於釋放資源,執行這個操作相當於:這個桌子我不用了,下一個是誰?來用吧。(如果沒有下一個人,那就直接走人了)

沒錯,PV就是“鎖”。《操作系統原理》中競爭條件、臨界區都是現實中不存在的概念,只有PV操作被具體實現了,就是我們稱之為鎖的東西。

四丶Java中的鎖

所有的“鎖”都是一種PV操作,鎖的區別在於你選擇的“計數器”是什麼?比如你選擇的計數器是當前對象那麼對應的關鍵是“synchronized”(還有個名字叫管程,天真的科學家們覺得面向對象的鎖好牛B,就賜予它一個專門的名詞),如果你的計數器是一個原子類型的值那麼可能就是AtomicInteger的inc或者dec操作。這個就是 鎖的粒度 ,鎖越小競爭條件就越小。

五丶分佈式服務框架設計

分佈式服務框架一般可以分為以下幾個部分,

(1)RPC基礎層:

包括底層通信框架,如NIO框架、通信協議,序列化和反序列化協議,

以及在這幾部分上的封裝,屏蔽底層通信細節和序列化方式差異

(2)服務發佈/消費:

服務提供者根據消費者請求消息中的接口名,方法名,參數列表等信息,通過Java反射,調用本地的接口實現類;

服務消費者將服務提供者發佈的接口封裝成遠程服務調用;

(3)服務調用鏈:

在服務調用的職責鏈中,通過在調用鏈切面的編碼完成相關的監控和擴展,如負載均衡,服務調用性能統計,調用完成通知,

失敗重發等功能

(4)服務註冊中心:

註冊中心負責服務的發佈和通知,需要支持服務的平滑上線下線等

(5)服務治理中心:

服務治理中心是一個可視化的模塊,提供對服務的可視化分析和維護,包括服務運行狀態,調用關係和健康度等

六丶分佈式系統原理

1. 哈希方式,把不同的值進行哈希運算,映射到,不同的機器或者節點。考慮冗餘的時候可以把多個哈希值映射到同一個地方。哈希的實現方式,取餘。其實現擴展時,比較困難,數據分散在很多機器上,擴展的時候要從個機器上獲取數據。而且容易出現分佈不均有的情況。

常見的哈希,用IP、URL、ID、或者固定的值進行哈希,總是得到相同的結果。

2. 按數據範圍分佈,比如ID在1~20的在機器A上,ID在21~40的在機器B上,ID在40~60的在機器C上實現,ID在60~100的分佈在機器D上,數據分佈比較均勻。如果某個節點處理能力有限,可以直接分裂該節點。維護數據分佈的元信息,可能出現單點瓶頸。幾千機器,每個機器又劃分為N個範圍,導致需要維護的數據分佈範圍元數據過大,導致可能需要幾臺機器實現。

一定要嚴格控制元數據量,進可能的減少元數據的存儲。

3. 按數據量分佈,另一類常用的數據分佈方式則是按照數據量分佈數據。與哈希方式和按數據範圍方式不同,數據量分佈數據與具體的數據特徵無關,而是將數據視為一個順序增長的文件,並將這個文件按照某一較為固定的大小劃分為若干數據塊(chunk),不同的數據塊分佈到不同的服務器上。與按數據範圍分佈數據的方式類似的是,按數據量分佈數據也需要記錄數據塊的具體分佈情況,並將該分佈信息作為元數據使用元數據服務器管理。

由於與具體的數據內容無關,按數據量分佈數據的方式一般沒有數據傾斜的問題,數據總是被均勻切分並分佈到集群中。當集群需要重新負載均衡時,只需通過遷移數據塊即可完成。集群擴容也沒有太大的限制,只需將部分數據庫遷移到新加入的機器上即可以完成擴容。按數據量劃分數據的缺點是需要管理較為複雜的元信息,與按範圍分佈數據的方式類似,當集群規模較大時,元信息的數據量也變得很大,高效的管理元信息成為新的課題。

4. 一致性哈希,構造哈希環,有哈希域[0,10],則構造3個部分,[1,4)/[4,9)/[9,10),[0,1)/分成了3個部分,這3部分是一個環狀,增加機器時,變動的是其附近的節點,分擔的是附近節點的壓力,其元數據的維護和按數據量分佈一樣。其未來的擴展,可以實現多個需節點。

5. 構建映射元數據,建立映射表的方式。

6. 副本與數據分佈,把一個數據副本分散到多臺服務器上。比如應用A的數據,存儲在A、B、C ,3臺機器上,如果3臺機器中,其中一臺出現問題,請求被處理到其他2臺機器上,如果加機器恢復,還需要從另外2臺機器上,Copy數據,又增加了這2臺機器的負擔。如果我們有應用A和應用B,各自有3臺機器,那麼我們可以把A應用分散在6臺機器上,B應用也分散在6臺機器上,可以實現相同的數據備份,但是應用存儲的數據被分散了。某臺機器損害,只是把該機器所承擔的負載平均分配到了,另外5臺機器上。恢復數據從5臺機器恢復,其速度快和給各臺服務器的壓力都不大,而且可以實現機器損害,更換完全不影響應用。

其原理是多個機器互為副本,是比較理想的實現負載分壓的方式。

7. 分佈式計算思想,移動數據不如移動計算,就進計算原則,減少跨進程、跨網絡、等跨度較大的實現,把計算所需的資源儘可能的靠近。因為可能出現網絡、遠程機器的瓶頸。

8. 常見分佈式系統數據分佈方式: GFS、HDFS:按數據量分佈;Map reduce 按GFS的數據分佈做本地化;BigTable、HBase按數據範圍分佈;Pnuts按哈希方式或者數據範圍分佈,可以選擇;Dynamo、Cassndra按一致性哈希;Mola、Armor、BigPipe按哈希方式分佈;Doris按哈希方式和按數據量分佈組合。

總結

以 上就是我對Java開發高併發使用及分佈式系統設計與原理分析問題及其優化總結,分享給大家,希望大家可以瞭解什麼是Java開發高併發使用及分佈式系統設計與原理分析問題及其優化。覺得收穫的話可以點個關注收藏轉發一波喔,謝謝大佬們支持!

1、多寫多敲代碼,好的代碼與紮實的基礎知識一定是實踐出來的

2、可以去百度搜索騰訊課堂圖靈學院的視頻來學習一下java架構實戰案例,還挺不錯的。

最後,每一位讀到這裡的網友,感謝你們能耐心地看完。希望在成為一名更優秀的Java程序員的道路上,我們可以一起學習、一起進步。

3丶想了解學習以上課程內容可加群:469717771 驗證碼(06 必過)

Java開發高併發使用及分佈式系統設計與原理分析

相關推薦

推薦中...