Cluster模式潛在問題及解決方案、Web服務(wù)綜合解決方案

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

分布式系統(tǒng)的特征,來源于百度百科

服務(wù)器集群的定義,來源于百度百科
分布式系統(tǒng)示例

第一部分:一致性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)榱?個,之前所有的求模都需要重新計算。


普通Hash算法存在的問題

如果在真實?產(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é)點


客戶端落點哈希環(huán)

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

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


擴(kuò)容--增加服務(wù)器

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é)點


一致性Hash算法+虛擬節(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));
        }
    }
上述代碼數(shù)據(jù)示意圖
  • ?致性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

ngx_http_upstream_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?件中配置


nging配置

小貼士

  • 哈希表的數(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)?


image.png

操作?式:
選取集群中的?個服務(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í)?)


分布式調(diào)度

第 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í)行什么邏輯由我們自己來指定。


Job任務(wù)分片

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


分片代碼

5.6 彈性擴(kuò)容

elasticjob彈性擴(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?致性


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)輪詢策略


Session丟失問題分析(重復(fù)登錄)

第 2 節(jié) 解決Session?致性的?案

  • Nginx的 IP_Hash 策略(可以使?)
    同?個客戶端IP的請求都會被路由到同?個?標(biāo)服務(wù)器,也叫做會話粘滯

    • 優(yōu)點:
      配置簡單,不?侵應(yīng)?,不需要額外修改代碼
    • 缺點:
      服務(wù)器重啟Session丟失
      存在單點負(fù)載?的?險
      單點故障問題
  • Session復(fù)制(不推薦)
    多個tomcat之間通過修改配置?件,達(dá)到Session之間的復(fù)制

    • 優(yōu)點:
      不?侵應(yīng)?
      便于服務(wù)器?平擴(kuò)展
      能適應(yīng)各種負(fù)載均衡策略
      服務(wù)器重啟或者宕機(jī)不會造成Session丟失
    • 缺點:
      性能低
      內(nèi)存消耗
      不能存儲太多數(shù)據(jù),否則數(shù)據(jù)越多越影響性能
      延遲性
  • 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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容