Dubbo最主要干2个事情。第一个,对服务的导出和引入。第二个,维护注册中心的服务。对于第一个,尽量参考Dubbo官方文档,有详细解释。对于第二个,这边详细说说。
首先找到服务注册和引用的入口RegistryProtocol,这部分不清楚的话,可以参考文档Dubbo官方文档-服务导出
public void register(URL registryUrl, URL registedProviderUrl) {
Registry registry = registryFactory.getRegistry(registryUrl);
registry.register(registedProviderUrl);
}
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// ----------------------此处省略一堆代码------------------------
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
// ----------------------此处省略一堆代码------------------------
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// ----------------------此处省略一堆代码------------------------
registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
Constants.CHECK_KEY, String.valueOf(false)));
// ----------------------此处省略一堆代码------------------------
}
上面省略了大量的代码,只是把关于registry的几个调用点贴出来了。可以看到有register,subscribe两个行为。然后看看RegistryService接口中,总共有多少行为。
public interface RegistryService {
// 向注册中心注册url
void register(URL url);
// 从注册中心删除(卸载)url
void unregister(URL url);
// 向注册中心注册对某个目录的监听
void subscribe(URL url, NotifyListener listener);
// 从注册中心删除(卸载)对某个目录的监听
void unsubscribe(URL url, NotifyListener listener);
// 从注册中心查询服务
List<URL> lookup(URL url);
}
从RegistryService可以看到除了上面出现的2个行为,还有2个反向的。还有一个查询。很容易理解。把RegistryService中的5个行为抽象出来之后,就可以有很好的扩展性。可以适配redis,zookeeper,数据库,或者开发者自定义实现方式。
注册过程分为2步
1.向注册中心发送注册(Register)请求(具体注册的内容和存储形式要看使用的是什么注册中心)
2.向注册中心发起订阅请求(订阅的具体内容,和订阅形式要看使用的是什么注册中心)
删除注册过程分为2步
1.向注册中心发送删除注册(UnRegister)请求
2.向注册中心发起取消订阅(Unsubscribe)请求
看下类结构图:
结构很清晰。Dubbo给出了4种实现。先从Zookeeper开始说。看下示意图。
zookeeper作为注册中心
服务启动并且注册完成后,每个服务都会在zookeeper上建立4个永久目录。可以从上图中看到。找到代码ZookeeperRegistry.toCategoriesPath。可以看到4个目录的建立是在这个地方设置的。
private String[] toCategoriesPath(URL url) {
// ----------------------此处省略一堆代码------------------------
categories = new String[]{Constants.PROVIDERS_CATEGORY, Constants.CONSUMERS_CATEGORY,
Constants.ROUTERS_CATEGORY, Constants.CONFIGURATORS_CATEGORY};
// ----------------------此处省略一堆代码------------------------
}
其中configurators和routers并不常用,主要在配置和服务治理的时候才会用到。常用的是providers和consumers。如果一个服务有N个Provider,那在providers下面会产生N个子节点。同样的,如果一个服务有N个Consumer,那在consumers下面会产生N个子节点。关于子节点中对服务的描述会比较长,这边给出一个示意图,调试代码的可以直接登录到Zookeeper查看自己的服务情况。
注册过程是在zookeeper上建立目录并且把服务的描述信息写进去。除此之外还需要订阅它关注的服务。服务也就是目录(Dubbo把它抽象为Directory)。Provider和Consumer都有个自己关心的几个目录。
Provider只关心configurators,因为它不关心有多少Consumer,或者Consumer的路由策略。所以Provider会监视configurators。
Consumer会关心configurators,routers,providers,它需要关心各个provider的动态,和自己的路由策略。所以Consumer会分别监视configurators,routers,providers。
对于所监视的目录,只要children发生任何变化都会触发childChanged事件。然后做出相应处理。
比如:Consumer监视providers的动态,当/dubbo/com.xxx.XxxService/providers目录下减少服务时。会触发Consumer重新搜有这个目录下所有的服务,并且重新维护Consumer的服务列表(具体对服务列表的维护需要关注Dubbo Directory模块的代码),客户代码再次调用这个服务时,被删除的服务节点不会再收到服务请求。
Dubbo为了兼容多个zk客户端,还搞了一个Zookeeper的Transprter层。官方给出了2种实现,分别是curator和zkClient。看下类图结构。
还是比较清晰的。看下ZookeeperTransporter的代码。
@SPI("curator")// 默认使用curator
public interface ZookeeperTransporter {
// 建立连接,并且返回一个ZookeeperClient,后续的一些列对zookeeper的操作都需要以ZookeeperClient为委托。
ZookeeperClient connect(URL url);
}
public class CuratorZookeeperTransporter implements ZookeeperTransporter {
// 与zookeeper建立连接的过程就是new CuratorZookeeperClient,连接的过程在构造方法中。
@Override
public ZookeeperClient connect(URL url) {
return new CuratorZookeeperClient(url);
}
}
与Zookeeper建立连接的过程就是new CuratorZookeeperClient,连接的过程在构造方法中。
public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> {
public CuratorZookeeperClient(URL url) {
super(url);
try {
CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
.connectString(url.getBackupAddress())
.retryPolicy(new RetryNTimes(1, 1000))
.connectionTimeoutMs(5000);
String authority = url.getAuthority();
if (authority != null && authority.length() > 0) {
builder = builder.authorization("digest", authority.getBytes());
}
client = builder.build();
// ----------------------此处省略一堆代码------------------------
client.start();// 建立连接
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
}
Zookeeper的Transporter的抽象,不仅抽象了connet的连接过程。还抽象了对节点的所有操作行为。看下ZookeeperClient的所有行为。
public interface ZookeeperClient {
// 创建节点(节点目录,是否是零时节点)
void create(String path, boolean ephemeral);
// 删除节点(节点目录)
void delete(String path);
// 获取节点下的children
List<String> getChildren(String path);
// 添加对children的监听
List<String> addChildListener(String path, ChildListener listener);
// 删除对children的监听
void removeChildListener(String path, ChildListener listener);
// 添加zookeeper状态监听
void addStateListener(StateListener listener);
// 删除zookeeper状态监听
void removeStateListener(StateListener listener);
// 是否处于连接状态
boolean isConnected();
// 关闭连接
void close();
URL getUrl();
}
上述抽象行为几乎覆盖了Zookeeper主要操作,Dubbo对Zookeeper的行为抽象不需要覆盖Zookeeper的所有操作,只需要关注Dubbo本身是否够用,够用即可。通过上面的类图可以看到ZookeeperClient有两个实现,分别是CuratorZookeeperClient和ZkclientZookeeperClient。这边看下Dubbo默认用的CuratorZookeeperClient。代码较多,这边只列出部分以示说明。
public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> {
// 建立连接后,持有的客户端。也就是ZookeeperClient的委托。所有的实际节点的操作都转交给它
private final CuratorFramework client;
public CuratorZookeeperClient(URL url) {
// 建立连接
}
@Override
public void createPersistent(String path) {
try {// 创建永久节点,用client委托
client.create().forPath(path);
} catch (NodeExistsException e) {
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
@Override
public void createEphemeral(String path) {
try {// 创建临时节点,用client委托
client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
} catch (NodeExistsException e) {
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
@Override
public void delete(String path) {
try {// 删除节点,用client委托
client.delete().forPath(path);
} catch (NoNodeException e) {
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
@Override
public List<String> getChildren(String path) {
try {// 获取子节点,用client委托
return client.getChildren().forPath(path);
} catch (NoNodeException e) {
return null;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
}
以上的示例以及解释,能够从结构上搞清楚Zookeeper的Transporter。
redis作为注册中心
Dubbo在使用Redis作为注册中心时,有2个小细节需要提醒下。
1.当redis设置了密码。那么就需要在配置时加上密码。会这么配置。
<dubbo:registry protocol="redis" address="redis://host:6379" password="password" />
启动会发现报错了。
java.lang.IllegalArgumentException: Invalid url, password without username!
说是没有设置username,但是Redis没有用户名这一说啊。怎么搞?
我之前看过一篇文章,解决方案是把Dubbo的源代码改了,把抛出IllegalArgumentException的地方注释掉,然后用修改后的class替换上去。方法能OK。但个人感觉没必要哈。Dubbo既然检查username,那就随便给它来一个。
<dubbo:registry protocol="redis" address="redis://host:6379" password="password" username="anything" />
上述代码是RedisRegistry在初始化Redis连接的部分代码。可以看到在连接Redis时,只是使用了password,而没有使用username,也就是说,username写成啥都行。那不就简单绕过去了么。没有必要去修改人家源代码。
2.Redis有多个DB的,Dubbo默认使用0号DB,怎么自定义呢?
还是上面这个问题的文章,解决方案是把JedisPool的源码修改下,在初始化的时候,把DB给写死,我去,这操作。。。也没的说了。
其实这个问题很简单,Dubbo是可以配置param的。
看上图,可以看到Dubbo这边取的是db.index这个paramerter。那就给它一个呗。
<dubbo:registry protocol="redis" address="redis://host:6379" password="password" username="anything">
<dubbo:parameter key="db.index" value="1"></dubbo:parameter>
</dubbo:registry>
以上两个问题,直接看下源码就能猜到解决方案了。下面说下RedisRegistry的实现。
与Zookeeper不同的是,Dubbo没有给Redis做Transporter的适配。不知道是什么原因。不管了,看下RedisRegistry。
RedisRegistry的构造方法。用来创建Redis连接。
public RedisRegistry(URL url) {
// ----------------------此处省略一堆代码------------------------
for (String address : addresses) {
int i = address.indexOf(':');
String host;
int port;
if (i > 0) {
host = address.substring(0, i);
port = Integer.parseInt(address.substring(i + 1));
} else {
host = address;
port = DEFAULT_REDIS_PORT;
}
// 建立连接
this.jedisPools.put(address, new JedisPool(config, host, port,
url.getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT), StringUtils.isEmpty(url.getPassword()) ? null : url.getPassword(),
url.getParameter("db.index", 0)));
}
// ----------------------此处省略一堆代码------------------------
}
public void doRegister(URL url) {
// ----------------------此处省略一堆代码------------------------
jedis.hset(key, value, expire);// 写入redis
jedis.publish(key, Constants.REGISTER);// 发布register消息
// ----------------------此处省略一堆代码------------------------
}
public void doUnregister(URL url) {
// ----------------------此处省略一堆代码------------------------
jedis.hdel(key, value);// 从redis删除key
jedis.publish(key, Constants.UNREGISTER);// 发布unregister消息
// ----------------------此处省略一堆代码------------------------
}
doRegister是做注册动作,使用hset方法写入redis。写完redis,还要发布register消息,通知关注此服务的节点。如果当前服务是Provider,发布register消息后,关注此服务的Consumer就会接到消息,并且从Redis再次全量获取服务列表,拉回去之后刷新自己的维护的服务列表。
doUnRegister是删除注册动作,也就是卸载服务,使用hdel方法删除Redis的key,还要发布unregister消息,通知关注此服务的节点。如果当前服务是Provider,发布unregister消息后,关注此服务的Consumer就会接到消息,并且从Redis再次全量获取服务列表,拉回去之后刷新自己的维护的服务列表。
@Override
public void doSubscribe(final URL url, final NotifyListener listener) {
String service = toServicePath(url);// 结构:/dubbo/com.xxx.XxxService
Notifier notifier = notifiers.get(service);
if (notifier == null) {
Notifier newNotifier = new Notifier(service);
notifiers.putIfAbsent(service, newNotifier);
notifier = notifiers.get(service);
if (notifier == newNotifier) {
// 每个服务都会搞一个线程,这个线程用来向redis订阅对当前这个服务的所有监听
// channel = "/dubbo/com.xxx.XxxService/*"
notifier.start();
}
}
// 下面这部分代码省略了,这边稍微说下它的作用
// 监听这个服务之后,首次先把当前这个服务中下的节点信息拿出来做一个初始化的update通知。具体可以再看看代码理解下
}
private class Notifier extends Thread {
// ----------------------此处省略一堆代码------------------------
@Override
public void run() {
while (running) {// 死循环,直到running被置为false
try {
if (!isSkip()) {
try {
for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
JedisPool jedisPool = entry.getValue();
try {
jedis = jedisPool.getResource();
try {
if (service.endsWith(Constants.ANY_VALUE)) {
if (!first) {
first = false;
Set<String> keys = jedis.keys(service);
if (keys != null && !keys.isEmpty()) {
for (String s : keys) {
doNotify(jedis, s);
}
}
resetSkip();
}
// 订阅服务
// channel结构:/dubbo/com.xxx.XxxService/*
jedis.psubscribe(new NotifySub(jedisPool), service); // blocking
} else {
if (!first) {
first = false;
doNotify(jedis, service);
resetSkip();
}
// 订阅服务
// channel结构:/dubbo/com.xxx.XxxService/*
jedis.psubscribe(new NotifySub(jedisPool), service + Constants.PATH_SEPARATOR + Constants.ANY_VALUE); // blocking
}
break;
} finally {
jedis.close();
}
} catch (Throwable t) { // Retry another server
logger.warn("Failed to subscribe service from redis registry. registry: " + entry.getKey() + ", cause: " + t.getMessage(), t);
// If you only have a single redis, you need to take a rest to avoid overtaking a lot of CPU resources
sleep(reconnectPeriod);
}
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
sleep(reconnectPeriod);
}
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
}
// ----------------------此处省略一堆代码------------------------
}
private class NotifySub extends JedisPubSub {
// ----------------------此处省略一堆代码------------------------
@Override
public void onMessage(String key, String msg) {
if (logger.isInfoEnabled()) {
logger.info("redis event: " + key + " = " + msg);
}
// 检查是是否是register或者unregister
if (msg.equals(Constants.REGISTER)
|| msg.equals(Constants.UNREGISTER)) {
try {
Jedis jedis = jedisPool.getResource();
try {
doNotify(jedis, key);// 通知
} finally {
jedis.close();
}
} catch (Throwable t) { // TODO Notification failure does not restore mechanism guarantee
logger.error(t.getMessage(), t);
}
}
}
// ----------------------此处省略一堆代码------------------------
}
从上面3部分代码,可以完整明白,Redis的注册,订阅,消息发布,消息接收并且通知等过程。
Dubbo对Redis还是有些偏心啊,没有给它做Transporter层。