分布式系統(tǒng)和服務(wù)器集群
布式和集群是不?樣的,分布式?定是集群,但是集群不?定是分布式。
因為集群就是多個實例?起?作,分布式將?個系統(tǒng)拆分之后那就是多個實例;集群并不?定是分布式,因為復(fù)制型的集群不是拆分?是復(fù)制,沒有體現(xiàn)出整個系統(tǒng)的功能是分散在各個節(jié)點上實現(xiàn)的。



第一部分:一致性Hash算法
Hash算法,?如說在安全加密領(lǐng)域MD5、SHA等加密算法,在數(shù)據(jù)存儲和查找??有Hash表等, 以上都應(yīng)?到了Hash算法。
為什么需要使?Hash?
Hash算法較多的應(yīng)?在數(shù)據(jù)存儲和查找領(lǐng)域,最經(jīng)典的就是Hash表,它的查詢效率?常之?,其中的哈希算法如果設(shè)計的?較ok的話,那么Hash表的數(shù)據(jù)查詢時間復(fù)雜度可以接近于O(1)。
示例
需求:提供?組數(shù)據(jù) 1,5,7,6,3,4,8,對這組數(shù)據(jù)進(jìn)?存儲,然后隨便給定?個數(shù)n,請你判斷n是否存在于剛才的數(shù)據(jù)集中?
順序查找法:通過循環(huán)來完成,?較原始,效率也不?
public static void main(String[] args) {
int key = 5;
int a[] = new int[]{1,5,7,6,3,4,8};
for (int i = 0; i < a.length; i++) {
if(a[i]==5){
System.out.println("找到了,下標(biāo)="+i);
return;
}
}
}
二分查找:排序之后折半查找,相對于順序查找法會提??些效率,但是效率也并不是特別好
public static void main(String[] args) {
int key = 1;
int a[] = new int[]{1,3,4,5,6,7,8};
int height = a.length-1;
int low = 0;
while (height >= low){
int index = (height-low)/2+low;
System.out.println(index);
if(a[index] == key){
System.out.println("找到了,下標(biāo)="+index);
break;
}
if(a[index]>key){
height = index;
}
if(a[index]<key){
low = index+1;
}
}
}
直接尋址法:直接把數(shù)據(jù)和數(shù)組的下標(biāo)綁定到?起,查找的時候,直接array[n]就取出了數(shù)據(jù)。優(yōu)點:速度快,一次查找得到結(jié)果。缺點:只適用于關(guān)鍵字的全域比較小,而且沒有兩個元素的關(guān)鍵字完全相同。

public static void main(String[] args) {
int key = 3;
int a[] = new int[]{1,3,4,5,6,7,8};
int b[] = new int[1000];
for (int i = 0; i < a.length; i++) {
b[a[i]] = i+1;
}
if(b[key] == 0) {
System.out.println(key+"不存在");
} else {
System.out.println("找到了,下標(biāo)="+(b[key]-1));
}
}
除留余數(shù)法:可能會出現(xiàn)Hash沖突
public static void main(String[] args) {
int key = 7;
int a[] = new int[]{1,3,4,5,6,7,8};
int b[] = new int[5];
for (int i = 0; i < a.length; i++) {
int hashCode = a[i]%b.length;
b[hashCode] = i+1;
}
if(b[key%b.length] == 0) {
System.out.println(key+"不存在");
} else {
System.out.println("找到了,下標(biāo)="+(b[key%b.length]-1));
}
}
由此衍生出開放尋址法和拉鏈法。
開放地址法:
根據(jù)以上hash函數(shù)計算數(shù)組下標(biāo)時,當(dāng)遇到數(shù)據(jù)存放的沖突時就需要重新找到數(shù)組的其他位置。關(guān)于開放地址法通常需要有三種方法:線性探測、二次探測、再哈希法。
線性探測:
線性探測就是使用算術(shù)取余的方法計算余數(shù),當(dāng)產(chǎn)生沖突時就通過線性遞增的方法進(jìn)行探測,一直到數(shù)組的位置為空,插入數(shù)據(jù)項即可。

private static int b[] = new int[5];
public static void main(String[] args) {
int a[] = new int[]{1,3,4,5,6};
for (int i = 0; i < a.length; i++) {
int hashCode = getHashCode(a[i]);
b[hashCode] = i+1;
}
for (int i = 0; i < b.length; i++) {
System.out.println(b[i]);
}
}
private static int getHashCode(int i) {
int hashCode = i%b.length;
while(b[hashCode]!=0) {
hashCode++;
hashCode%=b.length;
}
return hashCode;
}
二次探測:
在線性探測過程中會產(chǎn)生數(shù)據(jù)聚集問題,當(dāng)數(shù)據(jù)聚集越來越大時,數(shù)據(jù)經(jīng)哈?;缶托枰逶诰奂暮蠖?。這樣會使得效率變得很低。二次探測是防止聚集產(chǎn)生的一種嘗試,相隔比較遠(yuǎn)的單元進(jìn)行探測,而不是線性一個個的探測。
二次探測是過程是x+1,x+4,x+9,以此類推。二次探測的步數(shù)是原始位置相隔的步數(shù)的平方。
從數(shù)組的x[0]開始探測,第一次探測x[1]不為空,繼續(xù)探測x[4],依然不為空繼續(xù)探測,x[9]不為空依次繼續(xù),直至不超過數(shù)組邊界。

再哈希法:
再哈希是把關(guān)鍵字用不同的哈希函數(shù)再做一遍哈希化,用這個結(jié)果作為步長,對指定的關(guān)鍵字,探測的步長是不變的,可以說不同的關(guān)鍵字可以使用不同的步長,并且步長可以控制。一般來說,再哈希函數(shù)可以采用以下這種:
stepSize=constant-(key%constant);
雖然不同的關(guān)鍵字可能會映射到相同的數(shù)組單元,但是可能會有不一樣的探測步長。假設(shè)可以使用步長1~5進(jìn)行探測。步長是不能為零的,不然就會形成死循環(huán)。再哈希的實現(xiàn)方式可以在線性探測的基礎(chǔ)上添加一個再哈希函數(shù)即可,對應(yīng)在delete和find方法里面修改相關(guān)操作。
//再哈希函數(shù)
public int hashFunction2(int key)
{
return 5-key%5;//此處設(shè)置為探測步長為1~5
}
...
//在delete方法里添加以下語句
int stepSize=hashFunction2(key);
...
//將++hahshVal替換成以下
hashVal+=stepSize;
...
拉鏈地址法:
把具有相同散列地址的關(guān)鍵字(同義詞)值放在同一個單鏈表中,稱為同義詞鏈表。
有m個散列地址就有m個鏈表,同時用指針數(shù)組A[0,1,2…m-1]存放各個鏈表的頭指針,凡是散列地址為i的記錄都以結(jié)點方式插入到以A[i]為指針的單鏈表中。A中各分量的初值為空指針。當(dāng)新來的元素映射到?jīng)_突的數(shù)組位置時,就會插入到鏈表的頭部。

簡單總結(jié)
所以,Hash表的查詢效率?不?取決于Hash算法,hash算法能夠讓數(shù)據(jù)平均分布,既能夠節(jié)省空間?能提?查詢效率。hashcode其實也是通過?個Hash算法得來的。
第 1 節(jié) Hash算法應(yīng)?場景
Hash算法在分布式集群架構(gòu)中的應(yīng)?場景
Hash算法在很多分布式集群產(chǎn)品中都有應(yīng)?,?如分布式集群架構(gòu)Redis、Hadoop、ElasticSearch,Mysql分庫分表,Nginx負(fù)載均衡等
主要的應(yīng)?場景歸納起來兩個
- 請求的負(fù)載均衡(?如nginx的ip_hash策略)
Nginx的IP_hash策略可以在客戶端ip不變的情況下,將其發(fā)出的請求始終路由到同?個?標(biāo)服務(wù)器上,實現(xiàn)會話粘滯,避免處理session共享問題。我們可以對ip地址或者sessionid進(jìn)?計算哈希值,哈希值與服務(wù)器數(shù)量進(jìn)?取模運(yùn)算,得到的值就是當(dāng)前請求應(yīng)該被路由到的服務(wù)器編號,如此,同?個客戶端ip發(fā)送過來的請求就可以路由到同?個?標(biāo)服務(wù)器,實現(xiàn)會話粘滯。 - 分布式存儲
以分布式內(nèi)存數(shù)據(jù)庫Redis為例,集群中有redis1,redis2,redis3 三臺Redis服務(wù)器
那么,在進(jìn)?數(shù)據(jù)存儲時,<key1,value1>數(shù)據(jù)存儲到哪個服務(wù)器當(dāng)中呢?針對key進(jìn)?hash處理hash(key1)%3=index, 使?余數(shù)index鎖定存儲的具體服務(wù)器節(jié)點。
第 2 節(jié) 普通Hash算法存在的問題
普通Hash算法存在?個問題,以ip_hash為例,假定下載?戶ip固定沒有發(fā)?改變,現(xiàn)在tomcat3出現(xiàn)了問題,down機(jī)了,服務(wù)器數(shù)量由3個變?yōu)榱?個,之前所有的求模都需要重新計算。

如果在真實?產(chǎn)情況下,后臺服務(wù)器很多臺,客戶端也有很多,那么影響是很?的,縮容和擴(kuò)容都會存在這樣的問題,?量?戶的請求會被路由到其他的?標(biāo)服務(wù)器處理,?戶在原來服務(wù)器中的會話都會丟失。
第 3 節(jié) ?致性Hash算法
?致性哈希算法思路如下:

?先有?條直線,直線開頭和結(jié)尾分別定為為1和2的32次?減1,這相當(dāng)于?個地址,對于這樣?條線,彎過來構(gòu)成?個圓環(huán)形成閉環(huán),這樣的?個圓環(huán)稱為hash環(huán)。我們把服務(wù)器的ip或者主機(jī)名求hash值然后對應(yīng)到hash環(huán)上,那么針對客戶端?戶,也根據(jù)它的ip進(jìn)?hash求值,對應(yīng)到環(huán)上某個位置,然后如何確定?個客戶端路由到哪個服務(wù)器處理呢?按照順時針?向找最近的服務(wù)器節(jié)點

假如將服務(wù)器3下線,服務(wù)器3下線后,原來路由到3的客戶端重新路由到服務(wù)器4,對于其他客戶端沒有影響只是這??部分受影響(請求的遷移達(dá)到了最?,這樣的算法對分布式集群來說?常合適的,避免了?量請求遷移 )

增加服務(wù)器5之后,原來路由到3的部分客戶端路由到新增服務(wù)器5上,對于其他客戶端沒有影響只是這??部分受影響(請求的遷移達(dá)到了最?,這樣的算法對分布式集群來說?常合適的,避免了?量請求遷移 )

1)如前所述,每?臺服務(wù)器負(fù)責(zé)?段,?致性哈希算法對于節(jié)點的增減都只需重定位環(huán)空間中的一小部分?jǐn)?shù)據(jù),具有較好的容錯性和可擴(kuò)展性。但是,?致性哈希算法在服務(wù)節(jié)點太少時,容易因為節(jié)點分部不均勻?造成數(shù)據(jù)傾斜問題。例如系統(tǒng)中只有兩臺服務(wù)器,其環(huán)分布如下,節(jié)點2只能負(fù)責(zé)?常?的?段,?量的客戶端請求落在了節(jié)點1上,這就是數(shù)據(jù)(請求)傾斜問題
2)為了解決這種數(shù)據(jù)傾斜問題,?致性哈希算法引?了虛擬節(jié)點機(jī)制,即對每?個服務(wù)節(jié)點計算多個哈希,每個計算結(jié)果位置都放置?個此服務(wù)節(jié)點,稱為虛擬節(jié)點。具體做法可以在服務(wù)器ip或主機(jī)名的后?增加編號來實現(xiàn)。比如,可以為每臺服務(wù)器計算三個虛擬節(jié)點,于是可以分別計算 “節(jié)點1的ip#1”、“節(jié)點1的ip#2”、“節(jié)點1的ip#3”、“節(jié)點2的ip#1”、“節(jié)點2的ip#2”、“節(jié)點2的ip#3”的哈希值,于是形成六個虛擬節(jié)點,當(dāng)客戶端被路由到虛擬節(jié)點的時候其實是被路由到該虛擬節(jié)點所對應(yīng)的真實節(jié)點

第 4 節(jié) ?寫實現(xiàn)?致性Hash算法
- 普通Hash算法
public static void main(String[] args) {
// 客戶端ip地址
String[] ipAddressList = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
// 服務(wù)器數(shù)量
int serverCount = 5;
for (int i = 0; i < ipAddressList.length; i++) {
int hashCode = Math.abs(ipAddressList[i].hashCode());
int index = hashCode%serverCount;
System.out.println("客戶端:" + ipAddressList[i] + " 被路由到服務(wù)器編號為:" + index);
}
}
- ?致性Hash算法實現(xiàn)(不含虛擬節(jié)點)
public static void main(String[] args) {
// 服務(wù)器節(jié)點
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
// 服務(wù)器地址
String[] tomcatServers = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
// 將服務(wù)器放入節(jié)點
for (String tomcatServer : tomcatServers) {
int hashCode = Math.abs(tomcatServer.hashCode());
hashServerMap.put(hashCode, tomcatServer);
System.out.println("服務(wù)器"+tomcatServer+"綁定到節(jié)點"+hashCode);
}
// 客戶端ip地址
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
// 服務(wù)器數(shù)量
for (String client : clients) {
int hashcode = Math.abs(client.hashCode());
// 順時針,往前找。大于等于hashcode的服務(wù)器節(jié)點全找出來
SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(hashcode);
Integer firstKey = null;
if(integerStringSortedMap.isEmpty()){
// 往前找不到,就拿第一臺機(jī)器。如同一個閉環(huán)
firstKey = hashServerMap.firstKey();
} else {
firstKey = integerStringSortedMap.firstKey();
}
System.out.println("==========>>>>客戶端:" + client+"("+ hashcode + ") 被路由到服務(wù)器:" + hashServerMap.get(firstKey));
}
}

- ?致性Hash算法實現(xiàn)(含虛擬節(jié)點)
public static void main(String[] args) {
// 服務(wù)器節(jié)點
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
// 定義針對每個真實服務(wù)器虛擬出來?個節(jié)點
int virtualCount = 3;
// 服務(wù)器地址
String[] tomcatServers = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
// 將服務(wù)器放入節(jié)點
for (String tomcatServer : tomcatServers) {
int hashCode = Math.abs(tomcatServer.hashCode());
hashServerMap.put(hashCode, tomcatServer);
System.out.println("服務(wù)器"+tomcatServer+"綁定到節(jié)點"+hashCode);
for (int i = 0; i < virtualCount; i++) {
int virtualHashCode = Math.abs((tomcatServer+"#"+i).hashCode());
System.out.println("服務(wù)器"+tomcatServer+"綁虛擬節(jié)點"+virtualHashCode);
}
}
// 客戶端ip地址
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
// 服務(wù)器數(shù)量
for (String client : clients) {
int hashcode = Math.abs(client.hashCode());
// 順時針,往前找。大于等于hashcode的服務(wù)器節(jié)點全找出來
SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(hashcode);
Integer firstKey = null;
if(integerStringSortedMap.isEmpty()){
// 往前找不到,就拿第一臺機(jī)器。如同一個閉環(huán)
firstKey = hashServerMap.firstKey();
} else {
firstKey = integerStringSortedMap.firstKey();
}
System.out.println("==========>>>>客戶端:" + client+"("+ hashcode + ") 被路由到服務(wù)器:" + hashServerMap.get(firstKey));
}
}
第 5 節(jié) Nginx 配置?致性Hash負(fù)載均衡策略
ngx_http_upstream_consistent_hash 模塊是?個負(fù)載均衡器,使??個內(nèi)部?致性hash算法來選擇合適的后端節(jié)點。
該模塊可以根據(jù)配置參數(shù)采取不同的?式將請求均勻映射到后端機(jī)器,
consistent_hash $remote_addr:可以根據(jù)客戶端ip映射
consistent_hash $request_uri:根據(jù)客戶端請求的uri映射
consistent_hash $args:根據(jù)客戶端攜帶的參數(shù)進(jìn)?映
ngx_http_upstream_consistent_hash 模塊是?個第三?模塊,需要我們下載安裝后使?
1)github下載nginx?致性hash負(fù)載均衡模塊 https://github.com/replay/ngx_http_consistent_hash

2)將下載的壓縮包上傳到nginx服務(wù)器,并解壓
3)我們已經(jīng)編譯安裝過nginx,此時進(jìn)?當(dāng)時nginx的源碼?錄,執(zhí)?如下命令
./configure —add-module=/root/ngx_http_consistent_hash-master
make
make install
4)Nginx就可以使?啦,在nginx.conf?件中配置

小貼士
哈希表的數(shù)組容量為什么最好是質(zhì)數(shù)
答:好的HASH函數(shù)需要把原始數(shù)據(jù)均勻地分布到HASH數(shù)組里
用取余做HASH函數(shù)的情況,原始數(shù)據(jù)不大會是真正的隨機(jī)的,可能有某些規(guī)律,比如大部分是偶數(shù),這時候如果HASH數(shù)組容量是偶數(shù),容易使原始數(shù)據(jù)HASH后不會均勻分布。
比如 2 4 6 8 10 12這6個數(shù),如果對 6 取余 得到 2 4 0 2 4 0 只會得到3種HASH值,沖突會很多
如果對 7 取余 得到 2 4 6 1 3 5 得到6種HASH值,沒有沖突
同樣地,如果數(shù)據(jù)都是3的倍數(shù),而HASH數(shù)組容量是3的倍數(shù),HASH后也容易有沖突
用一個質(zhì)數(shù)則會減少沖突的概率,因為質(zhì)數(shù)只能被1和自身整除。比如長度為11,取模,11以內(nèi)就不會有索引沖突;但是如果長度是12,取模,12以內(nèi)2、3、4、6就會出現(xiàn)索引沖突。關(guān)于裝填因子
hash表中的數(shù)據(jù)項和表長的比例叫做裝填因子。當(dāng)裝填因子不太大時,聚集分布就會比較連貫。此時hash表肯定是某一個部分產(chǎn)生大量聚集,另外一部分還很稀疏。聚集會降低hash表的性能。二次探測產(chǎn)生的聚集
二次探測可以消除在線性探測中產(chǎn)生的聚集問題,但是二次探測還是會產(chǎn)生一種更明確更細(xì)的聚集。二次聚集的產(chǎn)生是在二次探測的基礎(chǔ)上產(chǎn)生的現(xiàn)象。例如N個數(shù)據(jù)經(jīng)hash函數(shù)計算后都映射到到數(shù)組下標(biāo)10,探測第二個數(shù)字需要以一步長,第三個數(shù)字需要以4步長為單位,第四個數(shù)字則需要以九為步長。好在二次探測并不常用,解決聚集問題還是有一種更好的辦法:再哈希法。
第二部分:集群時鐘同步問題
第 1 節(jié) 時鐘不同步導(dǎo)致的問題
時鐘此處指服務(wù)器時間,如果集群中各個服務(wù)器時鐘不?致勢必導(dǎo)致?系列問題,試想 “集群是各個服務(wù)器?起團(tuán)隊化作戰(zhàn),?家?作都不在?個點上,豈不亂了套!”
舉?個例?,電商?站業(yè)務(wù)中,新增?條訂單,那么勢必會在訂單表中增加了?條記錄,該條記錄中應(yīng)該會有“下單時間”這樣的字段,往往我們會在程序中獲取當(dāng)前系統(tǒng)時間插?到數(shù)據(jù)庫或者直接從數(shù)據(jù)庫服務(wù)器獲取時間。那我們的訂單?系統(tǒng)是集群化部署,或者我們的數(shù)據(jù)庫也是分庫分表的集群化部署,然?他們的系統(tǒng)時鐘缺不?致,?如有?臺服務(wù)器的時間是昨天,那么這個時候下單時間就成了昨天,那我們的數(shù)據(jù)將會混亂!如下

第 2 節(jié) 集群時鐘同步配置
-
集群時鐘同步思路
分布式集群中各個服務(wù)器節(jié)點都可以連接互聯(lián)網(wǎng)
image.png
分布式集群中某?個服務(wù)器節(jié)點可以訪問互聯(lián)?或者所有節(jié)點都不能夠訪問互聯(lián)?

操作?式:
選取集群中的?個服務(wù)器節(jié)點A(172.17.0.17)作為時間服務(wù)器(整個集群時間從這臺服務(wù)器同步,如果這臺服務(wù)器能夠訪問互聯(lián)?,可以讓這臺服務(wù)器和?絡(luò)時間保持同步,如果不能就?動設(shè)置?個時間)
1)?先設(shè)置好A的時間
把A配置為時間服務(wù)器(修改/etc/ntp.conf?件)
1、如果有 restrict default ignore,注釋掉它
2、添加如下??內(nèi)容
# 放開局域?同步功能,172.17.0.0是你的局域??段
restrict 172.17.0.0 mask 255.255.255.0 nomodify notrap
# local clock
server 127.127.1.0
fudge 127.127.1.0 stratum 10
3、重啟?效并配置ntpd服務(wù)開機(jī)?啟動
service ntpd restart
chkconfig ntpd on
2)集群中其他節(jié)點就可以定時從A服務(wù)器同步時間了
ntpdate 172.17.0.17
小貼士
- ?絡(luò)時間同步命令
Linux有定時任務(wù),crond,可以使?linux的定時任務(wù),每隔10分鐘執(zhí)??次ntpdate命令
#使? ntpdate ?絡(luò)時間同步命令
ntpdate -u ntp.api.bz #從?個時間服務(wù)器同步時間
第三部分:分布式ID解決方案
為什么需要分布式ID(分布式集群環(huán)境下的全局唯?ID)

解決方案
- UUID(可以?)
UUID 是指Universally Unique Identifier,翻譯為中?是通?唯?識別碼。產(chǎn)?重復(fù) UUID 并造成錯誤的情況?常低,是故?可不必考慮此問題。Java中得到?個UUID,可以使?java.util包提供的?法
public static void main(String[] args) {
// ex:92bb507d-8d8c-4ca2-a0bf-b7fe24ec2e32
System.out.println(java.util.UUID.randomUUID().toString());
}
- 獨?數(shù)據(jù)庫的?增ID(不建議用)
?如A表分表為A1表和A2表,那么肯定不能讓A1表和A2表的ID?增,那么ID怎么獲取呢?我們可以單獨的創(chuàng)建?個Mysql數(shù)據(jù)庫,在這個數(shù)據(jù)庫中創(chuàng)建?張表,這張表的ID設(shè)置為?增,其他地?需要全局唯?ID的時候,就模擬向這個Mysql數(shù)據(jù)庫的這張表中模擬插??條記錄,此時ID會?增,然后我們可以通過Mysql的select last_insert_id() 獲取到剛剛這張表中?增?成的ID.
?如,我們創(chuàng)建了?個數(shù)據(jù)庫實例global_id_generator,在其中創(chuàng)建了?個數(shù)據(jù)表,表結(jié)構(gòu)如下
-- ----------------------------
-- Table structure for DISTRIBUTE_ID
-- ----------------------------
DROP TABLE IF EXISTS `DISTRIBUTE_ID`;
CREATE TABLE `DISTRIBUTE_ID` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`createtime` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
當(dāng)分布式集群環(huán)境中哪個應(yīng)?需要獲取?個全局唯?的分布式ID的時候,就可以使?代碼連接這個
數(shù)據(jù)庫實例,執(zhí)?如下sql語句即可。
insert into DISTRIBUTE_ID(createtime) values(NOW());
select LAST_INSERT_ID();
注意:
1)這?的createtime字段?實際意義,是為了隨便插??條數(shù)據(jù)以?于能夠?增id。
2)使?獨?的Mysql實例?成分布式id,雖然可?,但是性能和可靠性都不夠好,因為你需要代碼連接到數(shù)據(jù)庫才能獲取到id,性能?法保障,另外mysql數(shù)據(jù)庫實例掛掉了,那么就?法獲取分布式id了。
3)有?些開發(fā)者?針對上述的情況將?于?成分布式id的mysql數(shù)據(jù)庫設(shè)計成了?個集群架構(gòu),那么其實這種?式現(xiàn)在基本不?,因為過于麻煩了。
-
SnowFlake 雪花算法(可以?,推薦)
雪花算法是Twitter推出的?個?于?成分布式ID的策略。
雪花算法是?個算法,基于這個算法可以?成ID,?成的ID是?個long型,那么在Java中?個long型是8個字節(jié),算下來是64bit,如下是使?雪花算法?成的?個ID的?進(jìn)制形式示意:
雪花算法示意圖 - 第一個部分,是 1 個 bit:0,這個是無意義的。
因為二進(jìn)制里第一個 bit 為如果是 1,那么都是負(fù)數(shù),但是我們生成的 id 都是正數(shù),所以第一個 bit 統(tǒng)一都是 0。 - 第二個部分是 41 個 bit:表示的是時間戳。
41 bit 可以表示的數(shù)字多達(dá) 2^41 - 1,也就是可以標(biāo)識 2 ^ 41 - 1 個毫秒值,換算成年就是表示 69 年的時間。 - 第三個部分是 5 個 bit:表示的是機(jī)房 id,10001。
最多代表 2 ^ 5 個機(jī)房(32 個機(jī)房)。 - 第四個部分是 5 個 bit:表示的是機(jī)器 id,1 1001。
每個機(jī)房里可以代表 2 ^ 5 個機(jī)器(32 臺機(jī)器) - 第五個部分是 12 個 bit:表示的序號,就是某個機(jī)房某臺機(jī)器上這一毫秒內(nèi)同時生成的 id 的序號,0000 00000000。
12 bit 可以代表的最大正整數(shù)是 2 ^ 12 - 1 = 4096,也就是說可以用這個 12 bit 代表的數(shù)字來區(qū)分同一個毫秒內(nèi)的 4096 個不同的 id。
java實現(xiàn)示例
public class IdWorker {
//因為二進(jìn)制里第一個 bit 為如果是 1,那么都是負(fù)數(shù),但是我們生成的 id 都是正數(shù),所以第一個 bit 統(tǒng)一都是 0。
//機(jī)器ID 2進(jìn)制5位 32位減掉1位 31個
private long workerId;
//機(jī)房ID 2進(jìn)制5位 32位減掉1位 31個
private long datacenterId;
//代表一毫秒內(nèi)生成的多個id的最新序號 12位 4096 -1 = 4095 個
private long sequence;
//設(shè)置一個時間初始值 2^41 - 1 差不多可以用69年
private long twepoch = 1585644268888L;
//5位的機(jī)器id
private long workerIdBits = 5L;
//5位的機(jī)房id
private long datacenterIdBits = 5L;
//每毫秒內(nèi)產(chǎn)生的id數(shù) 2 的 12次方
private long sequenceBits = 12L;
// 這個是二進(jìn)制運(yùn)算,就是5 bit最多只能有31個數(shù)字,也就是說機(jī)器id最多只能是32以內(nèi)
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 這個是一個意思,就是5 bit最多只能有31個數(shù)字,機(jī)房id最多只能是32以內(nèi)
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//記錄產(chǎn)生時間毫秒數(shù),判斷是否是同1毫秒
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId() {
return datacenterId;
}
public long getTimestamp() {
return System.currentTimeMillis();
}
public IdWorker(long workerId, long datacenterId, long sequence) {
// 檢查機(jī)房id和機(jī)器id是否超過31 不能小于0
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(
String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(
String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
// 這個是核心方法,通過調(diào)用nextId()方法,讓當(dāng)前這臺機(jī)器上的snowflake算法程序生成一個全局唯一的id
public synchronized long nextId() {
// 這兒就是獲取當(dāng)前時間戳,單位是毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
System.err.printf(
"clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
// 下面是說假設(shè)在同一個毫秒內(nèi),又發(fā)送了一個請求生成一個id
// 這個時候就得把seqence序號給遞增1,最多就是4096
if (lastTimestamp == timestamp) {
// 這個意思是說一個毫秒內(nèi)最多只能有4096個數(shù)字,無論你傳遞多少進(jìn)來,
//這個位運(yùn)算保證始終就是在4096這個范圍內(nèi),避免你自己傳遞個sequence超過了4096這個范圍
sequence = (sequence + 1) & sequenceMask;
//當(dāng)某一毫秒的時間,產(chǎn)生的id數(shù) 超過4095,系統(tǒng)會進(jìn)入等待,直到下一毫秒,系統(tǒng)繼續(xù)產(chǎn)生ID
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
// 這兒記錄一下最近一次生成id的時間戳,單位是毫秒
lastTimestamp = timestamp;
// 這兒就是最核心的二進(jìn)制位運(yùn)算操作,生成一個64bit的id
// 先將當(dāng)前時間戳左移,放到41 bit那兒;將機(jī)房id左移放到5 bit那兒;將機(jī)器id左移放到5 bit那兒;將序號放最后12 bit
// 最后拼接起來成一個64 bit的二進(jìn)制數(shù)字,轉(zhuǎn)換成10進(jìn)制就是個long型
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) | sequence;
}
/**
* 當(dāng)某一毫秒的時間,產(chǎn)生的id數(shù) 超過4095,系統(tǒng)會進(jìn)入等待,直到下一毫秒,系統(tǒng)繼續(xù)產(chǎn)生ID
* @param lastTimestamp
* @return
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//獲取當(dāng)前時間戳
private long timeGen(){
return System.currentTimeMillis();
}
/**
* main 測試類
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 22; i++) {
System.out.println(worker.nextId());
}
}
}
另外,?切互聯(lián)?公司也基于上述的?案封裝了?些分布式ID?成器,?如滴滴的tinyid(基于數(shù)據(jù)庫實現(xiàn))、百度的uidgenerator(基于SnowFlake)和美團(tuán)的leaf(基于數(shù)據(jù)庫和SnowFlake)等。
- 借助Redis的Incr命令獲取全局唯?ID(推薦)
Redis Incr 命令將 key 中儲存的數(shù)字值增?。如果 key 不存在,那么 key 的值會先被初始化為 0,然后再執(zhí)? INCR 操作。
127.0.0.1:6379> incr id
(integer) 1
127.0.0.1:6379> incr id
(integer) 2
第四部分:分布式調(diào)度問題(定時任務(wù)的分布式)
調(diào)度—>定時任務(wù),分布式調(diào)度—>在分布式集群環(huán)境下定時任務(wù)這件事
Elastic-job(當(dāng)當(dāng)?開源的分布式調(diào)度框架)
第 1 節(jié) 定時任務(wù)的場景
定時任務(wù)形式:每隔?定時間/特定某?時刻執(zhí)?
例如:
- 訂單審核、出庫
- 訂單超時?動取消、?付退款
- 禮券同步、?成、發(fā)放作業(yè)
- 物流信息推送、抓取作業(yè)、退換貨處理作業(yè)
- 數(shù)據(jù)積壓監(jiān)控、?志監(jiān)控、服務(wù)可?性探測作業(yè)
- 定時備份數(shù)據(jù)
- ?融系統(tǒng)每天的定時結(jié)算
- 數(shù)據(jù)歸檔、清理作業(yè)
- 報表、離線數(shù)據(jù)分析作業(yè)
第 2 節(jié) 什么是分布式調(diào)度
什么是分布式任務(wù)調(diào)度?有兩層含義
1)運(yùn)?在分布式集群環(huán)境下的調(diào)度任務(wù)。同?個定時任務(wù)程序部署多份,只應(yīng)該有?個定時任務(wù)在執(zhí)?。
2)分布式調(diào)度—>定時任務(wù)的分布式—>定時任務(wù)的拆分(即為把?個?的作業(yè)任務(wù)拆分為多個?的作業(yè)任務(wù),同時執(zhí)?)

第 3 節(jié) 定時任務(wù)與消息隊列的區(qū)別
共同點
- 異步處理
?如注冊、下單事件 - 應(yīng)?解耦
不管定時任務(wù)作業(yè)還是MQ都可以作為兩個應(yīng)?之間的?輪實現(xiàn)應(yīng)?解耦,這個?輪可以中轉(zhuǎn)數(shù)據(jù),當(dāng)然單體服務(wù)不需要考慮這些,服務(wù)拆分的時候往往都會考慮 - 流量削峰
雙??的時候,任務(wù)作業(yè)和MQ都可以?來扛流量,后端系統(tǒng)根據(jù)服務(wù)能?定時處理訂單或者從MQ抓取訂單抓取到?個訂單到來事件的話觸發(fā)處理,對于前端?戶來說看到的結(jié)果是已經(jīng)下單成功了,下單是不受任何影響的
不同點
- 定時任務(wù)作業(yè)是時間驅(qū)動,?MQ是事件驅(qū)動;
時間驅(qū)動是不可代替的,?如?融系統(tǒng)每?的利息結(jié)算,不是說利息來?條(利息到來事件)就算?下,?往往是通過定時任務(wù)批量計算;
所以,定時任務(wù)作業(yè)更傾向于批處理,MQ傾向于逐條處理;
第 4 節(jié) 定時任務(wù)的實現(xiàn)?式
定時任務(wù)的實現(xiàn)?式有多種。早期沒有定時任務(wù)框架的時候,我們會使?JDK中的Timer機(jī)制和多線程機(jī)制(Runnable+線程休眠)來實現(xiàn)定時或者間隔?段時間執(zhí)?某?段程序;后來有了定時任務(wù)框架,比如?名鼎鼎的Quartz任務(wù)調(diào)度框架,使?時間表達(dá)式(包括:秒、分、時、?、周、年)配置某?個任務(wù)什么時間去執(zhí)行:
- 引入依賴
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
- 定義?個job,需實現(xiàn)Job接?
public class DemoJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("我是?個定時任務(wù)邏輯");
}
}
- 定時任務(wù)作業(yè)主調(diào)度程序
public class QuartzMain {
// 創(chuàng)建作業(yè)任務(wù)調(diào)度器(類似于公交調(diào)度站)
public static Scheduler createScheduler() throws SchedulerException {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
return scheduler;
}
// 創(chuàng)建?個作業(yè)任務(wù)(類似于?輛公交?)
public static JobDetail createJob() {
JobBuilder jobBuilder = JobBuilder.newJob(DemoJob.class);
jobBuilder.withIdentity("jobName", "myJob");
JobDetail jobDetail = jobBuilder.build();
return jobDetail;
}
/**
* 創(chuàng)建作業(yè)任務(wù)時間觸發(fā)器(類似于公交?出?時間表)
* cron表達(dá)式由七個位置組成,空格分隔
* 1、Seconds(秒) 0~59
* 2、Minutes(分) 0~59
* 3、Hours(?時) 0~23
* 4、Day of Month(天)1~31,注意有的?份不?31天
* 5、Month(?) 0~11,或者
* JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC
* 6、Day of Week(周) 1~7,1=SUN或者 SUN,MON,TUE,WEB,THU,FRI,SAT
* 7、Year(年)1970~2099 可選項
* 示例:
* 0 0 11 * * ? 每天的11點觸發(fā)執(zhí)??次
* 0 30 10 1 * ? 每?1號上午10點半觸發(fā)執(zhí)??次
*/
public static Trigger createTrigger() {
// 創(chuàng)建時間觸發(fā)器,按?歷調(diào)度
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("triggerName", "myTrigger")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ? "))
.build();
// 創(chuàng)建觸發(fā)器,按簡單間隔調(diào)度
/*SimpleTrigger trigger1 = TriggerBuilder.newTrigger()
.withIdentity("triggerName","myTrigger")
.startNow()
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(3)
.repeatForever())
.build();*/
return trigger;
}
// 定時任務(wù)作業(yè)主調(diào)度程序
public static void main(String[] args) throws SchedulerException {
// 創(chuàng)建?個作業(yè)任務(wù)調(diào)度器(類似于公交調(diào)度站)
Scheduler scheduler = QuartzMain.createScheduler();
// 創(chuàng)建?個作業(yè)任務(wù)(類似于?輛公交?)
JobDetail job = QuartzMain.createJob();
// 創(chuàng)建?個作業(yè)任務(wù)時間觸發(fā)器(類似于公交?出?時間表)
Trigger trigger = QuartzMain.createTrigger();
// 使?調(diào)度器按照時間觸發(fā)器執(zhí)?這個作業(yè)任務(wù)
scheduler.scheduleJob(job, trigger);
scheduler.start();
}
}
以上,是回顧?下任務(wù)調(diào)度框架Quartz的?致?法,那么在分布式架構(gòu)環(huán)境中使?Quartz已經(jīng)不能更好的滿?我們需求,我們可以使?專業(yè)的分布式調(diào)度框架,這?我們推薦使?Elastic-job和XXL-JOB。
第 5 節(jié) 分布式調(diào)度框架Elastic-Job
5.1 Elastic-Job介紹
Elastic-Job是當(dāng)當(dāng)?開源的?個分布式調(diào)度解決?案,基于Quartz?次開發(fā)的,由兩個相互獨?的?項?Elastic-Job-Lite和Elastic-Job-Cloud組成。我們要學(xué)習(xí)的是 Elastic-Job-Lite,它定位為輕量級無中心化解決?案,使用Jar包的形式提供分布式任務(wù)的協(xié)調(diào)服務(wù),而Elastic-Job-Cloud?項?需要結(jié)合Mesos以及Docker在云環(huán)境下使用。
Elastic-Job的github地址:https://github.com/elasticjob
主要功能介紹
- 彈性調(diào)度
- 支持任務(wù)在分布式場景下的分片和高可用
- 能夠水平擴(kuò)展任務(wù)的吞吐量和執(zhí)行效率
- 任務(wù)處理能力隨資源配備彈性伸縮
- 資源分配
- 在適合的時間將適合的資源分配給任務(wù)并使其生效
- 相同任務(wù)聚合至相同的執(zhí)行器統(tǒng)一處理
- 動態(tài)調(diào)配追加資源至新分配的任務(wù)
- 作業(yè)治理
- 失效轉(zhuǎn)移
- 錯過作業(yè)重新執(zhí)行
- 自診斷修復(fù)
- 作業(yè)依賴(TODO)
- 基于有向無環(huán)圖(DAG)的作業(yè)間依賴
- 基于有向無環(huán)圖(DAG)的作業(yè)分片間依賴
- 作業(yè)開放生態(tài)
- 可擴(kuò)展的作業(yè)類型統(tǒng)一接口
- 豐富的作業(yè)類型庫,如數(shù)據(jù)流、腳本、HTTP、文件、大數(shù)據(jù)等
- 易于對接業(yè)務(wù)作業(yè),能夠與 Spring 依賴注入無縫整合
- 可視化管控端
- 作業(yè)管控端
- 作業(yè)執(zhí)行歷史數(shù)據(jù)追蹤
- 注冊中心管理
快速入門
- 引入依賴
<dependency>
<groupId>org.apache.shardingsphere.elasticjob</groupId>
<artifactId>elasticjob-lite-core</artifactId>
<version>${latest.release.version}</version>
</dependency>
- 作業(yè)開發(fā)
public class MyJob implements SimpleJob {
@Override
public void execute(ShardingContext context) {
switch (context.getShardingItem()) {
case 0:
// do something by sharding item 0
break;
case 1:
// do something by sharding item 1
break;
case 2:
// do something by sharding item 2
break;
// case n: ...
}
}
}
- 作業(yè)配置
JobConfiguration jobConfig = JobConfiguration.newBuilder("MyJob", 3).cron("0/5 * * * * ?").build();
- 作業(yè)調(diào)度
public class MyJobDemo {
public static void main(String[] args) {
new ScheduleJobBootstrap(createRegistryCenter(), new MyJob(), createJobConfiguration()).schedule();
}
private static CoordinatorRegistryCenter createRegistryCenter() {
CoordinatorRegistryCenter regCenter = new ZookeeperRegistryCenter(new ZookeeperConfiguration("zk_host:2181", "my-job"));
regCenter.init();
return regCenter;
}
private static JobConfiguration createJobConfiguration() {
// 創(chuàng)建作業(yè)配置
// ...
}
}
測試
1)可先啟動?個進(jìn)程,然后再啟動?個進(jìn)程(兩個進(jìn)程模擬分布式環(huán)境下,通?個定時任務(wù)部署了兩份在?作)。
2)兩個進(jìn)程逐個啟動,觀察現(xiàn)象。
3)關(guān)閉其中執(zhí)?的進(jìn)程,觀察現(xiàn)象。
Leader節(jié)點選舉機(jī)制
每個Elastic-Job的任務(wù)執(zhí)?實例App作為Zookeeper的客戶端來操作ZooKeeper的znode
(1)多個實例同時創(chuàng)建/leader節(jié)點。
(2)/leader節(jié)點只能創(chuàng)建?個,后創(chuàng)建的會失敗,創(chuàng)建成功的實例會被選為leader節(jié)點,執(zhí)?任務(wù)。
5.4 Elastic-Job-Lite輕量級去中心化的特點
如何理解輕量級和去中?化?

5.5 任務(wù)分片
?個?的?常耗時的作業(yè)Job,比如:?次要處理?億的數(shù)據(jù),那這?億的數(shù)據(jù)存儲在數(shù)據(jù)庫中,如果用?個作業(yè)節(jié)點處理?億數(shù)據(jù)要很久,在互聯(lián)?領(lǐng)域是不太能接受的,互聯(lián)?領(lǐng)域更希望機(jī)器的增加去橫向擴(kuò)展處理能?。所以,ElasticJob可以把作業(yè)分為多個的task(每?個task就是?個任務(wù)分片),每?個task交給具體的?個機(jī)器實例去處理(?個機(jī)器實例是可以處理多個task的),但是具體每個task執(zhí)行什么邏輯由我們自己來指定。

Strategy策略定義這些分?項怎么去分配到各個機(jī)器上去,默認(rèn)是平均去分,可以定制, 比如某?個機(jī)器負(fù)載 比較高或者預(yù)配置比較高,那么就可以寫策略。分片和作業(yè)本身是通過?個注冊中心協(xié)調(diào)的,因為在分布式環(huán)境下,狀態(tài)數(shù)據(jù)肯定集中到?點,才可以在分布式中溝通。

5.6 彈性擴(kuò)容

新增加?個運(yùn)?實例app3,它會?動注冊到注冊中?,注冊中?發(fā)現(xiàn)新的服務(wù)上線,注冊中?會通知ElasticJob 進(jìn)?重新分?。
注意:
1)分片項也是?個JOB配置,修改配置,重新分片,在下?次定時運(yùn)行之前會重新調(diào)用分片算法,那么這個分片算法的結(jié)果就是:哪臺機(jī)器運(yùn)?哪?個?片,這個結(jié)果存儲到zk中的,主節(jié)點會把分片給分好放到注冊中心去,然后執(zhí)行節(jié)點從注冊中心獲取信息(執(zhí)行節(jié)點在定時任務(wù)開啟的時候獲取相應(yīng)的分片)。
2)如果所有的節(jié)點掛掉值剩下?個節(jié)點,所有分片都會指向剩下的?個節(jié)點,這也是ElasticJob的高可用。
第五部分:Session共享(一致性)問題
Session共享及Session保持或者叫做Session?致性

第 1 節(jié) Session問題原因分析
出現(xiàn)這個問題的原因,從根本上來說是因為Http協(xié)議是?狀態(tài)的協(xié)議??蛻舳撕头?wù)端在某次會話中產(chǎn)?的數(shù)據(jù)不會被保留下來,所以第?次請求服務(wù)端?法認(rèn)識到你曾經(jīng)來過, Http為什么要設(shè)計為?狀態(tài)協(xié)議?早期都是靜態(tài)???所謂有?狀態(tài),后來有動態(tài)的內(nèi)容更豐富,就需要有狀態(tài),出現(xiàn)了兩種?于保持Http狀態(tài)的技術(shù),那就是Cookie和Session。?出現(xiàn)上述不停讓登錄的問題,分析如下圖:
場景:nginx默認(rèn)輪詢策略

第 2 節(jié) 解決Session?致性的?案
-
Nginx的 IP_Hash 策略(可以使?)
同?個客戶端IP的請求都會被路由到同?個?標(biāo)服務(wù)器,也叫做會話粘滯- 優(yōu)點:
配置簡單,不?侵應(yīng)?,不需要額外修改代碼 - 缺點:
服務(wù)器重啟Session丟失
存在單點負(fù)載?的?險
單點故障問題
- 優(yōu)點:
-
Session復(fù)制(不推薦)
多個tomcat之間通過修改配置?件,達(dá)到Session之間的復(fù)制- 優(yōu)點:
不?侵應(yīng)?
便于服務(wù)器?平擴(kuò)展
能適應(yīng)各種負(fù)載均衡策略
服務(wù)器重啟或者宕機(jī)不會造成Session丟失 - 缺點:
性能低
內(nèi)存消耗
不能存儲太多數(shù)據(jù),否則數(shù)據(jù)越多越影響性能
延遲性
- 優(yōu)點:
-
Session共享,Session集中存儲(推薦)
Session的本質(zhì)就是緩存,那Session數(shù)據(jù)為什么不交給專業(yè)的緩存中間件呢??如Redis
基于中間件實現(xiàn)的Session共享- 優(yōu)點:
能適應(yīng)各種負(fù)載均衡策略
服務(wù)器重啟或者宕機(jī)不會造成Session丟失
擴(kuò)展能?強(qiáng)
適合?集群數(shù)量使? - 缺點:
對應(yīng)?有?侵,引?了和Redis的交互代碼
image.png - 優(yōu)點:



