目录

声明

原创文章,转载请标注。https://www.cnblogs.com/boycelee/p/17993697

《码头工人的一千零一夜》是一位专注于技术干货分享的博主,追随博主的文章,你将深入了解业界最新的技术趋势,以及在Java开发和安全领域的实用经验分享。无论你是开发人员还是对逆向工程感兴趣的爱好者,都能在《码头工人的一千零一夜》找到有价值的知识和见解。

配置中心系列文章

《【架构师视角系列】Apollo配置中心之架构设计(一)》https://www.cnblogs.com/boycelee/p/17967590

《【架构师视角系列】Apollo配置中心之Client端(二)》https://www.cnblogs.com/boycelee/p/17978027

《【架构师视角系列】Apollo配置中心之Server端(ConfigSevice)(三)》https://www.cnblogs.com/boycelee/p/18005318

《【架构师视角系列】QConfig配置中心系列之架构设计(一)》https://www.cnblogs.com/boycelee/p/18013653

《【架构师视角系列】QConfig配置中心系列之Client端(二)》https://www.cnblogs.com/boycelee/p/18033286

一、架构

一、客户端架

架构介绍会从分层、职责、关系以及运行负责四个维度进行描述。

1、Server 职责

(1)配置管理

Server 是QConfig配置中心的服务端组件,负责管理应用程序的配置信息。它存储和维护应用程序的各种配置项。

(2)配置发布

Server 负责将最新的配置发布给注册在它上面的Client。当配置发生变更时,Config Service 负责通知所有订阅了相应配置的客户端。

(3)配置读取

Client 向 Server 发送请求,获取应用程序的配置信息。

2、Client 职责

(1)配置拉取

Client 负责向 Server 发送配置拉取请求,获取三方应用程序的配置。

(2)配置注入

Client 将从 Server 获取到的配置注入到三方应用程序中。

(3)配置变更监听

Client 可以注册对配置变更的监听器。当 Server 发布新的配置时, Client 能够感知到配置的变更,并触发相应的操作。

3、基本交互流程

(1)应用启动

Client 在应用启动时向 Server 发送配置拉取请求,获取初始的配置。

(2)配置变更通知

Server 在配置发生变更时,通知所有注册的 Client。

(3)配置更新

Client 接收到配置变更通知后,向 Server 发送请求,获取最新的配置。

(4)配置注入

Client 将获取到的最新配置注入到应用程序中,以便使用最新的配置信息。

通过以上交互流程达到应用不需要重启,动态配置变更的目的。

二、架构思考

架构师视角系列,在分析一款组件的源码时,需要深入思考其设计背后的动机。以下是读者在阅读本篇文章时应思考的问题:

  1. 配置拉取的设计:
  • 思考点: 设计中采用的配置拉取方式是如何选择的?背后的动机是什么?可能的考虑包括系统性能、可维护性和安全性。
  1. 配置的注入方式:
  • 思考点: 配置是如何被注入到组件中的?这种注入方式有何优势?设计选择的原因可能涉及松耦合、动态变化和代码可维护性等方面。
  1. 配置变更的通知机制:
  • 思考点: 配置变更是如何通知其他组件的?为什么选择当前的通知机制?可能的考虑包括实时性、效率以及系统整体的架构要求。
  1. 为什么配置拉取拆分为两个请求?
  • 思考点: 配置拉取为何拆分为两个独立的请求?这个设计决策的目的是什么?可能涉及到性能优化、可伸缩性以及减轻服务器负担的考虑。
  1. 长轮询的概念:
  • 思考点: 什么是长轮询?为何在配置方案中选择使用它?长轮询的优势在哪里?可能涉及到减少轮询频率、降低网络开销以及更及时的配置变更通知。
  1. 为什么需要做本地文件缓存?
  • 思考点: 为什么在组件中引入了本地文件缓存的机制?这样的设计有哪些优点?可能牵涉到性能优化、离线支持以及用户体验的方面。

在深入研究源码时,理解这些设计决策背后的原因,有助于更全面地理解系统架构,并为自己的设计提供有价值的启示。

三、源码剖析

1、注解初始化

1.1、逻辑描述

通过实现Spring框架提供的BeanPostProcessor接口,并完成postProcessBeforeInitialization函数的实现,我们能够在Bean初始化之前执行自定义的操作。BeanPostProcessor是Spring框架提供的一个扩展点,允许我们在Bean初始化前后插入自定义逻辑。在postProcessBeforeInitialization函数中,我们有机会遍历Bean的成员变量和函数,实现在初始化之前对它们进行定制化处理的需求。

1.2、时序图

1.3、代码位置

QConfigAnnotationProcessor#postProcessBeforeInitialization
class QConfigAnnotationProcessor extends PropertyPlaceholderConfigurer implements BeanPostProcessor, ApplicationContextAware, BeanClassLoaderAware {

    private boolean trimValue;

    private final Map<Class, AnnotationManager> managers = new HashMap<>();

    @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class<?> clazz = getRealClass(bean);
// 如果标记有DisableQConfig,则跳过
if (AnnotationUtils.isAnnotationDeclaredLocally(DisableQConfig.class, clazz)) return bean;
// 获取或创建manager,为所有的Spring Bean
AnnotationManager manager = getOrCreateManager(clazz);
// manager初始化
manager.init(clazz);
// 初始化后,首次执行注入动作
manager.processBean(bean);
return bean;
}
}

2、查找注解

2.1、逻辑描述

根据注解的Bean、Field、Method三种使用方式,进行注解扫描。其中动作加载数据、注册监听器,当监听到配置变更时执行act函数进行数据变更,如Method上的注解就通过反射的方式实现数据变更。

2.2、时序图

2.3、代码位置

AnnotationManager#init
class AnnotationManager {

    /**
* 初始化。分别有三种维度的注解,分别为Bean、Field、Method
* @param clazz
*/
void init(Class<?> clazz) {
if (inited) return;
synchronized (INIT_LOCK) {
if (inited) return;
// 处理Bean上的注解
this.beanProcessor = new BeanProcessor(clazz, this);
// 处理变量上注解
this.fieldProcessor = new FieldProcessor(clazz, this);
// 处理函数上注解(篇幅有限,只关注函数上注解)
this.methodProcessor = new MethodProcessor(clazz, this);
inited = true;
} }
}
MethodProcessor#MethodProcessor
class MethodProcessor {
private final List<Processor> processors; MethodProcessor(Class<?> clazz, AnnotationManager manager) {
ProcessorFactory factory = new ProcessorFactory();
processors = new ArrayList<>(); // 遍历所有声明的Method
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
Annotation annotation = manager.extractAnnotation(method);
if (annotation == null || annotation instanceof DisableQConfig) {
continue;
} Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1) {
throw new RuntimeException("method receives qconfig change must be on parameter, method: " + method);
} Map.Entry<Util.File, Feature> fileInfo = manager.getFileInfo(annotation);
Action action = new MethodInvoker(fileInfo.getKey(), method);
// 为标记有注解的Method创建一个Porcessor
factory.create(annotation, parameterTypes[0], method.getGenericParameterTypes()[0], fileInfo, action, processors, manager);
}
} public boolean process(Object bean) {
if (processors.isEmpty()) return false;
// 遍历所有processor(Method),通过反射的方式调用目标函数,替换新value
for (Processor processor : processors) {
processor.process(bean);
}
return true;
}
}
JsonProcessor#process
class JsonProcessor implements Processor {
private final JsonConfig config;
private final Action action;
private final QConfigLogLevel logLevel; JsonProcessor(Type genericType, String appCode, String file, Feature feature, final Action action, final QConfigLogLevel logLevel, final AnnotationManager manager) {
this.action = action;
this.logLevel = logLevel;
// 加载数据
config = JsonConfig.get(appCode, file, feature, JsonConfig.ParameterizedClass.of(genericType));
// 注册监听器
AnnotationListenerManager.getInstance().addAnnotationListener(config, new Configuration.ConfigListener() {
@Override
public void onLoad(Object conf) {
manager.process(action, conf, logLevel);
}
});
} public void process(Object bean) {
action.act(bean, config.current(), logLevel);
}
}
MethodInvoker#act
class MethodInvoker implements Action {

    @Override
public void act(Object bean, Object newValue, QConfigLogLevel logLevel) {
try {
// 调用目标函数,替换新value,bean为对象,method为反射调用的函数
method.invoke(bean, newValue); LogUtil.log(this, logLevel, this.value, newValue);
this.value = newValue;
} catch (Exception e) {
LOG.error("receive qconfig change error, class {}, method {}, file [{}], group [{}]", getClazz().getName(), method.getName(), file.file, file.group, e);
throw new RuntimeException(e);
}
}
}

3、配置初始化

2.1、逻辑描述

客户端启动首次加载配置,会首先读取本地存储的配置文件,然后将配置文件的版本信息发送至Server端,若Server端根据上传的版本信息判断有新的版本,则会通知client端,这时client端就会发起拉取配置文件的请求。

2.2、代码位置

AbstractDataLoader#preLoadLocal
abstract class AbstractDataLoader implements DataLoader {

    private static final LongPoller LONGPOLLER = new LongPoller(USED_CONFIGS, VERSIONS, new LongPoller.ConfigChangedCallback() {
@Override
public Optional<CountDownLatch> onChanged(Map<Meta, VersionProfile> map, TypedCheckResult changed) {
return loadIfUpdated(map, changed);
}
}); private static final QConfigServerClient CLIENT = QConfigServerClientFactory.create();
private static final ConfigLogger CONFIG_LOGGER = new HttpConfigLogger(CLIENT); static {
// 客户端启动首次加载
preLoadLocal();
LONGPOLLER.start();
} /**
* 根据本地之前缓存的配置预加载
*/
private static void preLoadLocal() {
//读取本地所有[版本文件]
final Map<Meta, VersionProfile> localVersions = FileStore.findAllFiles(); for (Map.Entry<Meta, VersionProfile> entry : localVersions.entrySet()) {
VERSIONS.put(entry.getKey(), new Version(entry.getValue()));
} try {
// 客户端本地
Optional<CountDownLatch> holder = checkUpdates(new HashMap<Meta, VersionProfile>(localVersions));
if (!holder.isPresent()) return; CountDownLatch latch = holder.get();
latch.await(2, TimeUnit.SECONDS);
} catch (Throwable e) {
LOG.warn("初始化出错,强制载入本地缓存配置!", e);
forceLoadLocalCache(localVersions);
}
} private static Optional<CountDownLatch> checkUpdates(Map<Meta, VersionProfile> versions) throws Exception {
if (versions == null || versions.isEmpty()) return Optional.absent(); // 获取新版本(远程)
TypedCheckResult remote = CLIENT.checkUpdate(versions).get(); // 加载配置数据
return loadIfUpdated(versions, remote);
}
}

3、建立连接(变更通知)

3.1、逻辑描述

Client端会与Server端建立长轮询,通过长轮询的方式获取最新版本。

3.2、代码位置

LongPoller#run
class LongPoller implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(LongPoller.class);

    private static final TomcatStateViewer TOMCAT_STATE = TomcatStateViewer.getInstance();

    private static final long OVERRIDE_CHECK_INTERVAL = 60 * 1000L;

    private volatile AtomicBoolean initialed = new AtomicBoolean(false);

    private static final QConfigServerClient CLIENT = QConfigServerClientFactory.create();
private static final Random LONG_POLLING_RANDOM = new Random();
private static final ScheduledExecutorService LONG_POLLING_EXECUTOR = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("qconfig-poller#")); private final Map<String, FileStore> usedConfigs;
private final Map<Meta, AbstractDataLoader.Version> localVersions;
private ConfigChangedCallback callback; LongPoller(Map<String, FileStore> usedConfigs, Map<Meta, AbstractDataLoader.Version> localVersions, ConfigChangedCallback callback) {
this.usedConfigs = usedConfigs;
this.localVersions = localVersions;
this.callback = callback;
} @Override
public void run() {
// 判断tomcat是否启动
while (TOMCAT_STATE.isStopped()) {
try {
logger.debug("tomcat is stopped, qconfig sleep");
Thread.sleep(5000L);
} catch (InterruptedException e) {
logger.warn("tomcat stop sleep interrupted", e);
return;
}
} logger.debug("start qconfig reloading");
try {
// 获取最新版本,加载数据
Optional<CountDownLatch> latch = reLoading();
if (latch.isPresent()) {
if (!latch.get().await(20, TimeUnit.SECONDS)) {
logger.warn("20 seconds elapsed and qconfig file change loading not finish, perhaps something wrong");
}
LONG_POLLING_EXECUTOR.execute(this);
} else {
long emptyCheckDelay;
if (initialed.compareAndSet(false, true)) {
emptyCheckDelay = 3 * 1000L;
} else {
emptyCheckDelay = 30 * 1000L;
}
LONG_POLLING_EXECUTOR.schedule(this, emptyCheckDelay, TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
logger.info("long-polling check update error", e);
long delay = LONG_POLLING_RANDOM.nextInt(60 * 1000);
LONG_POLLING_EXECUTOR.schedule(this, delay, TimeUnit.MILLISECONDS);
}
} void start() {
LONG_POLLING_EXECUTOR.execute(this);
} private Optional<CountDownLatch> reLoading() throws Exception {
Map<Meta, VersionProfile> map = Maps.newHashMap(); for (FileStore store : usedConfigs.values()) {
if (!store.getFeature().isAutoReload()) {
continue;
} boolean hasOverride = store.checkOverride(OVERRIDE_CHECK_INTERVAL);
if (hasOverride) {
continue;
} AbstractDataLoader.Version ver = localVersions.get(store.getMeta());
map.put(store.getMeta(), ver == null ? VersionProfile.ABSENT : ver.updated.get());
} if (map.isEmpty()) return Optional.absent(); // 通过长轮询,获取最新版本。
TypedCheckResult remote = CLIENT.longPollingCheckUpdate(map).get();
// 通知配置变更
return this.callback.onChanged(map, remote);
} interface ConfigChangedCallback {
Optional<CountDownLatch> onChanged(Map<Meta, VersionProfile> map, TypedCheckResult changed);
}
}
AbstractDataLoader#onChanged
abstract class AbstractDataLoader implements DataLoader {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDataLoader.class); private static final ConcurrentMap<Meta, Version> VERSIONS = new ConcurrentHashMap<Meta, Version>(); private static final ConcurrentMap<String, FileStore> USED_CONFIGS = new ConcurrentHashMap<String, FileStore>(); //qconfig的配置变更listener在这个线程池里执行
private static final Executor EXECUTOR = new ThreadPoolExecutor(1, Integer.MAX_VALUE, 30L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new NamedThreadFactory("qconfig-worker#")); private static final LongPoller LONGPOLLER = new LongPoller(USED_CONFIGS, VERSIONS, new LongPoller.ConfigChangedCallback() {
@Override
public Optional<CountDownLatch> onChanged(Map<Meta, VersionProfile> map, TypedCheckResult changed) {
// 配置加载(核心)
return loadIfUpdated(map, changed);
}
}); ...
}

4、拉取配置

4.1、逻辑描述

当Client端与Server端建立长轮询后,如果有新版本通知,则Client端会执行配置加载操作。拉取配置时,会首先判断版本号是否小于远端版本号,如果小于则判断本地是否有该版本数据,如果没有则拉取远端配置数据。

4.3、代码位置

abstract class AbstractDataLoader implements DataLoader {

    //qconfig的配置变更listener在这个线程池里执行
private static final Executor EXECUTOR = new ThreadPoolExecutor(1, Integer.MAX_VALUE, 30L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new NamedThreadFactory("qconfig-worker#")); private static final LongPoller LONGPOLLER = new LongPoller(USED_CONFIGS, VERSIONS, new LongPoller.ConfigChangedCallback() {
@Override
public Optional<CountDownLatch> onChanged(Map<Meta, VersionProfile> map, TypedCheckResult changed) {
return loadIfUpdated(map, changed);
}
}); ... private static Optional<CountDownLatch> checkUpdates(Map<Meta, VersionProfile> versions) throws Exception {
if (versions == null || versions.isEmpty()) return Optional.absent(); // 获取新版本
TypedCheckResult remote = CLIENT.checkUpdate(versions).get(); // 加载配置数据
return loadIfUpdated(versions, remote);
} /**
* 配置加载(核心)
* @param versions
* @param remote
* @return
*/
private static Optional<CountDownLatch> loadIfUpdated(Map<Meta, VersionProfile> versions, TypedCheckResult remote) {
final CountDownLatch latch = new CountDownLatch(versions.size()); for (Map.Entry<Meta, VersionProfile> entry : versions.entrySet()) {
final Meta key = entry.getKey();
final Version localVersion = VERSIONS.get(key);
VersionProfile remoteVersion = remote.getResult().get(key);
loadIfUpdated(key, localVersion, remoteVersion, latch);
} return Optional.of(latch);
} private static void loadIfUpdated(Meta fileMeta, Version localVersion, VersionProfile remoteVersion, CountDownLatch latch) {
if (localVersion == null) {
latch.countDown();
return;
} if (localVersion.updated.get().needUpdate(remoteVersion)) {
// 加载配置(本地查不到就查远程)
updateFile(fileMeta, remoteVersion, latch);
} else {
latch.countDown();
if (remoteVersion != null && remoteVersion.getVersion() <= Constants.PURGE_FILE_VERSION) {
FileStore.purgeAllRelativeFiles(fileMeta);
}
localVersion.setLoaded();
}
} private static void updateFile(final Meta meta, final VersionProfile newVersion, final CountDownLatch latch) {
// 本地文件是否能查找到?
if (foundInLocal(newVersion, meta)) {
EXECUTOR.execute(new Runnable() {
@Override
public void run() {
updateVersion(meta, newVersion, latch, null);
setLoaded(meta);
}
}); return;
} // 本地查不到就走下面的逻辑
final FileStore fileStore = USED_CONFIGS.get(meta.getKey());
// 远端拉取
final ListenableFuture<Snapshot<String>> future = CLIENT.loadData(meta, newVersion, fileStore == null ? Feature.DEFAULT : fileStore.getFeature());
future.addListener(new Runnable() {
public void run() {
try {
Snapshot<String> snapshot = future.get();
try {
FileStore.storeData(meta, newVersion, snapshot);
} catch (Throwable e) {
LOG.warn("缓存配置到本地磁盘失败", meta);
latch.countDown();
return;
}
updateVersion(meta, newVersion, latch, snapshot);
} catch (Throwable e) {
LOG.warn("获取文件错误!", e);
} finally {
setLoaded(meta);
}
}
}, EXECUTOR);
} }

6、配置注入

6.1、逻辑描述

当配置发生变化时,会首先将变化的配置信息存储至本地文件,然后触发配置变更逻辑,通知注册的监听器。

6.2、代码位置

AbstractDataLoader#updateVersion
abstract class AbstractDataLoader implements DataLoader {

    private static final ConcurrentMap<Meta, Version> VERSIONS = new ConcurrentHashMap<Meta, Version>();

    private static final ConcurrentMap<String, FileStore> USED_CONFIGS = new ConcurrentHashMap<String, FileStore>();

    //qconfig的配置变更listener在这个线程池里执行
private static final Executor EXECUTOR = new ThreadPoolExecutor(1, Integer.MAX_VALUE, 30L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new NamedThreadFactory("qconfig-worker#")); private static void updateVersion(final Meta meta, final VersionProfile newVersion, CountDownLatch latch, Snapshot<String> snapshot) {
Version ver = VERSIONS.get(meta); if (ver == null) {
VERSIONS.putIfAbsent(meta, new Version(VersionProfile.ABSENT));
ver = VERSIONS.get(meta);
} VersionProfile uVer = ver.updated.get();
boolean versionChanged = uVer.needUpdate(newVersion) && ver.updated.compareAndSet(uVer, newVersion);
latch.countDown();
if (versionChanged) {
FileStore store = USED_CONFIGS.get(meta.getKey());
if (store != null)
versionChanged(store, snapshot);
}
} private static void versionChanged(FileStore store, Snapshot<String> snapshot) {
VersionProfile version = VERSIONS.get(store.getMeta()).updated.get();
try {
store.setVersion(version, snapshot);
} catch (Exception e) {
LOG.warn("文件载入失败: meta: {}, version: {}", store.getMeta(), version, e);
}
}
}
class FileStore<T> {

    synchronized void setVersion(VersionProfile version, Snapshot<String> snapshot) throws Exception {
String data; if (snapshot != null) {
data = snapshot.getContent();
} else {
data = loadSnapshot(version);
} // 执行数据merge
data = templateTool.merge(meta.getFileName(), data); T t;
try {
t = conf.parse(data);
} catch (Throwable e) {
configLogger.log(ConfigLogType.PARSE_REMOTE_ERROR, meta, version.getVersion(), e);
throw new RuntimeException(e);
} FileVersion current = currentVersion.get();
FileVersion newVer = new FileVersion(FileVersion.Type.remote, version); if (FileVersion.needUpdate(current, newVer) && currentVersion.compareAndSet(current, newVer)) { if (storeAtLocal(snapshot)) {
saveToConfigRepository(meta, newVer.getVersion().getVersion(), data);
File versionFile = getVersionFile();
atomicWriteFile(versionFile, Long.toString(version.getVersion(), 10) + COMMA + version.getProfile());
} //触发配置变更逻辑
boolean success = conf.setData(t); log.info("use remote file, name={}, version={}", meta.getFileName(), version);
String message = Constants.EMPTY;
if (!success) {
message = "listener error";
}
configLogger.log(ConfigLogType.USE_REMOTE_FILE, meta, version.getVersion(), message);
purge(version);
purgedFiles.remove(meta);
}
} }
AbstractConfiguration#setData
public abstract class AbstractConfiguration<T> implements Configuration<T> {

   /**
* 配置变化setData
* @param data
* @return
*/
boolean setData(T data) {
return setData(data, true);
} /**
* 配置变化setData
* @param data
* @return
*/
boolean setData(T data, boolean trigger) {
synchronized (current) { current.set(data);
onChanged(); if (!future.isDone()) {
future.set(true);
} // 数据变化触发器
return triggers(data, trigger);
}
} private boolean triggers(T data, boolean trigger) {
if (!trigger) return true;
boolean result = true;
for (ConfigListener<T> listener : listeners) {
if (!trigger(listener, data)) result = false;
}
return result;
} private boolean trigger(ConfigListener<T> listener, T data) {
try {
// 配置变化,通知注册的监听器,并触发监听器的load函数
listener.onLoad(data);
return true;
} catch (Throwable e) {
log.error("配置文件变更, 事件触发异常. data: {}", data, e);
return false;
}
}
}

二、最后

《码头工人的一千零一夜》是一位专注于技术干货分享的博主,追随博主的文章,你将深入了解业界最新的技术趋势,以及在Java开发和安全领域的实用经验分享。无论你是开发人员还是对逆向工程感兴趣的爱好者,都能在《码头工人的一千零一夜》找到有价值的知识和见解。

懂得不多,做得太少。欢迎批评、指正。

【架构师视角系列】QConfig配置中心系列之Client端(二)的更多相关文章

  1. 从架构师视角看是否该用Kotlin做服务端开发?

    前言 自从Oracle收购Sun之后,对Java收费或加强控制的尝试从未间断,谷歌与Oracle围绕Java API的官司也跌宕起伏.虽然Oracle只是针对Oracle JDK8的升级收费,并释放了 ...

  2. SpringCloud系列——Config 配置中心

    前言 Spring Cloud Config为分布式系统中的外部化配置提供了服务器端和客户端支持.有了配置服务器,您就有了一个中心位置来管理跨所有环境的应用程序的外部属性.本文记录实现一个配置中心.客 ...

  3. ASP.Net Core 中使用Zookeeper搭建分布式环境中的配置中心系列一:使用Zookeeper.Net组件演示基本的操作

    前言:马上要过年了,祝大家新年快乐!在过年回家前分享一篇关于Zookeeper的文章,我们都知道现在微服务盛行,大数据.分布式系统中经常会使用到Zookeeper,它是微服务.分布式系统中必不可少的分 ...

  4. SpringCloud学习系列之五-----配置中心(Config)和消息总线(Bus)完美使用版

    前言 在上篇中介绍了SpringCloud Config的使用,本篇则介绍基于SpringCloud(基于SpringBoot2.x,.SpringCloud Finchley版)中的分布式配置中心( ...

  5. SpringCloud学习系列之四-----配置中心(Config)使用详解

    前言 本篇主要介绍的是SpringCloud中的分布式配置中心(SpringCloud Config)的相关使用教程. SpringCloud Config Config 介绍 Spring Clou ...

  6. SpringCloud系列之配置中心(Config)使用说明

    大家好,最近公司新项目采用SpingCloud全家桶进行开发,原先对SpringCloud仅仅只是停留在了解的初级层面,此次借助新项目的契机可以深入实践下SpringCloud,甚是Happy.大学毕 ...

  7. 基于docker部署的微服务架构(四): 配置中心

    原文:http://www.jianshu.com/p/b17d65934b58%20 前言 在微服务架构中,由于服务数量众多,如果使用传统的配置文件管理方式,配置文件分散在各个项目中,不易于集中管理 ...

  8. Spring Cloud构建微服务架构(四)分布式配置中心

    Spring Cloud Config为服务端和客户端提供了分布式系统的外部化配置支持.配置服务器为各应用的所有环境提供了一个中心化的外部配置.它实现了对服务端和客户端对Spring Environm ...

  9. springCloud系列 Config配置中心

    1.config服务的部署 2.yum文件的格式 大小写敏感 使用缩进表示层级关系 缩进时不允许使用Tab键,只允许使用空格. 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 3.热部署 4.配 ...

  10. Spring Cloud构建微服务架构(四)分布式配置中心(续)

    先来回顾一下,在前文中我们完成了什么: 构建了config-server,连接到Git仓库 在Git上创建了一个config-repo目录,用来存储配置信息 构建了config-client,来获取G ...

随机推荐

  1. [转帖]Kingbase实现Oracle userenv函数功能

    目录 1. 问题 2. 文档概述 3. Oracle userenv()函数功能调研 3.1. 函数名称/函数原型 3.2. 函数功能 3.3. 参数介绍 3.3.1. Parameter 3.4. ...

  2. [转帖]GC 日志

    https://www.xjx100.cn/news/188814.html?action=onClick 垃圾回收器的发展历史 1999年:随JDK1.3.1一起来的串行方式Serial GC(第一 ...

  3. [转帖]Linux内核映像vmlinux、Image、zImage、uImage区别

    https://zhuanlan.zhihu.com/p/466226177 本文介绍几种常用的Linux内核映像的区别. 一.vmlinux vmlinux:Linux内核编译出来的原始的内核文件, ...

  4. [转帖]013 Linux 搞懂「文件所属者更改及权限的赋予」从未如此简单 (chmod、chgrp、chown)

    https://my.oschina.net/u/3113381/blog/5435014   01 一图详解「ls -l」 02 两种符号区分表示文件和目录 -(横线) # 表示非目录文件 d # ...

  5. 转载:ubuntu各个版本的发行时间和停止支持的时间,更新到最新版和代号。

    版本:20.10  代号:Groovy Gorilla  发布时间:2020/10/22 版本:20.04 LTS   代号:Focal Fossa  发布时间:2020/4/23 版本:19.10  ...

  6. 初识C语言:掌握未来的编程利器

    ​ 欢迎大家来到贝蒂大讲堂 ​ 养成好习惯,先赞后看哦~ ​ 所属专栏:C语言学习 ​ 贝蒂的主页:Betty's blog 1. C语言是什么 在我们生活中,我们和父母.朋友.老师交流时候使用的就是 ...

  7. Spring Boot 统一RESTful接口响应和统一异常处理

    一.简介 基于Spring Boot 框架开发的应用程序,大部分都是以提供RESTful接口为主要的目的.前端或者移动端开发人员通过调用后端提供的RESTful接口完成数据的交换. 统一的RESTfu ...

  8. Vue3中shallowReactive和shallowRef对数据进行非深度监听

    1.Vue3 中 ref 和 reactive 都是深度监听 默认情况下, 无论是通过 ref 还是 reactive 都是深度监听. 深度监听存在的问题: 如果数据量比较大,非常消耗性能. 有些时候 ...

  9. js快速获取当前时间并且返回想要的格式

    function backCurrentTime (type) { let currentTime=new Date( new Date() + 8 * 3600 * 1000 ).toJSON(). ...

  10. Element-UI中Drawer抽屉去除标题自带黑色边框

    当点击事件drawer==true时,抽匣回打开 这时抽匣的标题会出现一个难看的蓝色边框,一会就会消失,但是好丑,所以要去掉它 解决方法 /deep/ :focus { outline: 0; } v ...