Nacos-04-@RefreshScope自动刷新原理

Nacos动态刷新原理

Nacos做配置中心的时候,配置数据的交互模式是有服务端push推送的,还是客户端pull拉取的?

短轮询

不管服务端的配置是否发生变化,不停发起请求去获取配置,比如支付订单场景中前端JS不断轮询订单支付的状态

这样的坏处显而易见,由于配置并不会频繁发生变更,如果是一直发请求,一定会对服务端造成很大的压力。还会造成数据推送的延迟,比如每10秒请求一次配置,如果在第11秒的时候配置更新,那么推送将会延迟9秒,等待下一次请求

这就是短轮询,为了解决短轮询的问题,有了长轮询的方案

长轮询

长轮询不是什么新技术,它其实就是由服务端控制响应客户端请求结果的返回时间,来减少客户端无效请求的一种优化手段,其实对于客户端来说,短轮询的使用并没有本质上的区别

客户端发起请求后,服务端不会立即返回请求结果,而是将请求hold挂起一段时间,如果此时间段内配置数据发生变更,则立即响应客户端,若一直无变更则等到指定超时时间后响应给客户端结果,客户端重新发起长链接

Nacos实现

Nacos客户端发送一个请求连接到服务端,然后服务端中会有一个29.5+0.5s的一个hold期,然后服务端会将此次请求放入到allSubs队列中等待,触发服务端返回结果的情况只有两种,第一种是时间等待了29.5秒,配置未发生改变,则返回未发生改变的配置;第二种是操作Nacos Dashboard或者API对配置文件发生一次变更,此时会触发配置变更的事件,发送一条LocalDataEvent消息,此时服务端监听到消息,然后遍历allSubs队列,根据对应的groupId找到配置变更的这条ClientLongPolling任务,并且通过连接返回给客户端

Nacos动态刷新避免了服务端对客户端进行push操作时需要保持双方的心跳连接,同样也避免了客户端对服务端进行pull操作时数据的时效性问题,不必频繁去拉去服务端的数据

通过上面原理的初步了解,显而易见,答案是:客户端主动拉取的,通长轮询的方式(Long Polling)的方式来获取配置数据

坑点

我们的需求是通过改变nacos上的value值在不重启服务的情况达到自动刷新读取到最新的配置,而正常配置只能说可以支持自动刷新配置读取到了容器中了,但是不会改变正在运行的服务

其实要知道自动刷新配置的原理,首先我们是通过长轮询+主动去Pull拉模式方式从nacos服务端获取配置的

我们在首次启动服务的时候,其实spring已经加载了nacos的jar,此时spring是有监听器的,通过定时去获取nacos的配置是否发生了变化,因为上面说了我们是通过长轮询+主动去Pull拉模式方式从nacos服务端获取配置的,那么肯定是缓存在本地的,如果spring启动一个定时任务专门在本地进行比较就会发现value值是改变的,然后监听到了这个事件就会通过发布事件去处理实现自动刷新配置业务,只会更新@RefreshScope和@Component + @ConfigurationProperties注解下的类的,其他的不会更新的


@RefreshScope注解实现动态刷新

/**
 * Convenience annotation to put a <code>@Bean</code> definition in
 * {@link org.springframework.cloud.context.scope.refresh.RefreshScope refresh scope}.
 * Beans annotated this way can be refreshed at runtime and any components that are using
 * them will get a new instance on the next method call, fully initialized and injected
 * with all dependencies.
 *
 * @author Dave Syer
 *
 */
@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;

}
  • 上面是RefreshScope的源码,该注解被@Scope注解使用,@Scope用来比较Spring Bean的作用域。
  • 注解的属性proxyMode默认使用TARGET_CLASS作为代理。

原理解析

为了实现动态刷新配置,主要就是想办法达成以下两个核心目标:

  • 让Spring容器重新加载Environment环境配置变量
  • Spring Bean重新创建生成

@RefreshScope主要就是基于@Scope注解的作用域代理的基础上进行扩展实现的,加了@RefreshScope注解的类,在被Bean工厂创建后会加入自己的refresh scope 这个Bean缓存中,后续会优先从Bean缓存中获取,当配置中心发生了变更,会把变更的配置更新到spring容器的Environment中,并且同事bean缓存就会被清空,从而就会从bean工厂中创建bean实例了,而这次创建bean实例的时候就会继续经历这个bean的生命周期,使得@Value属性值能够从Environment中获取到最新的属性值,这样整个过程就达到了动态刷新配置的效果。

img

@Scope

我们平常所使用的@scope都为singleton或是prototype,这两个是spring硬编码的

@scope为refresh的时候,spring是如何创建bean的

protected <T> T doGetBean() {
    ...
// Create bean instance.
				if (mbd.isSingleton()) {
					...
				}

				else if (mbd.isPrototype()) {
					...
				}

				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 {
                          //这边是获取bean,调用的是RefreshScope中的的方法
						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继承成了GenericScope类,最终调用的的是GenericScope的get方法

public class GenericScope
		implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {
                 @Override
	
  public Object get(String name, ObjectFactory<?> objectFactory) {
		// 将bean添加到缓存cache中
        BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
		this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
		try {
            // 调用下面的getBean方法
			return value.getBean();
		}
		catch (RuntimeException e) {
			this.errors.put(name, e);
			throw e;
		}
	}       
 
private static class BeanLifecycleWrapper {
        
		public Object getBean() {
            // 如果bean为空,则创建bean
			if (this.bean == null) {
				synchronized (this.name) {
					if (this.bean == null) {
						this.bean = this.objectFactory.getObject();
					}
				}
			}
            // 否则返回之前创建好的bean
			return this.bean;
		}
            }
        }

GenericScope 里面 包装了一个内部类 BeanLifecycleWrapperCache 来对加了 @RefreshScope 从而创建的对象进行缓存,使其在不刷新时获取的都是同一个对象。(这里你可以把BeanLifecycleWrapperCache 想象成为一个大Map 缓存了所有@RefreshScope 标注的对象)

事件监听发布

Nacos在启动的时候会为每个配置文件注册监听事件

public class NacosContextRefresher
		implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {	

    // 注册监听方法
	private void registerNacosListenersForApplications() {
		if (refreshProperties.isEnabled()) {
			for (NacosPropertySource nacosPropertySource : NacosPropertySourceRepository
					.getAll()) {
				// 判断单个配置文件是否支持刷新,就是refresh = true,开启了就会注册监听,有变化就会及时通知
				if (!nacosPropertySource.isRefreshable()) {
					continue;
				}

				String dataId = nacosPropertySource.getDataId();
                 // 调用注册方法
				registerNacosListener(nacosPropertySource.getGroup(), dataId);
			}
		}
	}
	private void registerNacosListener(final String group, final String dataId) {
		// 创建监听
		Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
			@Override
			public void receiveConfigInfo(String configInfo) {
				refreshCountIncrement();
				String md5 = "";
				if (!StringUtils.isEmpty(configInfo)) {
					try {
						MessageDigest md = MessageDigest.getInstance("MD5");
						md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
								.toString(16);
					}
					catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
						log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
					}
				}
				refreshHistory.add(dataId, md5);
                 //监听到配置变化时发布RefreshEvent刷新事件
				applicationContext.publishEvent(
						new RefreshEvent(this, null, "Refresh Nacos config"));
				if (log.isDebugEnabled()) {
					log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
				}
			}

			@Override
			public Executor getExecutor() {
				return null;
			}
		});

		try {
             //将监听添加到config里,注册上
			configService.addListener(dataId, group, listener);
		}
		catch (NacosException e) {
			e.printStackTrace();
		}
	}

找下receiveConfigInfo方法什么时候触发的

public class CacheData {
    
	private void safeNotifyListener(final String dataId, final String group, final String content,
                                    final String md5, final ManagerListenerWrap listenerWrap) {
        final Listener listener = listenerWrap.listener;

        Runnable job = new Runnable() {
            @Override
            public void run() {
                ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
                ClassLoader appClassLoader = listener.getClass().getClassLoader();
                try {
                    if (listener instanceof AbstractSharedListener) {
                        AbstractSharedListener adapter = (AbstractSharedListener) listener;
                        adapter.fillContext(dataId, group);
                        LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
                    }
				  ...
                    //看方法名可以猜到是通知监听的
                    listener.receiveConfigInfo(contentTmp);
                    ...
    }

继续往上找

    void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            //可以看到是通过判断配置文件的md5值是否发生变化
            if (!md5.equals(wrap.lastCallMd5)) {
                safeNotifyListener(dataId, group, content, md5, wrap);
            }
        }
    }

RefreshEvent事件发布后,肯定有监听的方法

public class RefreshEventListener implements SmartApplicationListener {
    
	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationReadyEvent) {
			handle((ApplicationReadyEvent) event);
		}
         //RefreshEventListner监听器会监听到这个事件
		else if (event instanceof RefreshEvent) {
			handle((RefreshEvent) event);
		}
	}

调用handel方法

	public void handle(RefreshEvent event) {
		if (this.ready.get()) { // don't handle events before app is ready
			log.debug("Event received " + event.getEventDesc());
            
             //this.refresh类中定义的是ContextRefresher
             //private ContextRefresher refresh;
			Set<String> keys = this.refresh.refresh();
			log.info("Refresh keys changed: " + keys);
		}
	}
public class ContextRefresher {
    
	public synchronized Set<String> refresh() {
        // 刷新spring的envirionment 变量配置
		Set<String> keys = refreshEnvironment();
        //this.scope类中定义的是RefreshScope
        //	private RefreshScope scope;
		this.scope.refreshAll();
		return keys;
	}
public class RefreshScope extends GenericScope implements ApplicationContextAware,
		ApplicationListener<ContextRefreshedEvent>, Ordered {
    
	public void refreshAll() {
        //调用父类GenericScope的destroy方法
		super.destroy();
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}

可以看到最终又回到GenericScope这里来,refresh方法最终调用destroy方法,清空之前缓存的bean

public class GenericScope implements Scope, BeanFactoryPostProcessor,
		BeanDefinitionRegistryPostProcessor, DisposableBean {
	@Override
	public void destroy() {
		List<Throwable> errors = new ArrayList<Throwable>();
		Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
		for (BeanLifecycleWrapper wrapper : wrappers) {
			try {
				Lock lock = this.locks.get(wrapper.getName()).writeLock();
				lock.lock();
				try {
                     //这里主要就是把之前的bean设置为null, 就会重新走createBean的流程
					wrapper.destroy();
				}
				finally {
					lock.unlock();
				}
			}
			catch (RuntimeException e) {
				errors.add(e);
			}
		}
		if (!errors.isEmpty()) {
			throw wrapIfNecessary(errors.get(0));
		}
		this.errors.clear();
	}

总结:ContextRefresher 就是外层调用方法用的,GenericScope 里面的 get 方法负责对象的创建和缓存,destroy 方法负责再刷新时缓存的清理工作


总结

综上所述,来总结下@RefreshScope 实现流程

1.需要动态刷新的类标注@RefreshScope 注解

2.@RefreshScope 注解标注了@Scope 注解,并默认了ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能就是在创建一个代理,在每次调用的时候都用它来调用GenericScope get 方法来获取对象

3.如属性发生变更则会发布RefreshEvent刷新事件,监听者会调用 ContextRefresher refresh() ->RefreshScope refreshAll() 进行缓存清理方法调用,并发送刷新事件通知 -> GenericScope 真正的 清理方法destroy() 实现清理缓存

4.在下一次使用对象的时候,会调用GenericScope get(String name, ObjectFactory<?> objectFactory) 方法创建一个新的对象,并存入缓存中,此时新对象因为Spring 的装配机制就是新的属性了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/21407.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mathtype公式符号显示不对

文章目录 问题解决方法结果 记录攥写论文遇到的问题及解决方法 问题 使用mathtype编辑公式过后&#xff0c;发现公式显示不对&#xff0c;出现两种问题&#xff1a; 1&#xff1a;部分符号变为方框 2&#xff1a;符号大小异常 例如&#xff1a; 解决方法 第一种&#xff1a…

【Linux 之五】 Linux中使用fdisk命令实现磁盘分区

最近由于工作的需要&#xff0c;初步研究了uboot中的fastboot实现方式。研究fastboot不可避免的需要了解磁盘分区的相关知识点&#xff0c;在linux下可以使用fdisk命令实现磁盘的分区。好了&#xff0c;下面步入正题。 1. 查看帮助信息&#xff08;fdisk --help&#xff09; …

我们详细讲讲UI自动化测试最佳设计模式POM

概念 什么是POM&#xff1f; POM是PageObjectModule&#xff08;页面对象模式&#xff09;的缩写&#xff0c;其目的是为了Web UI测试创建对象库。 在这种模式下&#xff0c;应用涉及的每一个页面应该定义为一个单独的类&#xff0c;类中应该包含此页面上的页面元素对象和处…

skywalking安全认证问题

skywalking安全认证 一、问题二、步骤2.1 skywalking-aop配置文件修改2.2 agent配置文件修改 一、问题 在springboot项目使用java-agent接入skywalking时&#xff0c;为保证两者之间的数据安全传输&#xff0c;准备加个安全认证 参考文章&#xff1a; https://www.helloworld…

亚马逊云科技使用Inf2实例运行GPT-J-6B模型

在2019年的亚马逊云科技re:Invent上&#xff0c;亚马逊云科技发布了Inferentia芯片和Inf1实例这两个基础设施。Inferentia是一种高性能机器学习推理芯片&#xff0c;由亚马逊云科技定制设计&#xff0c;其目的是提供具有成本效益的大规模低延迟预测。时隔四年&#xff0c;2023年…

java版企业电子招投标系统源码 招采系统源码 spring boot+mybatis+前后端分离实现电子招投标系统

spring bootmybatis前后端分离实现电子招投标系统 电子招投标系统解决方案 招标面向的对象为供应商库中所有符合招标要求的供应商&#xff0c;当库中的供应商有一定积累的时候&#xff0c;会节省大量引入新供应商的时间。系统自动从供应商库中筛选符合招标要求的供应商&#x…

【Mybatis】SpringBoot整合Mybatis

唠嗑部分 之前我们说了Mybatis的一些文章&#xff0c;相关文章&#xff1a; 【Mybatis】简单入门及工具类封装-一 【Mybatis】如何实现ORM映射-二 【Mybatis】Mybatis的动态SQL、缓存机制-三 【Mybatis】Mybatis处理一对多、多对多关系映射-四 这篇文章我们来说说SpringBoot如…

SpringCloud学习-实用篇03

以下内容的代码可见&#xff1a;SpringCloud_learn/day03 1.初识Docker 什么是Docker? 项目部署问题&#xff1a;大型项目组件较多&#xff0c;运行环境也较为复杂&#xff0c;部署时会碰到一些问题 依赖关系复杂&#xff0c;容易出现兼容性问题开发、测试、生产环境有差异 Do…

ADS - lesson 1. Patch antenna

Patch antenna 1. 开启 layout command line editor2. layout command line editor应用3. 画馈线4. 插入端口5. EM 冲冲冲6. 结果 1. 开启 layout command line editor ADS主界面 - Tools - App Manager… - 勾选 “layout command line editor” 然后重启软件 2. layout co…

一步一步详解LSTM网络【从RNN到LSTM到GRU等,直至attention】

一步一步详解LSTM网络【从RNN到LSTM到GRU等&#xff0c;直至attention】 0、前言1、Recurrent Neural Networks循环神经网络2、The Problem of Long-Term Dependencies长期依赖的问题3、LSTM Networks4、The Core Idea Behind LSTMs5、Step-by-Step LSTM Walk Through6、Varian…

微信小程序原生开发功能合集十五:个人主页功能实现

本章个人主页功能实现,展示当前登录用户信息、个人主页、修改密码、浏览记录、我的收藏、常见问题、意见反馈、关于我们等界面及对应功能实现。   另外还提供小程序开发基础知识讲解课程,包括小程序开发基础知识、组件封装、常用接口组件使用及常用功能实现等内容,具体如…

(1分钟速览)g2o入门指南--笔记版

在slam后端中&#xff0c;优化的框架很多&#xff0c;有ceres&#xff0c;g2o&#xff0c;gtsam这些。要想真正掌握slam后端的优化内容&#xff0c;这些框架是必不可少的上手练习的内容。本文则介绍有关g2o的相关内容&#xff0c;作为一个入门指南&#xff0c;目标&#xff1a;…

Python 学习 2022.08.28 周日

文章目录 一、 概述1.1&#xff09; 之前写的文章&#xff1a;1.2) 基础点1.3) 配置1.4) Python2 和 Python3 的区别1.5&#xff09; 相关问题跟踪解决1.6) 其他 一、 概述 1.1&#xff09; 之前写的文章&#xff1a; 【Python大系】Python快速教程《Python 数据库 GUI CGI编…

怎么取消只读模式?硬盘进入只读模式怎么办?

案例&#xff1a;电脑磁盘数据不能修改怎么办&#xff1f; 【今天工作的时候&#xff0c;我想把最近的更新的资料同步到电脑上的工作磁盘&#xff0c;但是发现我无法进行此操作&#xff0c;也不能对磁盘里的数据进行改动。有没有小伙伴知道这是怎么一回事&#xff1f;】 在使…

无线充+台灯专用PD诱骗芯片LDR6328S

近几年&#xff0c;日常生活中到处可以看到消费者使用支持Type-c接口的电子产品&#xff0c;如手机&#xff0c;笔记本&#xff0c;筋膜枪&#xff0c;蓝牙音箱等等。例如&#xff0c;像筋膜枪&#xff0c;蓝牙音箱&#xff0c;无人机&#xff0c;小风扇。 无线充台灯方案&…

容器安装Datax+Datax-web2.1(一)

目录 简介1、安装Datax-web2.1.11&#xff09;安装docker-compose2&#xff09;创建Datax-web和MySQL容器 2、安装Datax-web2.1.21&#xff09;安装MySQL2&#xff09;初始化数据3&#xff09;安装datax和datax-web4&#xff09;浏览器登录 DataxDatax-web2.1实现MySQL数据库数…

如何通过SOLIDWORKS driveworksxpress初步实现参数化设计

当提到参数化设计&#xff0c;我们首先需要了解究竟什么是参数化设计&#xff0c;它是指从一个系统的角度&#xff0c;计划所有的设计过程&#xff0c;在整个系统中建立组件、次组件和子零件之间的关系&#xff0c;在最上层的部分建立设计意图&#xff0c;并将其往较下层的部分…

RK3588光电载荷处理板研制进展

本来就是一个很小众的市场&#xff0c;但是偶尔也会有同行询问&#xff0c;这儿就简单汇报一下后期的进展 板子已经开发完成&#xff0c;并有幸得到了两个订单&#xff0c;虽然量不是很大&#xff0c;但是也很开心由于一段时间的努力和付出&#xff0c;将该设备应用在了国防事业…

微信小程序等待wx.requestPayment的回调函数执行完后再执行后续代码

async/await & Promise的再认识 背景 在开发微信小程序过程中&#xff0c;遇到如下需求&#xff1a; 需要等待wx.requestPayment的回调函数执行完后再执行后续代码 这是因为在调用wx.requestPayment之后&#xff0c;会弹出一个支付弹窗&#xff0c;如果此时点击右上角的…

Electron自定义窗口

Electron标题栏隐藏和自定义 Electron应用自定义标题栏样式 标题栏样式允许隐藏浏览器窗口的大部分色彩&#xff0c;同时保持系统原生窗口控件完整无损&#xff0c;并可以在 BrowserWindow 的构造器中使用 titleBarStyle 选项来配置。 应用 hidden 标题栏样式的结果是隐藏标…