命名服务,顾名思义,就是帮助我们对资源进行命名的服务,命名的目的当然是为了更好的定位了。这里所提到的资源在不同场景中包括但不限于计算机(主机)名和地址、应用提供的服务的地址或者远程对象等。
本文主要介绍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 引用,部署人员必须把
<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中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZooKeeper会自动为给定节点名加上一个数字后缀,作为新的节点名。如
/home/admin/hollis1
/home/admin/hollis2
/home/admin/hollis3
- 在zookeeper中可以创建顺序节点,在ZooKeeper中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZooKeeper会自动为给定节点名加上一个数字后缀,作为新的节点名。如
下面是一个用开源客户端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-0000000002
hollis-0000000003
那么,我们就可以通过/home/admin/hollis-0000000001
来唯一定位到一个节点了,那么我们直接用这个名称给其他的资源命名了。
总结
一些比较常见的分布式框架(RPC、RMI)等都需要用到命名服务,如何解决分布式场景中的统一命名是一个至关重要的话题。
通过本文的介绍,可以知道Zookeeper可以解决分布式场景中的统一命名问题。通过本文,读者不必立刻很深入的理解其中的原理,只需要知道zookeeper是可以做分布式的命名服务的就可以了,在以后的工作中遇到类似的场景可以想到zookeeper就够了。