Dubbo-聊聊注册中心的设计
前言
Dubbo源码阅读分享系列文章,欢迎大家关注点赞
SPI实现部分
注册中心作用

在整个Duubbo架构中,注册中心主要完成以下三件事情:
Provider应用启动后的初始化阶段会向注册中心完成注册操作; Consumer应用启动初始化阶段会完成对所需 Provider的进行订阅操作; 在Provider发生变更时通知监听的Consumer;
Registry在整个架构中主要是对Consumer 和 Provider 所对应的业务进行解耦,从而提升系统的稳定性。关于为什么需要注册中心,大家可以参考一下微服务下的注册中心如何选择,在这篇文章中我花了一小节来介绍这个问题。
源码分析
开始之前,首先来看下关于注册中心源码的项目结构,整个项目由两块组成,一个就是核心Api,另外一个就是具体一些中间件的实现,看到这个我们可能会有一些想法,这个定然会有一些模板类以及一些工厂设计,能想到这里,说明你已经具有很好的抽象思维,废话不多说开始源码。

核心Api
dubbo-registry-api是注册中心核心对象的抽象以及实现部分,我们首先来看下几个核心对象设计

Node
Node位于Dubbo的dubbo-common项目下面,在Dubbo中Node这个接口用来抽象节点的概念,Node不仅可以表示Provider和Consumer节点,还可以表示注册中心节点。Node节点内部定义三个方法:
getUrl方法返回当前节点的URL; isAvailable方法方法返回对象是否可用; destroy方法负责销毁对象;
RegistryService
RegistryService此接口定义注册中心的功能,定义五个方法:
register方法向注册中心注册一个URL; unregister方法取消一个URL的注册; subscribe方法订阅一个URL,订阅成功之后,当订阅的数据发生变化时,注册中心会主动通知第二个参数指定的 NotifyListener对象,NotifyListener接口中定义的 notify() 方法就是用来接收该通知的; unsubscribe方法取消一个URL定义,同时当数据发生变化时候也会主动发起通知; lookup方法查询符合条件的注册的数据,subscribe采用Push方式,而lookup采用的是Pull模式;
Registry

Registry接口继承了Node和RegistryService这两个接口,实现该接口类就是注册中心接口的节点,该方法内部也提供两个默认方法reExportRegister方法和reExportUnregister方法,这两个方法实际调用还是RegistryService中的方法。
public interface Registry extends Node, RegistryService {
//调用RegistryService的register
default void reExportRegister(URL url) {
register(url);
}
//调用RegistryService的unregister
default void reExportUnregister(URL url) {
unregister(url);
}
}
RegistryFactory
RegistryFactory是 Registry 的工厂类,负责创建 Registry 对象,通过@SPI 注解指定了默认的扩展名为 dubbo,@Adaptive注解表示会生成适配器类并根据 URL 参数中的 protocol 参数值选择相应的实现。
@SPI("dubbo")
public interface RegistryFactory {
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
下图是RegistryFactory多种不同的实现,每个 Registry 实现类都有对应的 RegistryFactory 工厂实现,每个 RegistryFactory 工厂实现只负责创建对应的 Registry 对象。

AbstractRegistryFactory
AbstractRegistryFactory 是一个实现了 RegistryFactory 接口的抽象类,内部维护一个Registry的Map集合以及提供销毁和创建注册中心方法,针对不同的注册中心可以有不同的实现。
//锁
protected static final ReentrantLock LOCK = new ReentrantLock();
//Map
protected static final Map<String, Registry> REGISTRIES = new HashMap<>();
销毁
销毁方法分为两个,一个全量,一个是单个,单个销毁在AbstractRegistry中调用,参数是注册实例对象。
//全量销毁
public static void destroyAll() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Close all registries " + getRegistries());
}
// Lock up the registry shutdown process
LOCK.lock();
try {
for (Registry registry : getRegistries()) {
try {
//一个一个销毁
registry.destroy();
} catch (Throwable e) {
LOGGER.error(e.getMessage(), e);
}
}
//清空map缓存
REGISTRIES.clear();
} finally {
// Release the lock
LOCK.unlock();
}
}
//单个销毁
public static void removeDestroyedRegistry(Registry toRm) {
LOCK.lock();
try {
REGISTRIES.entrySet().removeIf(entry -> entry.getValue().equals(toRm));
} finally {
LOCK.unlock();
}
}
创建/获取
getRegistry是对RegistryFactory实现,如果没有在缓存中,则进行创建实例对象createRegistry,createRegistry是抽象方法,为了让子类重写该方法,比如说redis实现的注册中心和zookeeper实现的注册中心创建方式肯定不同,而他们相同的一些操作都已经在AbstractRegistryFactory中实现,所以只要关注且实现该抽象方法即可。
//抽象的createRegistry方法
protected abstract Registry createRegistry(URL url);
//获取实例
public Registry getRegistry(URL url) {
Registry defaultNopRegistry = getDefaultNopRegistryIfDestroyed();
if (null != defaultNopRegistry) {
return defaultNopRegistry;
}
//构建key
url = URLBuilder.from(url)
.setPath(RegistryService.class.getName())
.addParameter(INTERFACE_KEY, RegistryService.class.getName())
.removeParameters(EXPORT_KEY, REFER_KEY)
.build();
String key = createRegistryCacheKey(url);
// Lock the registry access process to ensure a single instance of the registry
LOCK.lock();
try {
// double check
// fix https://github.com/apache/dubbo/issues/7265.
defaultNopRegistry = getDefaultNopRegistryIfDestroyed();
if (null != defaultNopRegistry) {
return defaultNopRegistry;
}
//获取实例对象
Registry registry = REGISTRIES.get(key);
if (registry != null) {
return registry;
}
//没有获取到就创建
registry = createRegistry(url);
if (registry == null) {
throw new IllegalStateException("Can not create registry " + url);
}
//放入Map集合中
REGISTRIES.put(key, registry);
return registry;
} finally {
// Release the lock
LOCK.unlock();
}
}
ListenerRegistryWrapper

ListenerRegistryWrapper是对RegistryFactory的扩展,创建Registry时候会包装一个ListenerRegistryWrapper对象,内部维护一个监听器RegistryServiceListener,当注册、取消注册、订阅以及取消订阅的时候,会发送通知。
AbstractRegistry
AbstractRegistry该抽象类是对Registry接口的实现,实现了Registry接口中的注册、订阅、查询、通知等方法,但是注册、订阅、查询、通知等方法只是简单地把URL加入对应的集合,没有具体的注册或订阅逻辑。此外该类还实现了缓存机制,只不过,它的缓存有两份,一份在内存,一份在磁盘。
//本地的Properties文件缓存,在内存中 与file是同步的
private final Properties properties = new Properties();
//该单线程池负责讲Provider的全量数据同步到properties字段和缓存文件中,
//如果syncSaveFile配置为false,就由该线程池异步完成文件写入
private final ExecutorService registryCacheExecutor = Executors.newFixedThreadPool(1, new NamedThreadFactory("DubboSaveRegistryCache", true));
//是否异步写入
private boolean syncSaveFile;
//注册数据的版本号 防止旧数据覆盖新数据
private final AtomicLong lastCacheChanged = new AtomicLong();
//保存Properties失败以后异常重试次数
private final AtomicInteger savePropertiesRetryTimes = new AtomicInteger();
//已经注册服务的URL集合,注册的URL不仅仅可以是服务提供者的,也可以是服务消费者的
private final Set<URL> registered = new ConcurrentHashSet<>();
//消费者Url订阅的监听器集合
private final ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<>();
//费者被通知的服务URL集合,最外部URL的key是消费者的URL,value是一个map集合,里面的map中的key为分类名,value是该类下的服务url集合
private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<>();
//注册中心URL
private URL registryUrl;
//本地磁盘Properties文件
private File file;
变更通知
当 Provider 端暴露的 URL 发生变化时,ZooKeeper 等服务发现组件会通知 Consumer 端的 Registry 组件,Registry 组件会调用 notify() 方法,被通知的 Consumer 能匹配到所有 Provider 的 URL 列表并写入 properties 集合以及本地文件中。
protected void notify(List<URL> urls) {
if (CollectionUtils.isEmpty(urls)) {
return;
}
//遍历订阅消费者URL的监听器集合,通知他们
for (Map.Entry<URL, Set<NotifyListener>> entry : getSubscribed().entrySet()) {
URL url = entry.getKey();
//匹配
if (!UrlUtils.isMatch(url, urls.get(0))) {
continue;
}
//遍历所有监听器
Set<NotifyListener> listeners = entry.getValue();
if (listeners != null) {
for (NotifyListener listener : listeners) {
try {
//通知监听器,URL变化结果
notify(url, listener, filterEmpty(url, urls));
} catch (Throwable t) {
logger.error("Failed to notify registry event, urls: " + urls + ", cause: " + t.getMessage(), t);
}
}
}
}
}
/**
* Notify changes from the Provider side.
*
* @param url consumer side url
* @param listener listener
* @param urls provider latest urls
*/
protected void notify(URL url, NotifyListener listener, List<URL> urls) {
//参数校验
if (url == null) {
throw new IllegalArgumentException("notify url == null");
}
if (listener == null) {
throw new IllegalArgumentException("notify listener == null");
}
if ((CollectionUtils.isEmpty(urls))
&& !ANY_VALUE.equals(url.getServiceInterface())) {
logger.warn("Ignore empty notify urls for subscribe url " + url);
return;
}
if (logger.isInfoEnabled()) {
logger.info("Notify urls for subscribe url " + url + ", urls: " + urls);
}
Map<String, List<URL>> result = new HashMap<>();
for (URL u : urls) {
//按照url中key为category对应的值进行分类,如果没有该值,就找key为providers的值进行分类
if (UrlUtils.isMatch(url, u)) {
String category = u.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY);
List<URL> categoryList = result.computeIfAbsent(category, k -> new ArrayList<>());
//分类结果放入result
categoryList.add(u);
}
}
if (result.size() == 0) {
return;
}
//处理通知监听器URL变化结果
Map<String, List<URL>> categoryNotified = notified.computeIfAbsent(url, u -> new ConcurrentHashMap<>());
for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
String category = entry.getKey();
List<URL> categoryList = entry.getValue();
//把分类标实和分类后的列表放入notified的value中
categoryNotified.put(category, categoryList);
//调用NotifyListener监听器
listener.notify(categoryList);
//单个Url变更,并将更改信息同步至内存缓存和磁盘缓存中
saveProperties(url);
}
}
private void saveProperties(URL url) {
if (file == null) {
return;
}
try {
StringBuilder buf = new StringBuilder();
//从通知列表中取出信息
Map<String, List<URL>> categoryNotified = notified.get(url);
//以空格为间隔拼接
if (categoryNotified != null) {
for (List<URL> us : categoryNotified.values()) {
for (URL u : us) {
if (buf.length() > 0) {
buf.append(URL_SEPARATOR);
}
buf.append(u.toFullString());
}
}
}
//推送url至内存缓存
properties.setProperty(url.getServiceKey(), buf.toString());
//增加版本号
long version = lastCacheChanged.incrementAndGet();
if (syncSaveFile) {
//如果磁盘文件未被加锁,将内存缓存同步至磁盘缓存
doSaveProperties(version);
} else {
//如果被加锁了,使用新的线程去执行,当前线程返回
registryCacheExecutor.execute(new SaveProperties(version));
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
缓存设计
注册中心有两份缓存,一份在内存,一份在磁盘。该方法的作用是将内存中的缓存数据保存在磁盘文件中,该方法有错误重试,最大重试次数是3,重试采用另一个线程去执行重试,不是当前线程。本地缓存设计相当于是一种容错机制,当网络抖动等原因而导致订阅失败时,Consumer端的Registry就可以通过getCacheUrls()方法获取本地缓存,从而得到最近注册的服务提供者。
//将内存中的文件写到磁盘上
public void doSaveProperties(long version) {
//版本号判断 防止重复写
if (version < lastCacheChanged.get()) {
return;
}
//判断磁盘文件是否为空
if (file == null) {
return;
}
// Save
try {
//lock文件,用于加锁操作
File lockfile = new File(file.getAbsolutePath() + ".lock");
if (!lockfile.exists()) {
lockfile.createNewFile();
}
//RandomAccessFile提供对文件的读写操作
try (RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
FileChannel channel = raf.getChannel()) {
//获取锁
FileLock lock = channel.tryLock();
if (lock == null) {
throw new IOException("Can not lock the registry cache file " + file.getAbsolutePath() + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.registry.file=xxx.properties");
}
// Save
try {
if (!file.exists()) {
file.createNewFile();
}
try (FileOutputStream outputFile = new FileOutputStream(file)) {
//从内存缓存中获取数据 写入文件
properties.store(outputFile, "Dubbo Registry Cache");
}
} finally {
lock.release();
}
}
} catch (Throwable e) {
//发生异常时,重试次数+1
savePropertiesRetryTimes.incrementAndGet();
//重试次数大于抛出异常
if (savePropertiesRetryTimes.get() >= MAX_RETRY_TIMES_SAVE_PROPERTIES) {
logger.warn("Failed to save registry cache file after retrying " + MAX_RETRY_TIMES_SAVE_PROPERTIES + " times, cause: " + e.getMessage(), e);
savePropertiesRetryTimes.set(0);
return;
}
//再次对比版本信息,如果版本已过期,返回不再处理
if (version < lastCacheChanged.get()) {
savePropertiesRetryTimes.set(0);
return;
} else {
//重试线程
registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
}
logger.warn("Failed to save registry cache file, will retry, cause: " + e.getMessage(), e);
}
}
//磁盘中文件加载到内存中
private void loadProperties() {
if (file != null && file.exists()) {
InputStream in = null;
try {
in = new FileInputStream(file);
properties.load(in);
if (logger.isInfoEnabled()) {
logger.info("Load registry cache file " + file + ", data: " + properties);
}
} catch (Throwable e) {
logger.warn("Failed to load registry cache file " + file, e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
}
}
注册/订阅
AbstractRegistry 实现了 Registry 接口,当新节点注册进来时候registry() 方法,会将当前节点要注册的 URL缓存到 registered集合,当节点下线时候, unregistry() 方法会从 registered 集合删除指定的 URL。当消费者新增加一个订阅的时候,subscribe() 方法会将当前节点作为 Consumer 的 URL 以及相关的 NotifyListener 记录到 subscribed 集合,当消费者取消一个订阅的时候,unsubscribe() 方法会将当前节点的 URL 以及关联的 NotifyListener 从 subscribed 集合删除。这四个方法相对比较简单,这里不做展示,此处设计为抽象类,当子类重写的时候可以对其进行增强。
恢复/销毁
当因为网络问题与注册中心断开连接之后,会进行重连,重新连接成功之后,会调用 recover() 方法将 registered 集合中的全部 URL 重新执行register() 方法,恢复注册数据。同样,recover() 方法也会将 subscribed 集合中的 URL 重新执行subscribe() 方法,恢复订阅监听器。
当前节点下线的时候,destroy() 方法会调用 unregister() 方法和 unsubscribe() 方法将当前节点注册的 URL 以及订阅的监听全部清理掉,此外还会销毁本实例。
public void destroy() {
if (logger.isInfoEnabled()) {
logger.info("Destroy registry:" + getUrl());
}
Set<URL> destroyRegistered = new HashSet<>(getRegistered());
if (!destroyRegistered.isEmpty()) {
for (URL url : new HashSet<>(destroyRegistered)) {
if (url.getParameter(DYNAMIC_KEY, true)) {
try {
//取消注册
unregister(url);
if (logger.isInfoEnabled()) {
logger.info("Destroy unregister url " + url);
}
} catch (Throwable t) {
logger.warn("Failed to unregister url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
}
}
}
}
Map<URL, Set<NotifyListener>> destroySubscribed = new HashMap<>(getSubscribed());
if (!destroySubscribed.isEmpty()) {
for (Map.Entry<URL, Set<NotifyListener>> entry : destroySubscribed.entrySet()) {
URL url = entry.getKey();
for (NotifyListener listener : entry.getValue()) {
try {
//取消订阅
unsubscribe(url, listener);
if (logger.isInfoEnabled()) {
logger.info("Destroy unsubscribe url " + url);
}
} catch (Throwable t) {
logger.warn("Failed to unsubscribe url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
}
}
}
}
//移除注册中心
AbstractRegistryFactory.removeDestroyedRegistry(this);
}
重试机制

关于重试机制,Dubbo将重试机制放在了FailbackRegistry类中,FailbackRegistry 设计思想,重写了 AbstractRegistry 中 register()/unregister()、subscribe()/unsubscribe() 以及 notify() 这五个核心方法,结合时间轮,实现失败重试机制。此外,还添加了四个未实现的抽象模板方法,由其继承者去实现,这里也就是典型的模板类的设计。
核心字段介绍
//注册失败的URL Key是注册失败的 URL,Value 是对应的重试任务
private final ConcurrentMap<URL, FailedRegisteredTask> failedRegistered = new ConcurrentHashMap<URL, FailedRegisteredTask>();
//取消注册失败URL
private final ConcurrentMap<URL, FailedUnregisteredTask> failedUnregistered = new ConcurrentHashMap<URL, FailedUnregisteredTask>();
//订阅失败的URL
private final ConcurrentMap<Holder, FailedSubscribedTask> failedSubscribed = new ConcurrentHashMap<Holder, FailedSubscribedTask>();
//取消订阅失败的URL
private final ConcurrentMap<Holder, FailedUnsubscribedTask> failedUnsubscribed = new ConcurrentHashMap<Holder, FailedUnsubscribedTask>();
//重试的时间间隔
private final int retryPeriod;
//用于定时执行失败的时间轮
private final HashedWheelTimer retryTimer;
构造方法首先会调用父类的构造方法完成本地缓存相关的初始化操作,然后根据传入URL参数中获取重试操作的时间间隔来初始化 retryPeriod 字段,最后初始化 HashedWheelTimer时间轮。
public FailbackRegistry(URL url) {
//调用
super(url);
this.retryPeriod = url.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD);
retryTimer = new HashedWheelTimer(new NamedThreadFactory("DubboRegistryRetryTimer", true), retryPeriod, TimeUnit.MILLISECONDS, 128);
}
核心方法
register
register方法重写了父类的注册方法,首先调用父类的register将url加入对应的容器,然后从failedRegistered 和failedUnregistered 两个容器中移除失败URL,然后执行doRegister方法,doRegister是抽象方法,具体的实现交给其继承者,如果注册失败抛出异常,会将URL加入failedRegistered 容器中。
@Override
public void register(URL url) {
if (!acceptable(url)) {
logger.info("URL " + url + " will not be registered to Registry. Registry " + url + " does not accept service of this protocol type.");
return;
}
//执行父类的方法加入到容器中
super.register(url);
//移除注册失败
removeFailedRegistered(url);
//移除取消注册失败
removeFailedUnregistered(url);
try {
//抽象方法交给具体子类去实现
doRegister(url);
} catch (Exception e) {
Throwable t = e;
// If the startup detection is opened, the Exception is thrown directly.
boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
&& url.getParameter(Constants.CHECK_KEY, true)
&& !CONSUMER_PROTOCOL.equals(url.getProtocol());
boolean skipFailback = t instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
} else {
logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
}
//注册发生异常将注册失败放入到注册失败的容器中
addFailedRegistered(url);
}
}
接下来我们看下添加重试任务的方法addFailedRegistered,该方法相对比较简单,核心就是将失败的任务放到容器中,然后将失败的任务加入时间轮等待执行。
private void addFailedRegistered(URL url) {
//判断容器中是是否存在任务
FailedRegisteredTask oldOne = failedRegistered.get(url);
if (oldOne != null) {
return;
}
//将任务添加容器中
FailedRegisteredTask newTask = new FailedRegisteredTask(url, this);
oldOne = failedRegistered.putIfAbsent(url, newTask);
if (oldOne == null) {
// never has a retry task. then start a new task for retry.
//将任务提交到时间轮中 等待retryPeriod秒后执行
retryTimer.newTimeout(newTask, retryPeriod, TimeUnit.MILLISECONDS);
}
}
对于其他unregister()、subscribe()、unsubscribe() 都与register()类似这里就不做过多介绍,简单看下提供几个抽象的方法。
public abstract void doRegister(URL url);
public abstract void doUnregister(URL url);
public abstract void doSubscribe(URL url, NotifyListener listener);
public abstract void doUnsubscribe(URL url, NotifyListener listener);
重试任务
addFailedRegistered方法中创建FailedRegisteredTask以及其他重试任务,都是继承AbstractRetryTask,接下来我们要来关于AbstractRetryTask的设计和实现。

在AbstractRetryTask抽象类中,有一个核心run方法实现已经一个抽象方法,该抽象方法也是模板类作用。
@Override
public void run(Timeout timeout) throws Exception {
//检查定义任务状态以及时间轮状态
if (timeout.isCancelled() || timeout.timer().isStop() || isCancel()) {
// other thread cancel this timeout or stop the timer.
return;
}
//检查重试次数
if (times > retryTimes) {
// reach the most times of retry.
logger.warn("Final failed to execute task " + taskName + ", url: " + url + ", retry " + retryTimes + " times.");
return;
}
if (logger.isInfoEnabled()) {
logger.info(taskName + " : " + url);
}
try {
//执行重试
doRetry(url, registry, timeout);
} catch (Throwable t) { // Ignore all the exceptions and wait for the next retry
logger.warn("Failed to execute task " + taskName + ", url: " + url + ", waiting for again, cause:" + t.getMessage(), t);
// reput this task when catch exception.
//执行异常 则重新等待
reput(timeout, retryPeriod);
}
}
protected void reput(Timeout timeout, long tick) {
//边界值检查
if (timeout == null) {
throw new IllegalArgumentException();
}
//检查定时任务
Timer timer = timeout.timer();
if (timer.isStop() || timeout.isCancelled() || isCancel()) {
return;
}
//递增times
times++;
//添加下次定时任务
timer.newTimeout(timeout.task(), tick, TimeUnit.MILLISECONDS);
}
protected abstract void doRetry(URL url, FailbackRegistry registry, Timeout timeout);
接下来我们看下FailedRegisteredTask对AbstractRetryTask�的实现,子类doRetry方法会执行关联Registry的doRegister() 方法,完成与服务发现组件交互。如果注册成功,则会调用removeFailedRegisteredTask()方法将当前关联的 URL 以及当前重试任务从 failedRegistered集合中删除。如果注册失败,则会抛出异常,执行AbstractRetryTask的reput()方法重试。
@Override
protected void doRetry(URL url, FailbackRegistry registry, Timeout timeout) {
// 重新注册
registry.doRegister(url);
// 删除注册任务
registry.removeFailedRegisteredTask(url);
}
未完待续
下一篇会简单介绍一下时间轮、ZK对注册中心的实现以及在,欢迎大家点点关注,点点赞!

Dubbo-聊聊注册中心的设计的更多相关文章
- Dubbo多注册中心和Zookeeper服务的迁移
一.Dubbo多注册中心 1. 应用场景 例如阿里有些服务来不及在青岛部署,只在杭州部署,而青岛的其它应用需要引用此服务,就可以将服务同时注册到两个注册中心. consumer.xml <?xm ...
- Dubbo配置注册中心设置application的name使用驼峰命名法存在的隐藏项目启动异常问题
原创/朱季谦 首先,先提一个建议,在SpringBoot+Dubbo项目中,Dubbo配置注册中心设置的application命名name的值,最好使用xxx-xxx-xxx这样格式的,避免随便使用驼 ...
- 如果有人问你 Dubbo 中注册中心工作原理,就把这篇文章给他
注册中心作用 开篇首先想思考一个问题,没有注册中心 Dubbo 还能玩下去吗? 当然可以,只要知道服务提供者地址相关信息,消费者配置之后就可以调用.如果只有几个服务,这么玩当然没问题.但是生产服务动辄 ...
- Dubbo Multicast 注册中心即相关代码实现
Dubbo 的 Multicast注册中心有下面特点: 不需要启动任何中心节点,只要广播地址一样,就可以互相发现 组播受网络结构限制,只适合小规模应用或开发阶段使用. 组播地址段: 224.0.0.0 ...
- Javaconfig形式配置Dubbo多注册中心
多注册中心,一般用不到,但是某些情况下的确能解决不少问题,可以将某些dubbo服务注册到2套dubbo系统中,实现服务在2套系统间的共用. 网上的配置说明很多,但包括dubbo官方说明文档都是以xml ...
- Dubbo及注册中心原理
本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天是猿灯塔“365天原创计划”第4天. 今天呢!灯塔君跟大家讲: 一.Dubbo意义 网站应用的架构变化经历了一个从所有服务分布在一台服务器上(A ...
- 9. 细节见真章,Formatter注册中心的设计很讨巧
目录 本文提纲 版本约定 你好,我是A哥(YourBatman). Spring设计了org.springframework.format.Formatter格式化器接口抽象,对格式化器进行了大一统, ...
- Dubbo多注册中心
一.创建提供者08-provider-registers (1) 创建工程 直接复制05-provider-group工程,并命名为08-provider-registers (2) 修改配置文件 二 ...
- dubbo 多注册中心
这个我调试了下,多个注册中心在创建代理的时候,每个注册中心对应一个invoker,持有一个RegistryDirectory对应一个zkClinet,并且维护这样一个map: 那些不正确zk在创建代理 ...
随机推荐
- Win10使用fvm管理多个Flutter版本
Win10使用fvm管理多个Flutter版本 参考:https://blog.csdn.net/PyMuma/article/details/115298645 1.升级Flutter 由于现在的f ...
- 应用集成-在Hexo、Hugo博客框架中使用Gitalk基于Github上仓库项目的issue无后端服务评论系统实践
关注「WeiyiGeek」公众号 设为「特别关注」每天带你玩转网络安全运维.应用开发.物联网IOT学习! 希望各位看友[关注.点赞.评论.收藏.投币],助力每一个梦想. 本章目录 目录 0x00 Gi ...
- linux centos7开启防火墙端口
firewall-cmd --zone=public --add-port=3306/tcp --permanent firewall-cmd --reload
- kubeadm部署k8s v1.19.4版本集群
1. 准备2台2c4g虚机 配置地址192.168.198.144,192.168.198.146,一台作为master,一台作为node 2. 部署环境准备,每一台虚机都需要操作 # 关闭防火墙sy ...
- MyBatis快速上手与知识点总结
目录 1.MyBatis概述 1.1 MyBatis概述 1.2 JDBC缺点 1.3 MyBatis优化 2.MyBatis快速入门 3.Mapper代理开发 3.1 Mapper代理开发概述 3. ...
- 常用的SSH,你了解多少?(长文警告)
1.SSH工作原理 从ssh的加密方式说开去,看下文 1.1.对称加密 客户端和服务端采用相同的密钥进行数据的加解密,很难保证密钥不丢失,或者被截获.隐藏着中间人攻击的风险 如果攻击者插在用户与远程主 ...
- KingbaseES interval 分区表介绍
KingbaseES从V008R006C005B0041版本开始支持Oracle的Interval分区表功能. Interval分区表是一种特殊的范围分区表.当执行INSERT或者UPDATE时,若数 ...
- AD画板从头开始
AD画板从头开始 前言 近期认真的画了一次板子,以前虽然也画过,但是都是很随意的,这次是做一个小项目,然后因为有一段时间没有画板了,发现自己很多基础的东西都忘记了,这里就来记录一下从头到尾的过程.本次 ...
- mocha、chai和supertest单元测试
mocha单元测试 1. 因为有时候在代码中加了新的东西需要反复测试接口 或者 别人要求 重新跑接口非常的繁琐 2. 所有我们需要一个帮我们重复测试的东西 那就是mocha 3. 先下载 一定不要全 ...
- pod(一):Kubernetes(k8s)创建pod的两种方式
目录 一.系统环境 二.前言 三.pod 四.创建pod 4.1 环境介绍 4.2 使用命令行的方式创建pod 4.2.1 创建最简单的pod 4.2.2 创建pod,指定镜像下载策略 4.2.3 创 ...