01.29 分佈式:關於命名服務的知識點都在這裡了

命名服務,顧名思義,就是幫助我們對資源進行命名的服務,命名的目的當然是為了更好的定位了。這裡所提到的資源在不同場景中包括但不限於計算機(主機)名和地址、應用提供的服務的地址或者遠程對象等。

本文主要介紹Java中的命名服務、簡單的命名服務的實現策略以及在分佈式場景中如何實現命名服務。

JNDI

要介紹命名服務,不得不提 Java 命名和目錄接口(Java Naming and Directory Interface,JNDI),他是J2EE中重要的規範之一,標準的J2EE容器都提供了對JNDI規範的實現。

在沒有JNDI的場景中,我們要配置一個JDBC驅動鏈接數據庫時我們需要做以下操作:

Class.forName("com.mysql.jdbc.Driver"); 
Connection conn=DriverManager.getConnection("jdbc:mysql://DBName?user=hollis&password=hollischuang");

上面的代碼中,把數據庫鏈接相關的字符串直接寫到了代碼中,這不是一個好的做法。有過web開發經驗的人都知道,在真正的web開發中我們並不需要這樣定義JDBC的連接,我們一般都是把哪些固定的字符串配置到配置文件中,然後在代碼中直接從配置中讀取。甚至有很多數據庫處理的框架(Hibernate\\mybatis)會幫我們把創建數據庫鏈接等操作全部都封裝好。

使用 JNDI 得到數據源:

Context ctx=new InitialContext(); 

Object datasourceRef=ctx.lookup("java:comp/env/jdbc/mydatasource");
DataSource ds=(Datasource)datasourceRef;
Connection c=ds.getConnection();

為了讓 JNDI 解析 java:comp/env/jdbc/mydatasource 引用,部署人員必須把 標籤插入 web.xml 文件(Web 應用程序的部署描述符)。 標籤的意思就是“這個組件依賴於外部資源”。

<resource-ref>
<description>Dollys DataSource/<description>
<res-ref-name>jdbc/mydatasource/<res-ref-name>
<res-ref-type>javax.sql.DataSource/<res-ref-type>
<res-auth>Container/<res-auth>
/<resource-ref>

上面介紹的JNDI是一種Java的命名服務。他充分的反映出命名服務的特點——對某一資源進行命名,然後通過名稱來定位唯一的資源。

到這裡,我們可以確定的是:命名服務的目的是定義一個唯一的名字。這個名字的作用是可以用來定義唯一的資源。那麼,我們想一想,在日常開發中我們如何給一組資源中的每一個某一個進行一個唯一的命名呢?在數據庫開發中,通常有兩種方案:自增的ID和UUID。

數據庫自增ID

在數據庫中,為了標識唯一記錄,可以使用自增ID,只要指定某個字段是自增的,那麼數據庫就會幫我們維護這個字段的自增。不同數據庫的實現原理不一樣,即使是MySql數據庫,不同的引擎的實現方式也不盡相同。InnoDB 中AUTO_INCREMENT的實現原理可以參考:innodb-auto-increment-handling

但是,無論如何,自增ID的實現都是基於單庫單表的。也就是說一旦涉及到分庫分表及分佈式環境,就不能依賴數據庫的自增字段來唯一標識一條記錄了。也就是說,他生成的ID也就不再能保證是唯一的了。

UUID

UUID(Universally Unique Identifier)全局唯一標識符,是指在一臺機器上生成的數字,它保證對在同一時空中的所有機器都是唯一的。按照開放軟件基金會(OSF)制定的標準計算,用到了以太網卡地址、納秒級時間、芯片ID碼和許多可能的數字。由以下幾部分的組合:當前日期和時間(UUID的第一個部分與時間有關,如果你在生成一個UUID之後,過幾秒又生成一個UUID,則第一個部分不同,其餘相同),時鐘序列,全局唯一的IEEE機器識別號(如果有網卡,從網卡獲得,沒有網卡以其他方式獲得),UUID的唯一缺陷在於生成的結果串會比較長。

UUID是由一組32位數的16進制數字所構成,也就是說若每納秒產生1兆個UUID,要花100億年才會將所有UUID用完。

在Java中,可以通過java.util.UUID的UUID.randomUUID();來生成一個UUID。

UUID是可以保證唯一性的,因為在這個長度為32位的ID中包含了時間、時鐘序列、全局唯一IEEE機器識別號等。但是,他有兩個比較明顯的缺點,那就是長度過長和沒有任何含義。長度自然不必說,他有32位16進制數字。對於『550e8400-e29b-41d4-a716-446655440000』這個字符串來說,我想任何一個程序員都看不出其表達的含義。一旦使用它作為全局唯一標識,就意味著在日後的問題排查和開發調試過程中會遇到很大的困難。


上面介紹了兩種傳統的數據庫中生成唯一標識的方法:自增ID和UUID。他們的優缺點正好相反:

  • 自增ID的優點是語義比較明確,至少我們可以知道他是第幾個生成的,而且,在很多場景中我們需要ID的自增性。但是他無法在分佈式環境中保證其唯一性。
  • UUID的優點是可以在分佈式環境中保證其唯一性,但是沒有明確的語義。

那麼,有沒有一種方法可以在分佈式環境生成一組自增的、唯一的ID呢?

Zookeeper的命名服務

Zookeeper是一個開放源碼的分佈式服務協調組件,是Google Chubby的開源實現。是一個高性能的分佈式數據一致性解決方案。他將那些複雜的、容易出錯的分佈式一致性服務封裝起來,構成一個高效可靠的原語集,並提供一系列簡單易用的接口給用戶使用。(http://www.hollischuang.com/archives/tag/zookeeper)

Zookeeper 的命名服務與 JNDI 能夠完成的功能是差不多的,它們都是將有層次的目錄結構關聯到一定資源上,但是 Zookeeper 的命名服務更加是廣泛意義上的關聯,也許你並不需要將名稱關聯到特定資源上,你可能只需要一個不會重複名稱,就像數據庫中產生一個唯一的數字主鍵一樣。

Zookeeper可以實現命名服務有兩個重要的前提

一、節點類似於文件系統中的目錄結構二、可以創建順序節點

上面說過,我們想在分佈式環境生成一組自增的、唯一的ID,那麼看看zookeeper如何保證這兩點。

  • 唯一性
  • 由於zookeeper中的節點的結構和文件系統中的目錄結構是類似的,想想我們自己的電腦,我們使用一個全路徑是不是可以唯一定位到某個目錄中的某個文件。如 /home/admin/hollis.txt是可以唯一定位到一個文件的。
  • 自增性
  • 在zookeeper中可以創建順序節點,在ZooKeeper中,每個父節點會為他的第一級子節點維護一份時序,會記錄每個子節點創建的先後順序。基於這個特性,在創建子節點的時候,可以設置這個屬性,那麼在創建節點過程中,ZooKeeper會自動為給定節點名加上一個數字後綴,作為新的節點名。如 /home/admin/hollis1/home/admin/hollis2 /home/admin/hollis3

下面是一個用開源客戶端ZKClient實現的命名服務的例子:

ZkClient client = new ZkClient(server, 5000, 5000, new BytesPushThroughSerializer());
final String fullNodePath = root.concat("/home/admin").concat("hollis");
final String ourPath = client.createPersistentSequential(fullNodePath, null);
client.delete(ourPath);
sout(ourPath);

以上代碼就可以在/home/admin節點下創建出順序的hollis節點,節點名稱hollis-0000000001 hollis-0000000002hollis-0000000003那麼,我們就可以通過/home/admin/hollis-0000000001來唯一定位到一個節點了,那麼我們直接用這個名稱給其他的資源命名了。

總結

一些比較常見的分佈式框架(RPC、RMI)等都需要用到命名服務,如何解決分佈式場景中的統一命名是一個至關重要的話題。

通過本文的介紹,可以知道Zookeeper可以解決分佈式場景中的統一命名問題。通過本文,讀者不必立刻很深入的理解其中的原理,只需要知道zookeeper是可以做分佈式的命名服務的就可以了,在以後的工作中遇到類似的場景可以想到zookeeper就夠了。


分享到:


相關文章: