Dubbo的订阅分布实现可基于注册中心,也可简单的通过直接通信的方式实现,但直接通信的方式可靠性比较低,目前大多数的实现还是需要一个注册中心。Dubbo内部实现了几个基于不同组件实现的注册中心(Redis,Nacos,Zookeeper等等),因为主要是了解订阅发布实现的原理,所以我基于最较简单的Redis为入口进行了解学习。
整体流程
先简单梳理下整体流程
- 服务治理中心(dubbo-admin)启动,并同时订阅所有消费者、服务提供者、路由和配置元数据信息。
- provider启动时,会向注册中心写入自己的元数据信息,同时会订阅配置元数据信息。
- consumer启动时,也会向注册中心写入自己的元数据信息,并订阅provider、路由和配置元数据信息。
- 当有provider离开或有新的provider加入时,注册中心provider的目录会发生变化,变化信息会动态通知给消费者,服务治理中心。
发布的实现
- 采用Redis的过期机制和publish/subscribe特性实现发布
- provider发布服务时,会在Redis中创建一个kv(默认超时时间为1分钟),并且在redis通道中发布一条register事件消息。
- provider实例化的同时,也会实例化一个RedisRegistry,RedisRegistry的构造方法中会启动一个expireExecutor定时调度线程池,该定时任务会不停地向Redis发送请求并将当前provider在redis中的kv进行延时
- 当provider的kv对已经过期,则将对应的kv从redis删除,并向redis通道发布一条unregister事件消息。
伪代码如下(具体代码可见2.6.x及以下的源码):
public class RedisRegistry{
// 定时调度线程池, 不断地调用deferExpired()
private final ScheduledExecutorService expireExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("DubboRegistryExpireTimer", true));
private final ScheduledFuture<?> expireFuture;
// 注册实例初始化,对于每一个dubbo角色(provider/consumer/admin)其都会有一个register实例,因为每一个dubbo角色都会需要发布/订阅的功能
public RedisRegistry() {
// 一些初始化操作
// ...
this.expireFuture = expireExecutor.scheduleWithFixedDelay(new Runnable() {
@Overrider
public void run() {
try{
deferExpired();
}catch(Throwable e) {
//...
}
}
}, initialDelay, expiredPeriod, TimeUnit.MILLISECONDS);
}
// 延续redis中key的超时事件,证明provider是在线的
private void deferExpired() {
// 获取已注册的所有provider
Redis redis = RedisPool.getResource();
for(URL url : getRegistered()) {
String key = toCategoryPath(url);
// 对key续期
if(redis.hset(key, url.toFullString(), System.currentTimeMillis() + expirePeriod)) == 1) {
// 如果续期返回1, 说明key已经被删除,这次算重新发布,因此在通道内广播
redis.publish(key, Constants.REGISTER);
}
}
// 如果是服务治理中心,清理相应已过期的key,并发送ungister事件
if(admin){
clean(redis);
}
}
// 发布的实现
public void doRegister(URL url) {
String key = toCategoryPath(url);
String value = url.toFullString();
String expired = String.valueOf(System.currentTimeMills + expiredPeriod);
Redis redis = RedisPool.getResource();
// 向redis写入一个key,标识provider的注册信息
redis.hset(key, value, expired);
// 发布一个注册事件
redis.publish(key, Constants.REGISTER);
}
// 注销的实现,与发布同理
public void unRegister() {
String key = toCategoryPath(url);
String value = url.toFullString();
String expired = String.valueOf(System.currentTimeMills + expiredPeriod);
Redis redis = RedisPool.getResource();
// 删除provider对应的key
redis.hdel(key, value, expired);
// 发布一个注销事件
redis.publish(key, Constants.UNREGISTER);
}
}
订阅的实现
- 初始化时,启动一个Notifer内部线程类,在启动时会异步去监听Redis通道的事件。
- 初始化时,全量拉取注册中心上所有的服务信息配置,并保存在本地内存和硬盘中。
public class RedisRegistry {
public void doSubScribe(URL url, final NotifyListener listener){
String service = toServicePath(url);
Notifier notifier = notifiers.get(service);
// 如果是首次订阅,则会先创建一个Notifier内部类,启动时异步去进行redis通道的订阅
if(notifier == null) {
Notifier newNotifier = new Notifier(service);
notifiers.put(service, newNotifier);
newNotifier.start();
}
// 在异步订阅redis通道的同时,主线程继续向下执行,全量拉取一次注册中心上的所有服务信息
Redis redis = RedisPool.getResource();
if (service.endsWith('*') {
// 以*结尾的是服务治理中心,订阅所有的服务
for(String key : redis.allKeys(service)) {
doNotify(redis, key);
}
} else {
// 普通的consumer,只订阅自己需要的provider
doNotify(redis, redis.getKey(service));
}
}
// 具体实现配置更新/保存的方法
private void doNotify() {
// 1.调用FallbackRegistry的doNotify -> 调用AbstractRegistry#notify
// 2.调用AbstractRegistry#notify中会调用saveProperties()保存、更新所订阅provider的配置信息.
}
}
代码结构
订阅的实现中提到了实际的配置更新逻辑是由AbstractRegistry#notify方法承担的,这是也是Dubbo注册中心的设计理念,在整个注册中心的逻辑部分采用了
模板模式
AbstractRegistry实现了Registry接口中的注册、订阅、查询、通知等方法,还实现硬盘文件持久化注册信息的通用方法,但是其所有方法只是简单的把URL加入对应的集合,没有具体的注册或订阅逻辑FailbackRegistry又继承了AbstractRegistry,重写了父类的注册、订阅、查询和通知等方法,并添加了重试机制。此外,还添加了四个未实现的抽象模板方法,供开发根据这四个方法去定义自己的注册中心
protected abstract void doRegister(URL url);
protected abstract void doUnregister(URL url);
protected abstract void doSubscribe(URL url, NotifyListener listener);
protected abstract void doUnsubscribe(URL url, NotifyListener listener);
另外,注册中心的具体创建也是采用
模板模式+工厂模式,自定义的注册中心实现AbstractRegistryFactory#getRegistry方法,实现自己的工厂类。这种极强的拓展性可以让用户更轻松的针对注册中心模块进行二次开发。