概述
在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件
quick start
-
引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
-
nacos 录入配置
nameSpace : 命名空间
data id : 配置文件id,默认{active}.${extFile}
group id: 组id
-
添加配置(bootStrap.yml,多文件配置)
spring:
application:
name: spring.cloud.web01
cloud:
nacos:
config:
server-addr: yserver:8848
group: spring.cloud.web
file-extension: yml
namespace: spring-cloud-web
extension-configs:
- data-id: application.yml
group: spring.cloud.web
refresh: true
- data-id: biz.yml
group: spring.cloud.web
refresh: true
extension-configs : 配置列表,参考yml 配置list
-
测试类
@Configuration
@RefreshScope
@Data
public class BizConfig {
@Value("${switchOff}")
private int switchOff;
}
@Configuration
@RefreshScope
@Data
public class RedisConfig {
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
// @Bean(name = "redisProperties")
// RedisProperties redisPropertiesConfig() {
// RedisProperties redisProperties = new RedisProperties();
// redisProperties.setDatabase(database);
// redisProperties.setHost(host);
// redisProperties.setPort(port);
// return redisProperties;
// }
}
@RestController
@RequestMapping("/config")
public class TestNacosController {
@Autowired
private BizConfig bizConfig;
@Autowired
private RedisConfig redisConfig;
@RequestMapping("/getSwitch")
public int get() {
return bizConfig.getSwitchOff();
}
@RequestMapping("/getRedisHost")
public String getRedisHost(){
return redisConfig.getHost();
}
}
spring cloud nacos 接入方式
-
原生注解方式
@RefreshScope
@Value("${key}")
@Configuration
@RefreshScope
@Data
public class BizConfig {
@Value("${switchOff}")
private int switchOff;
}
-
监听器方式
@Slf4j
@Component
public class Case1AlicloudNacosConfigBean implements InitializingBean {
@Autowired
private NacosConfigManager nacosConfigManager;
@Override
public void afterPropertiesSet() {
try {
ConfigService configService = nacosConfigManager.getConfigService();
addListener("price-admin", "basic", configService);
addListener("price-admin", "botConfig", configService);
addListener("price-admin", "smsConfig", configService);
} catch (NacosException e) {
e.printStackTrace();
}
}
private void addListener(String group, String dataId, ConfigService configService) throws NacosException {
//todo 反序列化
//添加监听
configService.addListener(dataId, group, getListener(dataId));
}
private Listener getListener(String dataId) {
return new Listener() {
@Override
public void receiveConfigInfo(String value) {
log.info("recieve: {}", dataId);
//todo 反序列化
}
@Override
public Executor getExecutor() {
return null;
}
};
}
}
nacos 原理
- 服务管理:实现服务CRUD,域名CRUD,服务健康状态检查,服务权重管理等功能
- 配置管理:实现配置管CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能
- 元数据管理:提供元数据CURD 和打标能力
- 插件机制:实现三个模块可分可合能力,实现扩展点SPI机制
- 事件机制:实现异步化事件通知,sdk数据变化异步通知等逻辑
- 日志模块:管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码+帮助文档
- 回调机制:sdk通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性
- 寻址模式:解决ip,域名,nameserver、广播等多种寻址模式,需要可扩展
- 推送通道:解决server与存储、server间、server与sdk间推送性能问题
- 容量管理:管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性
- 流量管理:按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制
- 缓存机制:容灾目录,本地缓存,server缓存机制。容灾目录使用需要工具
- 启动模式:按照单机模式,配置模式,服务模式,dns模式,或者all模式,启动不同的程序+UI
- 一致性协议:解决不同数据,不同一致性要求情况下,不同一致性机制
- 存储模块:解决数据持久化、非持久化存储,解决数据分片问题
- Nameserver:解决namespace到clusterid的路由问题,解决用户环境与nacos物理环境映射问题
- CMDB:解决元数据存储,与三方cmdb系统对接问题,解决应用,人,资源关系
- Metrics:暴露标准metrics数据,方便与三方监控系统打通
- Trace:暴露标准trace,方便与SLA系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打通
- 接入管理:相当于阿里云开通服务,分配身份、容量、权限过程
- 用户管理:解决用户管理,登录,sso等问题
- 权限管理:解决身份识别,访问控制,角色管理等问题
- 审计系统:扩展接口方便与不同公司审计系统打通
- 通知系统:核心数据变更,或者操作,方便通过SMS系统打通,通知到对应人数据变更
- OpenAPI:暴露标准Rest风格HTTP接口,简单易用,方便多语言集成
- Console:易用控制台,做服务管理、配置管理等操作
- SDK:多语言sdk
- Agent:dns-f类似模式,或者与mesh等方案集成
- CLI:命令行对产品进行轻量化管理,像git一样好用
nacos client
@RefreshScope
spring 中bean 的作用域
scope=singleton 单例
scope=prototype 多例
scope=request
scope=session
scope= global session
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean方法如下
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
/**
* @see Scope#proxyMode()
* @return proxy mode
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
...
// Create bean instance.
//单例
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
//多例
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
//其他
else {
String scopeName = mbd.getScope();
if (!StringUtils.hasLength(scopeName)) {
throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
}
Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new ScopeNotActiveException(beanName, scopeName, ex);
}
}
}
...
RefreshScope注解中清楚说明,它是@Scope("refresh"),就是上面代码的的第三种状况。
动态刷新配置缓存
- spring boot spi 注入几个核心类
- NacosContextRefresher 引入spring-cloud-starter-alibaba-nacos-config依赖,spring-cloud-starter-alibaba-nacos-config spring.factories 中找com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,如下图
NacosContextRefresher 实现了 ApplicationListener, ApplicationContextAware接口 ApplicationReadyEvent :Application 启动成功事件
下面来看com.alibaba.cloud.nacos.refresh.NacosContextRefresher 几个核心方法
//监听事件
public void onApplicationEvent(ApplicationReadyEvent event) {
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
private void registerNacosListenersForApplications() {
if (this.isRefreshEnabled()) {
Iterator var1 = NacosPropertySourceRepository.getAll().iterator();
while(var1.hasNext()) {
NacosPropertySource propertySource = (NacosPropertySource)var1.next();
if (propertySource.isRefreshable()) {
String dataId = propertySource.getDataId();
this.registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
}
//注册监听
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = (Listener)this.listenerMap.computeIfAbsent(key, (lst) -> {
return new AbstractSharedListener() {
public void innerReceive(String dataId, String group, String configInfo) {
NacosContextRefresher.refreshCountIncrement();
NacosContextRefresher.this.nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
//发布RefreshEvent事件
NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));
if (NacosContextRefresher.log.isDebugEnabled()) {
NacosContextRefresher.log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo));
}
}
};
});
try {
//添加监听
this.configService.addListener(dataKey, groupKey, listener);
} catch (NacosException var6) {
log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), var6);
}
}
- org.springframework.cloud.endpoint.event.RefreshEventListener监听
public class RefreshEventListener implements SmartApplicationListener {
private static Log log = LogFactory.getLog(RefreshEventListener.class);
private ContextRefresher refresh;
private AtomicBoolean ready = new AtomicBoolean(false);
public RefreshEventListener(ContextRefresher refresh) {
this.refresh = refresh;
}
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return ApplicationReadyEvent.class.isAssignableFrom(eventType)
|| RefreshEvent.class.isAssignableFrom(eventType);
}
//监听
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) {
//处理 RefreshEvent 事件
handle((RefreshEvent) event);
}
}
//设置ready=true,aqs
public void handle(ApplicationReadyEvent event) {
this.ready.compareAndSet(false, true);
}
//处理 RefreshEvent 事件
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
}
- org.springframework.cloud.context.refresh.ContextRefresher refresh 方法
public synchronized Set<String> refresh() {
// refresh environment
Set<String> keys = refreshEnvironment();
// refresh all
this.scope.refreshAll();
return keys;
}
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
//获取有变化的key
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
//发布事件
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
- org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder 监听
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
private ApplicationContext applicationContext;
private Map<String, Exception> errors = new ConcurrentHashMap<>();
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
this.beans = beans;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
/**
* A map of bean name to errors when instantiating the bean.
* @return The errors accumulated since the latest destroy.
*/
public Map<String, Exception> getErrors() {
return this.errors;
}
@ManagedOperation
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
@ManagedOperation
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
// TODO: determine a more general approach to fix this.
// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(bean.getClass().getName())) {
return false; // ignore
}
this.applicationContext.getAutowireCapableBeanFactory()
.destroyBean(bean);
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name);
return true;
}
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
catch (Exception e) {
this.errors.put(name, e);
throw new IllegalStateException("Cannot rebind to " + name, e);
}
}
return false;
}
@ManagedAttribute
public Set<String> getNeverRefreshable() {
String neverRefresh = this.applicationContext.getEnvironment().getProperty(
"spring.cloud.refresh.never-refreshable",
"com.zaxxer.hikari.HikariDataSource");
return StringUtils.commaDelimitedListToSet(neverRefresh);
}
@ManagedAttribute
public Set<String> getBeanNames() {
return new HashSet<>(this.beans.getBeanNames());
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
}
nacos通过发布RefreshEvent事件通知spring-cloud进行刷新操作,spring-cloud监听到事件后做两件事:
- 刷新属性源–属性源相对应的属性bean从旧的换成新的
- 触发scope的refreshAll操作,针对RefreshScope来说就是清空了他所管理的缓存bean,待再次调用时重新创建,创建过程就会注入新的属性源