【深入理解SpringCloud微服务】深入理解微服务配置中心原理,并手写一个微服务配置中心

【深入理解SpringCloud微服务】深入理解微服务配置中心原理,并手写一个微服务配置中心

为什么要使用配置中心

在没有配置中心以前,我们应用程序的配置都是保存在本地,如果是单体架构的话,这种方式是没有问题的,优点是非常的简单。

但是如果在微服务架构下,每个微服务自己管理自己的配置文件的话,这种方式就太混乱,不利于运维人员对配置文件的维护。

因此,在微服务架构下,通常会引入配置中心,所有微服务的配置都统一维护在配置中心,应用程序从配置中心读取配置并加载,这样对不同服务配置的维护就显得更方便。

配置中心原理

配置中心通常分为配置中心服务端和配置中心客户端。配置中心服务端通过某种存储方式存储配置信息,比如数据库、文件、github。而配置中心客户端请求配置中心服务端拉取配置信息,常见的是通过http请求拉取配置。

配置中心还会提供接口或者一个界面去进行配置添加和修改。

然后配置中心客户端从配置中心服务端拉取到配置信息后,会加载配置信息到本地环境中,这样我们的应用程序就能读取到从配置中心拉取回来的配置。

最后,配置中心通常还会有配置变更通知的功能。当配置中心发生配置变更时,需要通知客户端,可以使用与客户端维持的长连接进行推送,或者由客户端主动轮询。当客户端收到配置变更通知时,会刷新本地环境。

如何手写一个配置中心

以上的一整套操作,如果从零搞起,是非常麻烦的,如果我们基于Spring,使用SpringBoot开发的话,有一些接口是可以复用的,我们下面了解一下。

使用PropertySourceLocator

SpringBoot在启动的时候,会使用PropertySourceLocator#locate()方法查找配置信息,PropertySourceLocator#locate()方法返回PropertySource,里面包含了查找到的配置信息,然后SpringBoot会把PropertySource添加到Environment对象中。

于是,我们可以自己实现一个PropertySourceLocator重写locate()方法,locate()方法请求配置中心获取配置。我们把自己实现的PropertySourceLocator注册到Spring容器中,SpringBoot启动时就会调用我们的PropertySourceLocator去配置中心拉取配置,加载到Environment中,这样就省掉了很多代码。

监听配置变更,刷新配置

当配置中心发生配置变更时,要通知客户端。配置中心可以利用长连接进行推送,也可以客户端自己进行长轮询。

当客户端接收到配置变更时的环境刷新操作。客户端需要重新请求配置中心拉取配置,刷新本地环境Environment,然后还要把变更的配置重新赋值到引用该配置的对象当中。

在SpringBoot中,有一个RefreshEventListener监听器,会监听RefreshEvent事件,当RefreshEventListener监听到RefreshEvent事件时,会创建一个新SpringContext去加载配置信息,并且销毁被@RefreshScope注解修饰的bean。在下一次请求使用到被销毁的bean时,Spring会重新实例化,那么这个bean就会读取到最新的配置。

这样,当客户端接收到配置中心的配置变更通知时,只要通过通过Spring的事件监听机制发布一个RefreshEvent事件即可,这样又能省掉很多代码。

实现一个微服务配置中心

下面是我手写实现的一个配置中心,由于篇幅关系,只会展示核心代码。如果要看详细代码的话,文末会附上git仓库的地址,从上面下载即可。

服务端

配置中心服务端是基于SpringBoot开发的,采用传统的MVC架构。

库表

我们的配置中心是基于数据库存储配置信息的。

sql 复制代码
CREATE TABLE `t_config_file` (
  `f_id` bigint(20) NOT NULL,
  `f_environment` varchar(1024) DEFAULT NULL COMMENT '环境(自定义,如:dev、test)',
  `f_service_name` varchar(1024) DEFAULT NULL COMMENT '配置所属服务名',
  `f_name` varchar(1024) DEFAULT NULL COMMENT '配置文件名',
  `f_priority` int(11) DEFAULT 0 COMMENT '优先级,值越大越优先',
  `f_create_time` bigint(20) DEFAULT NULL COMMENT '创建时间',
  `f_modify_date` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '修改时间',
  PRIMARY KEY (`f_id`),
  KEY `idx_config_file_modify_date` (`f_modify_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='配置文件表';

CREATE TABLE `t_config_file_item` (
  `f_id` bigint(20) NOT NULL,
  `f_config_file_id` bigint(20) NOT NULL COMMENT '配置文件id',
  `f_name` varchar(1024) NOT NULL COMMENT '配置项名称',
  `f_value` varchar(1024) NOT NULL COMMENT '配置项内容',
  `f_create_time` bigint(20) DEFAULT NULL COMMENT '创建时间',
  `f_modify_date` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '修改时间',
  PRIMARY KEY (`f_id`),
  KEY `idx_config_file_item_modify_date` (`f_modify_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='配置文件配置项表';
  • t_config_file是配置文件表
  • t_config_file_item是配置文件配置项表
  • t_config_file_item通过f_config_file_id关联t_config_file表,t_config_file与t_config_file_item是一对多关系。

t_config_file里的一条记录表示一个配置文件,t_config_file_item里的一条记录表示配置文件里的一个配置项。

ConfigCenterController

java 复制代码
@RestController
@RequestMapping("/config/center")
public class ConfigCenterController {

    @Autowired
    private ConfigCenterService configCenterService;

	...
	
}

ConfigCenterController暴露了配置文件的增删改查接口,调用ConfigCenterService进行处理。

ConfigCenterService

java 复制代码
@Service
public class ConfigCenterService {

	...

    @Autowired
    private ConfigFileDao configFileDao;

	...

	// modify()修改配置的方法,
	// 修改完后,会调用LongPollingService通知客户端发生配置变更

	...
	
}

上面只展示了ConfigCenterService的修改配置文件和根据服务名查询所有配置文件的这两个方法。

ConfigCenterService调用ConfigFileDao操作数据库,对t_config_file表和t_config_file_item表进行增删改查操作。

其中modify方法做完修改操作后,判断如果修改操作执行成功,还会调用LongPollingService通过长连接通知客户端。

LongPollingService

我们采用了websocket保持客户端与服务端的长连接,当有配置变更时,通过websocket通知客户端刷新配置信息。

LongPollingService正是服务端暴露的websocket端点,被@ServerEndpoint注解修饰。LongPollingService里面封装了websocket连接建立和关闭时进行的操作,以及推送配置变更通知的操作。

java 复制代码
// 对外暴露websocket端点
@ServerEndpoint("/ws/config/center")
@Component
public class LongPollingService {

	// 当建立websocket连接后,会保存websocket连接对应的Session到Map中
	// 格式为 [environmen: [serviceName: session]] 双层Map

	...
	
	// 通知客户端发生配置变更
    public static void notify(String environmen, String serviceName) throws IOException {
    	// 根据指定环境environmen指定服务名environmen,取得对应的Session
    	// 该Session代表与对应客户端建立的长连接
        Map<String, Session> serviceNameSessionMap = envServiceNameSessionMapping.get(environmen);
        if (serviceNameSessionMap != null) {
            Session session = serviceNameSessionMap.get(serviceName);
            if (session != null) {
            	// 通过session向客户端推送配置变更通知
                session.getBasicRemote().sendText(Constant.CONFIG_CHANGE_MARK);
            }
        }
    }

}

当建立websocket连接后,会保存websocket连接对应的Session到LongPollingService的Map中,格式为"environmen: \[serviceName: session]"这样的双层Map。

LongPollingService.notify(environmen, serviceName)会通过environmen环境名(比如dev、uat)和serviceName服务名从Map中获取到对应的session,这个session表示与指定环境指定服务名的客户端建立的websocket长连接。然后通过这个session向对应客户端推送配置变更通知。

ConfigCenterServerConfig

ConfigCenterServerConfig是一个被@Configuration注解修饰的配置类,通过在spring.factories指定该配置类进行自动装配。

ConfigCenterServerConfig会通过@ComponentScan、@Bean等注解配置我们上面说到的类。

客户端

客户端也是使用SpringBoot进行开发的,以jar包的形式被其他微服务引用。

ConfigService

ConfigService是一个接口,定义了获取配置信息的方法。

java 复制代码
public interface ConfigService {

    List<ConfigFileDto> getAll(String environment, String serviceName, String name);

    ConfigFileDto get(String fileId);

}

ConfigService的实现类内部通过OkHttp请求配置中心服务端拉取配置信息。

SimpleMicroserviceConfigPropertySourceLocator

SimpleMicroserviceConfigPropertySourceLocator就是我们上面说的实现了Spring的PropertySourceLocator接口的实现类,重写了locate方法,在locate方法中调用ConfigService通过http请求配置中心拉取配置信息,再把配置中心返回的配置信息转换成CompositePropertySource返回。

SimpleMicroserviceConfigPropertySourceLocator.locate(Environment)

java 复制代码
    @Override
    public PropertySource<?> locate(Environment environment) {
    	// 创建一个CompositePropertySource,用于保存请配置中心拉取回来的配置信息
        CompositePropertySource compositePropertySource = new CompositePropertySource(PROPERTY_SOURCE_NAME);
        // 调用configService从配置中心拉取配置信息
        List<ConfigFileDto> configFileDtos = configService.getAll(simpleMicroserviceConfigProperties.getEnvironment(), simpleMicroserviceConfigProperties.getServiceName(), null);
        // 拉取回来的配置信息,保存到CompositePropertySource
        configFileDtos.stream()
                .map(configFileDto -> new MapPropertySource(configFileDto.getName(), configFileDto.getConfigMap()))
                .forEach(compositePropertySource::addFirstPropertySource);
        return compositePropertySource;
    }

SpringBoot在启动之后,会调用PropertySourceLocator的locate方法,然后把locate方法方法返回的PropertySource添加到Environment中。SpringBoot会调用这里的SimpleMicroserviceConfigPropertySourceLocator的locate方法,然后把返回的CompositePropertySource添加到Environment中。

ConfigCenterClientConfig

ConfigCenterClientConfig就是一个配置类,被spring.factories指定自动装配,通过@Bean注解往Springboot注册ConfigService和SimpleMicroserviceConfigPropertySourceLocator。

LongPollingClient

java 复制代码
@Component
public class LongPollingClient implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {

    ...

    public void connect() {
        ...
        // 通过okhttp与配置中心服务端建立websocket连接
        // RefreshListener是一个监听器,收到配置中心的通知会回调
        mOkHttpClient.newWebSocket(request, new RefreshListener(...));
        ...
    }

	// 监听ApplicationReadyEvent事件,触发websocket连接建立
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        ...
        this.connect();
    }

    ...
}

LongPollingClient实现了Spring的ApplicationListener接口,监听ApplicationReadyEvent事件,触发与配置中心服务端的websocket连接建立,建立websocket连接依然是使用okhttp。

然后在创建websocket连接,设置了一个监听器RefreshListener,在收到配置中心的通知时会回调该监听器。

RefreshListener

RefreshListener继承了WebSocketListener,WebSocketListener是OKHttp提供的抽象类,代表一个websocket监听器。

java 复制代码
public class RefreshListener extends WebSocketListener {

    ...

	// websocket连接建立后的回调,向配置中心服务端注册
	// 发送environmen(环境)和serviceName(服务名)到注册中心
    @Override
    public void onOpen(WebSocket webSocket, Response response) {
        JSONObject message = new JSONObject();
        message.put("environmen", environmen);
        message.put("serviceName", serviceName);
        webSocket.send(message.toJSONString());
    }

	// 收到配置中心服务端通知时回调,通过Spring事件监听机制,发一个RefreshEvent事件
    @Override
    public void onMessage(WebSocket webSocket, String text) {
        applicationContext.publishEvent(
                new RefreshEvent(this, null, "refresh config"));
    }

    ...

}

当websocket连接建立后,会回调RefreshListener的onOpen方法,会向配置中心服务端注册自己对应的environmen和serviceName,配置中心服务端会以environmen和serviceName为key,记录与该websocket连接对应的Session的映射关系,方便配置变更时通知客户端。

当客户端收到配置中心服务端的配置变更通知时,会回调RefreshListener的onMessage方法,会通过Spring事件监听机制,发一个RefreshEvent事件。发送的RefreshEvent事件会被RefreshEventListener接收,触发配置的重新加载和销毁旧的bean,上面已经说过。

LongPollingClientConfig

LongPollingClientConfig也是一个配置了,被spring.factories指定自动装配,通过@Bean往Spring注册一个LongPollingClient。

总结

这里我们可以再回顾一下服务端的LongPollingService和客户端的LongPollingClient的关系。

  1. 通过websocket建立了长连接
  2. 建立连接后,会回调监听器RefreshListener发送对应的environment和serviceName,LongPollingService接收到后会保存其与Session的对应关系
  3. 当服务端发生配置变更,会调用LongPollingService发送配置变更通知,LongPollingService会通过environment和serviceName取得对应的Session,通过Session推送配置变更通知
  4. 当客户端接收到配置变更通知后,会回调RefreshListener发送一个RefreshEvent事件
  5. 通过Spring的事件监听机制,RefreshEventListener接收到RefreshEvent事件,触发配置的重新加载和销毁旧的bean

由于篇幅关系,上面并没有展示详细的代码,如果想详细阅读代码的,可以git仓库下载:

https://gitee.com/huang_junyi/simple-microservice

相关推荐
xcl092540 分钟前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
灯澜忆梦2 小时前
【RabbitMQ #10】 | MQ可靠性
分布式·rabbitmq
一 乐2 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长3 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn3 小时前
Spring 注解使用详解
java·spring
一条小小yu3 小时前
Spring IoC的理解
java·后端·spring
落魄实习生3 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈4 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
萧瑟余晖4 小时前
Dubbo SPI扩展机制详解
架构·dubbo
Shulex4 小时前
面向跨境电商多渠道消息系统的技术架构:亚马逊站内信合规对接与自动化执行链路设计
运维·架构·自动化