1.6 Nacos服务配置中心应用实践

1.配置中心简介

1.1 背景分析

我们知道,除了代码之外,软件还有一些配置信息,比如数据库的用户名和密码,还有一些我们不想写死在代码里的东西,例如像线程池大小、队列长度等运行参数,以及日志级别、算法策略等, 还有一些是软件运行环境的参数,如Java 的内存大小,应用启动的参数,包括操作系统的一些 参数配置…… 所有这些东西,我们都叫做软件配置。以前,我们把软件配置写在一个配置文件中,就像 Windows 下的 ini 文件,或是 Linux 下的 conf 文件。然而,在分布式系统下,这样的方式就变得非常不好管理,并容易出错。假如生产环境下,项目现在正在运行,此时修改了配置文件,我们需要让这些配置生效,通常的做法是不是要重启服务。但重启是不是会带来系统服务短时间的暂停,从而影响用户体验呢,还有可能会带来经济上的很大损失(例如双11重启下服务)。基于这样的背景,配置中心诞生了。

1.2 配置中心概述

配置中心最基础的功能就是存储一个键值对,用户发布一个配置(configKey),然后客户端获取这个配置项(configValue);进阶的功能就是当某个配置项发生变更时,不停机就可以动态刷新服务内部的配置项,例如,在生产环境上我们可能把我们的日志级别调整为 error 级别,但是,在系统出问题我们希望对它 debug 的时候,我们需要动态的调整系统的行为的能力,把日志级别调整为 debug 级别。还有,当你设计一个电商系统时,设计大促预案一定会考虑,同时涌进来超过一亿人并发访问的时候,假如系统是扛不住的,你会怎么办,在这个过程中我们一般会采用限流,降级。系统的限流和降级本质上来讲就是从日常的运行态切换到大促态的一个行为的动态调整,这个本身天然就是配置起到作用的一个相应的场景。

1.3配置中心的选型

在面向分布式的微服务系统中,如何通过更高效的配置管理方式,实现微服务系统架构持续“无痛”的演进,并动态调整和控制系统的运行时态,配置中心的选型和设计起着举足轻重的作用。市场上主流配置中心有Apollo(携程开源),nacos(阿里开源),Spring Cloud Config(Spring Cloud 全家桶成员)。我们在对这些配置中心进行选型时重点要从产品功能、使用体验、实施过程和性能等方面进行综合考量。本次课程我们选择nacos,此组件不仅提供了注册中心,还具备配置中心的功能。

1.4 小节面试分析

  • 什么是配置中心?(存储项目配置信息的一个服务)
  • 为什么要使用配置中心?(集中管理配置信息,动态发布配置信息)
  • 市场上有哪些主流的配置中心?(Apollo,nacos,……)

2.Nacos配置快速入门

2.1 业务描述

在sca-provider项目中添加一个Controller对象,例如ProviderLogController,基于此Controller中的方法演示日志级别的配置。

2.2 配置准备工作

第一步:创建ProviderLogController对象,例如:

package com.jt.provider.controller;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 基于此controller演示配置中心的作用.
 * 在这个controller中我们会基于日志对象
 * 进行日志输出测试.
 */
//@Slf4j
@RestController
public class ProviderLogController {
    //创建一个日志对象
    //org.slf4j.Logger (Java中的日志API规范,基于这个规范有Log4J,Logback等日志库)
    //org.slf4j.LoggerFactory
    //log对象在哪个类中创建,getLogger方法中的就传入哪个类的字节码对象
    //记住:以后只要Java中使用日志对象,你就采用下面之中方式创建即可.
    //假如在log对象所在的类上使用了@Slf4j注解,log不再需要我们手动创建,lombok会帮我们创建
   private static Logger log=
           LoggerFactory.getLogger(ProviderLogController.class);
    @GetMapping("/provider/log/doLog01")
    public String doLog01(){//trace<debug<info<warn<error
        System.out.println("==doLog01==");
        log.trace("===trace===");
        log.debug("===debug===");
        log.info("===info====");
        log.warn("===warn===");
        log.error("===error===");
        return "log config test";
    }
}

@Slf4j注解的作用就是告诉Lombok在类中帮我们去创建日志对象,所以使用此注解就不需要我们手动创建日志对象了( private static Logger log=LoggerFactory.getLogger(ProviderLogController.class);),否则存在俩个相同的日志对象会报错;

第二步:在已有的sca-provider项目中添加如配置依赖,例如:

  <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>

此依赖会引入nacos的配置中心相关的API对象,基于这些对象可以读取配置中心的数据,此后,nacos默认的配置中心文件变为bootstrap.yml(或bootstrap.properties),并且在bootstrap.yml与application.yml配置文件均存在时,优先使用bootstrap.yml配置文件;

第三步: 将项目sca-provider的application.yml的名字修改为bootstrap.yml(启动优先级最高),并添加配置中心配置,代码如下:

spring:
  application:
    name: sca-provider
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yml # Configure the data format of the content, default to properties

2.3 新建Nacos配置

打开nacos配置中心,新建配置,如图所示:
在这里插入图片描述

其中,Data ID的值要与bootstrap.yml中定义的spring.application.name的值相同(服务名-假如有多个服务一般会创建多个配置实例,不同服务对应不同的配置实例)。配置发布以后,会在配置列表中,显示我们的配置,例如:
l
在这里插入图片描述

如果在创建nacos配置时提示发布失败,请检查参数是否正确的报错,那么我们需要到nacos安装目录的elogs目录下,查看日志文件,如果日志中出现下面的报错:
在这里插入图片描述

则是因为我们在创建nacos数据库的数据表时缺少了encrypted_data_key字段,只需要为表添加该字段即可:

ALTER TABLE config_info ADD COLUMN `encrypted_data_key` text NOT NULL COMMENT '秘钥' ;
 
ALTER TABLE config_info_beta ADD COLUMN `encrypted_data_key` text NOT NULL COMMENT '秘钥' ;
 
ALTER TABLE his_config_info ADD COLUMN `encrypted_data_key` text NOT NULL COMMENT '秘钥' ;

新版本的enacos为保证用户敏感配置数据的安全,其提供了配置加密的新特性。降低了用户使用的风险,也不需要再对配置进行单独的加密处理。

数据库表 config_infoconfig_info_betahis_config_info中需要新增字段 encrypted_data_key ,用来存储每一个配置项加密使用的秘钥。新版本的默认创建表的sql中已经添加该字段。

2.4 测试Nacos数据读取

配置创建好以后,启动sca-provider服务,然后打开浏览器,输入http://localhost:8081/provider/log/doLog01,检测idea控制台日志输出。然后再打开nacos控制台动态更新日志级别,再访问资源并检测后台日志输出.
在这里插入图片描述

然后,修改nacos配置中心的日志级别,再刷新浏览器,检测日志的输出,是否会发生变化.

2.5 @RefreshScope注解的应用

对于nacos配置中心而言,有系统内部对配置变化的感知,还有外部系统对配置的感知,假设我们想要获取当前配置中心的日志级别设置,需要定义一个属性,通过@Value注解获取,当此时属性是在构建对象时进行初始化赋值的,当我们启动服务后,再次更改日志级别时,无法同步更新获取到的日志级别,假如我们系统在浏览器中能看到日志级别的变化,该如何实现日志界别额同步更新呢?我们现在来实现一个案例.

第一步:在ProviderLogController类的上面添加一个@RefreshScope注解,例如:

@RefreshScope
@RestController
public class ProviderLogController{
  //.....
}

其中,@RefreshScope的作用是在配置中心的相关配置发生变化以后,能够及时看到类中属性值的更新(底层是通过重新创建Controller对象的方式,对属性进行了重新初始化)。

第二步:添加ProviderLogController中添加一个获取日志级别(debug<info<warn<error)的的属性和方法,代码如下:

@Value("${logging.level.com.jt:info}")
private String logLevel;
@GetMapping("/provider/log/doLog02")
public String doLog02(){
   log.info("log level is  {}",logLevel);
   return  "log level  is "+logLevel;
}

第三步:启动sca-provider服务,然后打开浏览器并输入http://localhost:8081/provider/log/doLog02进行访问测试。

说明,假如对配置的信息访问不到,请检测项目配置文件的名字是否为bootstrap.yml,检查配置文件中spring.application.name属性的值是否与配置中心的data-id名相同,还有你读取的配置信息缩进以及空格写的格式是否正确.

2.6 小节面试分析

  • 配置中心一般都会配置什么内容?(可能会经常变化的配置信息,例如连接池,日志、线程池、限流熔断规则)
  • 什么信息一般不会写到配置中心?(服务端口,服务名,服务的注册地址,配置中心)
  • 项目中为什么要定义bootstrap.yml文件?(此文件被读取的优先级比较高,可以在服务启动时读取配置中心的数据)
  • Nacos配置中心宕机了,我们的服务还可以读取到配置信息吗?(可以,从内存,客户端获取了配置中心的配置信息以后,会将配置信息在本地内存中存储一份.)
  • 微服务应用中我们的客户端如何获取配置中心的信息?(我们的服务一般首先会从内存读取配置信息,同时我们的微服务还可以定时向nacos配置中心发请求拉取(pull)更新的配置信息)
  • 微服务应用中客户端如何感知配置中心数据变化?(1.4.x版本的nacos客户端会基于长轮询机制从nacos获取配置信息,所谓的长轮询就是没有配置更新时,会在nacos服务端的队列进行等待.)
  • 服务启动后没有从配置中心获取我们的配置数据是什么原因?(依赖,配置文件名字bootstrap.yml,配置中心的dataId名字是否正确,分组是否正确,配置的名字是否正确,缩进关系是否正确,假如是动态发布,类上是否有@RefreshScope注解)
  • 你项目中使用的日志规范是什么?(SLF4J)
  • 你了解项目中的日志级别吗?(debug,info,error,…,可以基于日志级别控制日志的输出)

3.Nacos配置管理模型

3.1 概述

Nacos 配置管理模型由三部分构成,如图所示:
在这里插入图片描述
其中:

  • Namespace:命名空间,对不同的环境进⾏隔离,⽐如隔离开发环境和⽣产环境。
  • Group:分组,将若⼲个服务或者若⼲个配置集归为⼀组。
  • Service/DataId:某⼀个服务或配置集,一般对应一个配置文件。

3.2 命名空间设计

Nacos中的命名空间一般用于配置隔离,这种命名空间的定义一般会按照环境(开发,生产等环境)进行设计和实现.我们默认创建的配置都存储到了public命名空间,如图所示:
在这里插入图片描述
创建新的开发环境并定义其配置,然后从开发环境的配置中读取配置信息,该如何实现呢?

第一步:创建新命名空间,如图所示:
在这里插入图片描述
命名空间成功创建以后,会在如下列表进行呈现。
在这里插入图片描述
在指定命名空间下添加配置,也可以直接取配置列表中克隆,例如:
在这里插入图片描述
在这里插入图片描述
克隆成功以后,我们会发现在指定的命名空间中有了我们克隆的配置,如图所示:
在这里插入图片描述

此时我们修改dev命名空间中Data Id的sca-provider配置,如图所示:
在这里插入图片描述

修改项目module中的配置文件bootstrap.yml,添加如下配置,关键代码如下:

在这里插入图片描述

其中,namespace后面的字符串为命名空间的id,可直接从命名空间列表中进行拷贝.然后重启服务,继续刷新http://localhost:8081/provider/log/doLog02地址。检测输出,看看输出的内容是什么,是否为dev命名空间下配置的内容。

3.3 分组设计及实现

当我们在指定命名空间下,按环境或服务做好了配置以后,有时还需要基于服务做分组配置,例如,一个服务在不同时间节点(节假日,活动等)切换不同的配置,可以在新建配置时指定分组名称,如图所示:
在这里插入图片描述
其中,这里的useLocalCache为自己定义的配置值,表示是否使用本地缓存.

配置发布以后,修改boostrap.yml配置类,在其内部指定我们刚刚创建的分组,代码如下:

server:
  port: 8081
spring:
  application:
    name: sca-provider #进行服务注册必须配置服务名
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: 127.0.0.1:8848
        group: DEFAULT_GROUP_51 # Group, default is DEFAULT_GROUP
        file-extension: yml # Configure the data format of the content, default to properties
        namespace: 76e43031-7fa3-4552-80dc-535acff1c532

在指定的Controller类中添加属性和方法用于获取和输出DEFAULT_GROUP_51中的useLocalCache的值,代码如下:

package com.jt.provider.controller;

@RefreshScope
@RestController
public class ProviderCacheController {
    @Value("${useLocalCache:false}")
    private boolean useLocalCache;

    @RequestMapping("/provider/cache01")
    public String doUseLocalCache01(){
        return "useLocalCache'value is   "+useLocalCache;
    }
}

然后重启服务,进行访问测试,检测内容输出。
在这里插入图片描述

3.4 共享配置设计及读取

当同一个namespace的多个配置文件中都有相同配置时,可以对这些配置进行提取,然后存储到nacos配置中心的一个或多个指定配置文件,哪个微服务需要,就在服务的配置中设置读取即可。例如:

第一步:在nacos中创建一个共享配置文件,例如:
在这里插入图片描述
其中,这里的secret可以理解为一个密钥。

第二步:在指定的微服务配置文件(bootstrap.yml)中设置对共享配置文件的读取,例如:

server:
  port: 8081
spring:
  application:
    name: sca-provider #进行服务注册必须配置服务名
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: 127.0.0.1:8848
        group: DEFAULT_GROUP_51 # Group, default is DEFAULT_GROUP
        # 配置中心文件扩展名
        file-extension: yml # Configure the data format of the content, default to properties
        # 命名空间
        namespace: 76e43031-7fa3-4552-80dc-535acff1c532
        # 分组名
        # group: DEFAULT_GROUP
        # 共享配置
        shared-configs[0]:
          data-id: app-public.yml
          refresh: true #默认false,共享配置更新,引用此配置的地方是否要更新

此时表明读取在dev命名空间下,data-id为app-public.yml的配置作为共享配置分享到此命名空间的其他配置中;

第三步:在指定的Controller类中读取和应用共享配置即可,例如:

package com.jt.provider.controller;

@RefreshScope
@RestController
public class ProviderSecretController {
    @Value("${app.secret:123456}")
    private String secret;
    @GetMapping("/provider/secret")
    public String doGetSecret(){
        return "The Secret is "+secret;
    }
}

第四步:启动服务,然后打开浏览器进行访问测试。
在这里插入图片描述

此时,我们可以对ProviderLogController类进行改造,尝试获取secret的值并输出:

    @Value("${app.secret:123456}")
    private String secret;
    @GetMapping("/provider/log/doLog02")
    public String doLog02(){
        return "log level is "+ logLevel+ "---------"+"The Secret is "+secret;
    }

重启服务后我们可以看到,因为refresh的值设置为true,其他配置中也可以获取到共享配置中的相关信息,并且页面会跟随共享配置中值的变化,同步更新Secret的值;

在这里插入图片描述

3.5 小节面试分析

  • Nacos配置管理模型的背景?(环境不同配置不同)
  • Nacos配置中的管理模型是怎样的?(namespace,group,service/data-id)
  • Nacos客户端(微服务)是否可以读取共享配置?(可以)

4.本地缓存应用实践

4.1 概述

假设我们现在正在开发一个商城的首页,页面中的商品分类基本上不会有太大的变化,所以我们每次打开首页时,都需要从数据库获取商品分类的话,对于数据库的压力是比较大的,并且是没有必要的,我们可以将其放入缓存中,用户访问首页时首先判断缓存中是否有商品分类的数据,当有数据时从缓存中获取商品分类,缓存中没有数据时,才从数据库获取商品分类;

4.2 模拟商品分类获取代码

我们在ProviderCacheController类中完成相关代码;

第一步:构建一个线程安全的List对象,基于此对象存储从数据库获取的一些数据

    //构建一个线程安全的List对象,基于此对象存储从数据库获取的一些数据
    private List<String> cache = new CopyOnWriteArrayList<>();

第二步:创建doUseLocalCache02方法,完成数据的获取

	 @GetMapping("/provider/cache02")
    public List<String> doUseLocalCache02(){
        if(!useLocalCache){
            System.out.println("Get Data from Database");
            return Arrays.asList("电器","服装","生活用品");
        }
        if(cache.isEmpty()){
            synchronized (cache){
                if(cache.isEmpty()){
                    System.out.println("从数据库获取数据");
                    //假设这些数据来自数据库
                    List<String> cateList = Arrays.asList("电器","服装","生活用品");
                    cache.addAll(cateList);
                }
            }
        }
        return cache;
    }

我们在这里首先进行useLocalCache缓存开关的判断,确定其是否开启了本地缓存,如果关闭,我们需要从数据库中去获取数据;为了与下面缓存开启时去数据库获取数据进行区分,我们这里打印Get Data from Database语句;

如果缓存开关是开启状态,则我们需要先判断缓存cache 中是否有数据,当缓存中没有数据时,则到数据库中获取数据,缓存中有数据时,直接返回缓存cache 即可;

但是这里还有一个问题,就是有并发情况存在时,多个访问同时访问,都判断为缓存开启,并且缓存中没有数据,则其都会访问数据库获取数据,那此时我们的代码就没有了意义,所以我们这里需要添加一个锁,用来控制访问,并且在锁中再进行一次判断,当第一个线程进入并获取数据后,后续排队的线程进入后先判断缓存cache 中是否有数据,有数据则直接返回,没有数据则再次访问数据库获取数据;

完成后,我们首先确定useLocalCache的值为false进行测试:
在这里插入图片描述

获取到数据后,我们观察控制台:
在这里插入图片描述

观察控制台我们可以看到数据是判断useLocalCache缓存开关关闭时,直接到数据库获取到的,我们将useLocalCache缓存开关更改为true后再次进行测试:
在这里插入图片描述

再次访问我们可以看到,此时数据是直接从缓存中获取的;

4.3 模拟多线程读取数据

4.3.1 模拟多线程状态下不加锁获取数据库数据

我们再sca-common包下创建CopyOnWriteArrayListTests包做下面的测试:

  1. 构建一个线程对象cache 作为缓存
  2. 构建一个查询不加锁的查询方法,判断缓存中没有数据时到数据库中索取数据
  3. 创建多个线程对象调用查询方法

完整代码如下:

package com.jt.cache;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListTests {
    private static List<String> cache = new CopyOnWriteArrayList<>();

    public static List<String> selectALL(){
        //if(cache.isEmpty()){
            //synchronized (cache){
                if(cache.isEmpty()){
                    List<String> cateList = Arrays.asList("A","B","C");
                    cache.addAll(cateList);
                }
            //}
        //}
        return cache;
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run(){
                System.out.println(selectALL());
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run(){
                System.out.println(selectALL());
            }
        };
        Thread t3 = new Thread(){
            @Override
            public void run(){
                System.out.println(selectALL());
            }
        };
        t1.start();
        t2.start();
        t3.start();
    }
}

我们运行方法后可以看到控制台输出如下信息:
在这里插入图片描述

此时缓存中的信息出现了并发问题,有到数据库取了一次的结果,也有到数据库中取了2次3次的结果;

4.3.2 模拟多线程下未判断缓存是否为空且加锁获取数据库数据

    public static List<String> selectALL(){
        //if(cache.isEmpty()){
            synchronized (cache){
                if(cache.isEmpty()){
                    List<String> cateList = Arrays.asList("A","B","C");
                    cache.addAll(cateList);
                }
            }
        //}
        return cache;
    }

我们这里只将锁的注释去掉,运行后可以得到下面的结果:
在这里插入图片描述

此时获取的数据已经是正常的了,但因为并没有在最外层判断缓存是否为空,所以获取到的数据均为数据库中查询得到的;

4.3.3 模拟多线程下判断缓存为空且加锁获取数据库数据

我们将所有的注释全部去掉后再次进行测试可以观察到运行速度明显加快,此时我们只在数据库中查询了一次数据,后续都是在缓存中获取的数据

5 总结(Summary)

5.1 重难点分析

  • 配置中心的选型。(市场活跃度、稳定性、性能、易用)
  • Nacos配置中心基本应用。(新建,修改、删除配置以后,在Nacos客户端应用配置)
  • 配置管理模型应用。(namespace,group,service/dataId)
  • Nacos配置变更的动态感知。(底层原理分析)

5.2 FAQ分析

  • 为什么需要配置中心?(动态管理发布配置,无需重启服务,更好保证服务的可用)
  • 配置中一般要配置什么内容?(经常变化的配置数据-日志级别,线程池、连接池、…)
  • 市面上有哪些主流配置中心?(Nacos,….)
  • 配置中心选型时要重点考虑哪些因素?(市场活跃度、稳定性、性能、易用)
  • Nacos客户端(微服务业务)如何动态感知配置中心数据变化的?(nacos2.0之前nacos客户端采用长轮询机制每隔30秒拉取nacos配置信息.)
  • Nacos配置管理模型是怎样的?(命名空间-namespace,分组-group,服务实例-dataId)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值