分布式之Nacos配置中心

Nacos作为配置中心源码分析

1、什么是Naocs配置中心

官方文档: https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config

Nacos 提供用于存储配置和其他元数据的 key/value 存储,为分布式系统中的外部化配置提供服务器端和客户端支持。使用 Spring Cloud Alibaba Nacos Config,您可以在 Nacos Server 集中管理你 Spring Cloud 应用的外部属性配置。

2、Nacos的使用

2.1 给Nacos2.1.0配置数据库

倒入数据

image.png

修改内容

image.png

image.png

2.2 版本推荐

https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

image.png

2.3 父工程指定版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.2.RELEASE</version>
</parent>
<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2.4 子工程引入依赖

 <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--  单元测试类 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

2.5 利用接口对配置进行操作

public class ConfigListenerTest {
    private static String serverAddr = "localhost";
    private static String dataId = "nacos-demo.yaml";
    private static String group = "DEFAULT_GROUP";
    private static ConfigService configService;

    @Test
    public void testListener() throws NacosException, InterruptedException {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
        //获取配置服务
        configService = NacosFactory.createConfigService(properties);;
        //获取配置
        String content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);
        //注册监听器
        CountDownLatch countDownLatch = new CountDownLatch(5);
        configService.addListener(dataId, group, new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }

            @Override
            public void receiveConfigInfo(String configInfo) {
                System.out.println("配置发生变化:" + configInfo);
                countDownLatch.countDown();
            }

        });
        countDownLatch.await();
    }
    @Test
    public void publishConfig() throws NacosException {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
        //获取配置服务
        configService = NacosFactory.createConfigService(properties);
        configService.publishConfig(dataId,group,"age: 30", ConfigType.PROPERTIES.getType());
    }


    @Test
    public void removeConfig() throws NacosException, InterruptedException {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
        //获取配置服务
        configService = NacosFactory.createConfigService(properties);
        boolean isRemoveOk = configService.removeConfig(dataId, group);
        System.out.println(isRemoveOk);
    }

}

2.6 和Springboot整合

@RefreshScope
@RestController
public class NacosConfigController {

    @Value("${name}")
    private String name;

    @RequestMapping("/getName")
    public String getName(HttpServletRequest httpRequest){
        return name;
    }
}

2.7 里面放置定时任务

启动

@EnableScheduling
@SpringBootApplication
public class NacosConfigApplication {
    public static void main(String[] args)  {
         SpringApplication.run(NacosConfigApplication.class);
    }
}

增加定时任务

@RestController
public class NacosConfigController {

    @Value("${name}")
    private String name;

    @RequestMapping("/getName")
    public String getName(HttpServletRequest httpRequest){
        return name;
    }
    // 定时任务每5秒执行一次
    @Scheduled(cron = "*/2 * * * * ?")
    public void execute(){
        System.out.println("获取姓名:" + name);
    }


}

发现问题:如果我们更改配置,则对应的定时任务就失败

2.8 分析失败原因

2.8.1 Schedule执行原理

当我们更改配置的时候,我们看日志会进行容器的刷新

image.png

此时容器中并没有对应的NacosConfigController对应的实例对象。所以我们的定时任务不会执行,我们可以调用一下我们controller对应的方法,然后容器中就有了NacosConfigController对应的实例,有了这个实例,我们的定时任务就会执行,因为这个定时任务是基于后置处理器进行执行的。

image.png

2.8.2 @RefreshScope对象被清理的原因

那为什么刷新容器后NacosConfigController这Bean都没有了呢?

因为在我们更新配置后,我们的容器会将@RefreshScope标注的对象清掉,好我们来分析一下

image.png

image.png

Scope对应的有一个接口

image.png

image.png


image.png

image.png

真正实例创建 除了单例、多例、其他

image.png

我们分析这里从缓存中获取,

image.png

我们知道 单例获取是从单例对象池中,原型是重新构建Bean ,而我们Refresh是从BeanLifecycleWraperCache里面

image.png

image.png

image.png

image.png

也就是从缓存中获取对象,同时这里有个destroy

image.png

总结一下:

1、@RefreshScope中有个@Scope里面值是Refresh,他创建对象是放到对应的缓存中,我们通过GenericScope#get方法从缓存中获取对应Bean对象

2、我们更新数据的时候,会发送一个RefreshEvent事件,容器会监听这个事件,然后将缓存中数据进行删除

3、而我们定时任务是在创建bean的后置处理器中执行的,此时bean都被清理了,所以定时任务也没有了

4、我们再次访问对应的NacosConfigController的时候,我们就会创建对应的对象放到缓存,此时定时任务也就执行了

解决问题方案:

我们缓存删除是监听RefreshEvent事件而处理的,我们现在也可以监听事件进行处理,监听事件,如果事件发生,它回调用对应监听器,然后就会实例化,这样定时任务也可以执行

@Slf4j
@RefreshScope
@RestController
public class NacosConfigController implements ApplicationListener<RefreshScopeRefreshedEvent> {

    @Value("${name}")
    private String name;

    @RequestMapping("/getName")
    public String getName(HttpServletRequest httpRequest){
        return name;
    }
    // 定时任务每5秒执行一次
    @Scheduled(cron = "*/2 * * * * ?")
    public void execute(){
        System.out.println("获取姓名:" + name);
    }


    @Override
    public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
        log.info("监听刷新容器事件");
    }
}

3、源码分析

image.png

3.1 服务启动加载bootstrap.propertis

image.png

image.png

准备环境加载bootstrap.propertis,这里他会发送事件进行监听,我们可以直接进入 #load我们可以在load打上断点

image.png

总结:我们可以根据堆栈信息来发现,它是在准备环境的时候发送一个事件,ConfigFileApplicationListener监听事件,最后调用PropertiesPropertySourceLoader 对资源进行加载

3.2 客户端拉取远程配置进行合并

image.png

image.png

这里最终会调用用到NacosPropertySourceLocator# ,我们在locate上打上断点可以看一下堆栈信息

image.png

好,我们看一下locate方法

image.png

记载配置文件后最终会用composite进行合并,那他们无论加载共享配置、扩展配置和当前应用配置最终会调到NacosPropertySourceLocator#loadNacosDatalfPresent

image.png

image.png

image.pngimage.png

image.png

image.png

image.png

重点我们看一下他的配置加载

image.png

image.png

key1:从本地获取配置

image.png

key2:远程获取配置

image.png

image.png

image.png

总结:这里我们注意Spring的扩展点之一:在我们Bean构建之前加载一些数据,比如配置属性,我们就可以用这个扩展点,这里加载配置中心内容,这个内容用于后面bean对象创建。

例如:

image.png

进行配置

image.png

3.3 服务端处理配置拉取

image.png

客户端请求为ConfigQueryRequest,则从服务端进行搜索找到对应处理类

image.png

image.png

下面对应的方法比较长,我们可以看一下关键的点

image.png

image.png

总结:分析我们发送请求是从缓存文件中获取到的,这里带出两个问题,1、因为我们是从缓存中获取的,那我们直接修改数据库应该是不起作用的 2、缓存一定是从数据库中获取的,那什么时候设置进去的呢

3.4 服务启动进行数据库数据加载

image.png

image.png

服务端启动时就会依赖DumpService的init方法,从数据库中load 配置存储在本地磁盘上,并将一些重要的元信息例如MD5值缓存在内存中。服务端会根据心跳文件中保存的最后一次心跳时间,来判断到底是从数据库dump全量配置数据还是部分增量配置数据(如果机器上次心跳间隔是6h以内的话)。

全量dump当然先清空磁盘缓存,然后根据主键ID每次捞取一千条配置刷进磁盘和内存。增量dump就是捞取最近六小时的新增配置(包括更新的和删除的),先按照这批数据刷新一遍内存和文件,再根据内存里所有的数据全量去比对一遍数据库,如果有改变的再同步一次,相比于全量dump的话会减少一定的数据库IO和磁盘I0次数。

image.png

构建bean时候一定初始化@PostConstruct 对应的方法

image.png

判断全量和增量获取数据

image.png

image.png

全量拉取

image.png

image.png

image.png

image.png

总结:这里的md5值的我们学习,我们md5算法获取对应文件的值,如果这个值变化说明文件发生了变化,利用这中方式我们可以通过文件md5值来判断其他是否有变化

3.5 加载数据发生变更,发送事件

image.png

image.png

image.png

image.png

image.png

如果队列写满则如下操作:但是我们上面是服务启动暂时没有新的服务进来,所以这里subscriber是空的也就无法调用

image.png

如果正常情况下应该是放到队列里面,那就应该有取的地方,全文搜索对应的queue

image.png

哪里调用到这里呢?

image.png

image.png

从这里我们可以知道DefaultPublisher是一个线程,所有会调用run方法

总结:我们应该学会发布监听事件

3.6 监听配置变更

image.png

image.png

image.png

全文搜索ApplicationReadyEvent,查看NacosContextRefresher

image.png

image.png

image.png

发送RefreshEvent事件后,就是对@RefreshScope标志的实例进行删除,这里可以参考2.7分析失败原因

image.png

image.png

image.png

image.png

key1:销毁对应实例

image.png

key2:发送事件RefreshScopeRefreshedEvent 这也是我们通过监听这个事件,来实例化对应的实例的

3.7 服务端端更改配置

image.png

我们配置中心发布配置,一定是调用SpringMVC中的Controller方法 ,我们进行全文搜索,通过名称进行分析应该是ConfigController

image.png

image.png

image.png

key1: 更新数据库

image.png

image.png

image.png

key2:发布事件,进行客户端通知配置变更,以及集群同步

全文搜索ConfigDataChangeEvent

image.png

image.png

AsyncRpcTask是一个任务,并向里面传递了rpcQuene的一个任务队列,我看一下他是怎样处理的。

image.png

image.png

key1:我们看一下数据持久化, key2中集群数据同步就是发送一个rpc请求

我们重点key1:

image.png

image.png

image.png

image.png

上面我们解释过这里是异步处理一定有个地方处理他

image.png

image.png

image.png

image.png

image.png

image.png

发布LocalDataChangeEvent事件

全文搜索对应的事件

image.png

image.png

image.png

image.png

查看任务RpcPushTask对应的方法:

image.png

向客户端发送请求,进行配置变更的通知

image.png

3.8 客户端处理事件

image.png

我们知道发送请求的是ConfigChangeNotifyRequest,好,我们到客户端全文搜索一下,找对应的处理

image.png

image.png

我们全文搜索listenExecutebell找对应的处理位置

image.png

image.png

image.png

image.png

3.9 客户端定时拉取配置

image.png

image.png

image.png

image.png

image.png

3.10 于Nacos1.x长轮询做对比

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鷄米花

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值