目录

声明

原创文章,转载请标注。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. [转帖]15分钟了解TiDB

    https://zhuanlan.zhihu.com/p/338947811 由于目前的项目把mysql换成了TiDb,所以特意来了解下tidb.其实也不能说换,由于tidb和mysql几乎完全兼容, ...

  2. [转帖]【有效解决】Edge浏览器提示你的连接不是专用连接怎么办?

    https://www.xitongzhijia.net/xtjc/20230524/290887.html Win11正式版iso镜像最新(22H2新版) V2023 大小:4.22 GB类别:Wi ...

  3. [转帖]Java和Scala的前世今生

    第一部分:Java 计算机语言介绍 第一代语言:机器语言.指令以二进制代码形式存在 第二代语言:汇编语言.使用助记符表示一条机器指令 第三代语言:高级语言 C.Pascal.Fortran面向过程的语 ...

  4. 【转帖】ChatGPT的前身:InstructGPT

    https://www.jianshu.com/p/6daf35cbc46a ChatGPT的论文目前还没有发布,在其官方博客(https://openai.com/blog/chatgpt/)中对方 ...

  5. CS231N Assignment1 SVM 笔记

    svm.ipynb 为SVM实现一个完全矢量化的损失函数 为其解析梯度实现完全矢量化表达式 使用数值梯度检查实现结果 使用验证集调整学习率和正则化 使用 SGD 优化损失函数 可视化最终学习权重 第一 ...

  6. 过滤器filters对时间格式的处理

    在表格中,我们经常会对时间格式进行处理: 这个时候,我们就可以使用过滤器了. 过滤器是不会,改变原始值 {{ mess | dotime }} {{ mess | do2time }} mess: & ...

  7. 使用protobuf生成代码import包找不到

    protobuf使用import导入包找不到 前言 解决方案 protobuf使用import导入包找不到 前言 使用protobuf生成go代码,发现protobuf中一个import引用找不到 p ...

  8. Spring WebSocket实现实时通信的详细教程

    简介 WebSocket 是基于TCP/IP协议,独立于HTTP协议的通信协议.WebSocket 连接允许客户端和服务器之间的全双工通信,以便任何一方都可以通过已建立的连接将数据推送到另一方. 我们 ...

  9. 20.5 OpenSSL 套接字RSA加密传输

    RSA算法同样可以用于加密传输,但此类加密算法虽然非常安全,但通常不会用于大量的数据传输,这是因为RSA算法加解密过程涉及大量的数学运算,尤其是模幂运算(即计算大数的幂模运算),这些运算对于计算机而言 ...

  10. Rsync+Inotify 实现数据同步

    Rsync 是UNIX及类UNIX-Like平台下一款强大的数据镜像备份软件,它不像FTP或其他文件传输服务那样需要进行全备份,Rsync 可以根据数据的变化进行差异备份,从而减少数据流量,提高工作效 ...