ConfigX

ConfigX是一套完整的配置管理解决方案,分为配置管理平台和配置管理客户端。

入门指南

项目介绍

ConfigX项目提供了一个解决分布式系统的配置管理方案,为分布式系统中的外部配置提供服务器和客户端支持。 使用ConfigX Server,您可以在所有环境中管理应用程序的外部属性。 客户端和服务器上的概念映射与Spring Environment和PropertySource抽象相同,因此它们与Spring应用程序非常契合,但可以与任何以任何语言运行的应用程序一起使用。

ConfigX包含了Client和Server两个部分: configx-web 配置服务端,管理配置信息 configx-client 配置客户端,客户端调用服务端暴露接口获取配置信息,并集成到Spring Environment和PropertySource中。

ConfigX Client根据Spring框架的Environment和PropertySource从ConfigX Sever获取配置,因此需要先了解Spring Environment、PropertySource 、Profile等技术。

动机

随着微服务的流行,配置管理是实现微服务必不可少的一个组件。

现在市面上有很多配置管理系统:

  • 淘宝的diamond
  • 百度的disconf
  • 奇虎360的QConf
  • 微博的vintage

其中disconf对spring集成比较好,但是我觉得disconf有以下几点不足:

  1. 与Spring集成并不是特别简单。
  2. 没有提供配置文件到Spring Bean简单映射实现。
  3. 没有提供Spring国际化消息文件的集成。
  4. 无法实现配置继承(覆盖)。
  5. 无法实现配置发布的原子性。
  6. 没有提供配置修改历史记录。

Spring Cloud是一个实现微服务的框架,它提供了Spring Cloud Config来管理配置,Spring Cloud Config客户端和服务器上的概念映射与Spring Environment和PropertySource抽象相同,因此它们与Spring应用程序非常契合,但可以与任何以任何语言运行的应用程序一起使用。

但是个人觉得Spring Cloud Config有以下几点不足:

  1. 没有提供配置文件到Spring Bean简单映射实现。
  2. 没有提供Spring国际化消息文件的集成。
  3. 无法实现配置发布的原子性。

为了解决这些问题,我决定自己实现一套配置管理系统,它必须做到以下几点:

  1. 与Spring集成简单

    ConfigX-Client借鉴了Spring Boot和Spring Cloud Config的设计,全部使用标准的Spring扩展点与Spring进行集成,所以与Spirng应用的集成非常简单。

    在Spring应用中使用ConfigX只需要4个注解:

    • @EnableConfigService
      启用ConfigX支持
    • @ConfigBean
      定义配置文件转换成Spring Bean
    • @VersionRefreshScope
      指定Spring Bean是可刷新的bean,当配置修改时,bean会重新刷新
    • @EnableMessageSource
      启用ConfigX对Spring消息国际化的支持
  2. 支持配置文件映射成Spring Bean

    ConfigX只需要在Spring Bean上加上注解@ConfigBean,ConfigBean的value直接配置文件名称,就可以轻松将配置文件映射成Spring的bean。

  3. 支持管理Spring国际化消息文件

    ConfigX只需要通过注解@EnableMessageSource来启用ConfigX对Spring国际化的支持,并不用修改获取国际化消息的代码,使用Spring标准的API来获取国际化消息。

  4. 支持配置继承(覆盖)

    ConfigX的配置文件定义了不同的profile,客户端通过激活多个profile来激活不同的配置,如果不同的profile下有相同的配置,则按客户端激活profile的顺序优先获取较前面的profile的配置。

  5. 支持配置发布的原子性

    ConfigX通过对配置进行多版本并发控制,使得修改的多个配置可以原子性的发布,应用程序中不用担心访问到部分生效的配置。

特性

  • 配置管理
    • 属性配置管理,配置管理平台提供友好的配置编辑器,对于日期时间可以非常方便的进行设置。
    • 文件配置管理,配置管理平台提供强大的文件编辑器,可以非常方便的对文件进行操作。
    • 配置修改并发布后,应用可以实时获取到修改的配置并实时生效。
  • 维度设计
    • App

      具体的应用

    • Env

      应用的环境,比如开发环境,测试环境,线上环境,不同环境之间的配置完全隔离。

    • Profile

      Maven和Spring中都提供了Profile,然后可以激活不同的profiles,Spring Boot和Spring Cloud Config中的配置管理也充分使用了Spring的Profile技术来进行配置管理。

      Profile可以让我们定义一系列的配置,然后客户端指定激活哪些profile。这样我们就可以定义多个profile,然后不同的客户端激活不同的profiles,从而达到不同环境使用不同配置信息的效果,如果多个profile下有相同的配置,则按profile的指定顺序来获取较前面的profile下的配置。

      由于Env之间的配置是完全隔离的,所以增加Profile维度来弥补Env维度的不足。

      配置管理平台会创建一个default profile,如果未定义profile,配置将会放在default profile下,default profile自动激活,不用客户端显示激活。

      通过Profile维度,可以轻松实现以下功能:

      • 配置继承,一个应用部署的多套系统使用的配置几乎相同,只有少部分不同,这样这些系统就可以使用同一个环境,然后定义多个Profile,这些系统通过激活不同的profiles来达到配置继承(覆盖)的目的。
      • 配置分支,当项目同时多个分支开发时,为了保证配置不相互影响,可以给每个分支定义新的profile,每个分支的配置放在分支自己的profile下,然后激活分支自己的profile即可。
      • 灰度发布,为新版本的代码定义一个新的profile,然后灰度发布的系统激活这个新的profile下的配置。
  • 配置版本控制

    跟代码版本控制类似,我们对配置也就行了版本控制,可以查看配置的历史修改记录。

  • 发布模式

    我们提供两种发布模式

    • 自动发布

      配置修改完立刻自动发布,配置实时生效

    • 审核发布

      配置修改完后,并不立刻发布,而是需要建立一个发布单,发布单审核通过后,才能发布,审核发布模式可以实现更安全可控的发布,减少配置错误导致的严重后果的风险。

  • 配置发布的原子性

    在客户端中实现了多版本的配置管理控制,当发布多个配置时,每个线程看到的这几个配置要么都是修改前的值,要么都是修改后的值,不会出现某些配置是修改后值而另外一些配置是修改前的值,保证配置的原子性。

  • 与Spring集成简单,对应用代码无侵入。

    • 支持Spring属性文件

    原来Spring应用中的属性文件迁移到配置管理平台后,仍然可以使用@Value注解来注入属性文件中的属性,应用程序并不需要修改任何代码,原因是ConfigX使用了Spring框架的Environment和PropertySource扩展。

    • 支持XML/JSON等文件映射到Spring Bean

    当需要将一个文件映射成Spring Bean时,只需要将这个文件配置在配置管理平台中,然后在Spring Bean上增加注解@ConfigBean(value=”配置文件名”, converter=文件转换成Bean的转换器类)。

    • 支持Spring国际化消息文件

    原来Spring应用中的i18n消息文件迁移到配置管理平台后,仍然可以使用MessageSource.getMessage方法来获取国际化消息,应用程序并不需要修改任何代码,原因是CongigX使用了Spring的MessageSource扩展。

维度设计

ConfigX配置包括App、Env和Profile三个维度。

App 应用

App用来指定一个具体的应用。

Env 环境

Env环境用来指定应用的环境,比如开发环境,测试环境,线上环境,不同环境之间的配置完全隔离。

Profile 剖面

Profile是ConfigX中所定义的一系列配置的逻辑组名称,只有当这些Profile被激活的时候,才会将Profile中所对应的配置注册到应用中。

Profile可以让我们定义一系列的配置,然后客户端指定激活哪些profile。这样我们就可以定义多个profile,然后不同的客户端激活不同的profiles,从而达到不同环境使用不同配置信息的效果,如果多个profile下有相同的配置,则按profile的指定顺序来获取较前面的profile下的配置。

由于Env之间的配置是完全隔离的,所以增加Profile维度来弥补Env维度的不足。

配置管理平台会创建一个default profile,如果未定义profile,配置将会放在default profile下,default profile自动激活,不用客户端显示激活。

通过Profile维度,可以轻松实现以下功能:

  • 配置继承,一个应用部署的多套系统使用的配置几乎相同,只有少部分不同,这样这些系统就可以使用同一个环境,然后定义多个Profile,这些系统通过激活不同的profiles来达到配置继承(覆盖)的目的。
  • 配置分支,当项目同时多个分支开发时,为了保证配置不相互影响,可以给每个分支定义新的profile,每个分支的配置放在分支自己的profile下,然后激活分支自己的profile即可。
  • 灰度发布,为新版本的代码定义一个新的profile,然后灰度发布的系统激活这个新的profile下的配置。

更多Profile设计参考

Maven Profile

mvn命令可以通过-p选项来激活profiles。

-P,–activate-profiles <arg> Comma-delimited list of profiles

了解更多 Maven Profiles

Spring Profile

Spring Profile是Spring 3引入的概念,包括默认Profile和明确激活的Profiles。

默认Profile是指在没有任何profile被激活的情况下也能自动激活的profile,通过spring.profiles.default指定默认的profile。

明确激活的Profile,通过spring.profiles.active指定,也可以在程序中使用ConfigurableEnvironment#setActiveProfiles来激活profiles。

了解更多 Spring Environment abstraction

Spring Boot

Spring Boot中也支持profiles来获取配置。

相关的属性:

  • spring.profiles.active
  • spring.profiles.include
  • spring.profiles

了解更多 Spring Boot Profiles

Spring Cloud Config

Spring Cloud Config也支持profile来获取配置。

了解更多 Spring Cloud Config

安装

configx-web安装

下载configx-web

https://github.com/zouzhirong/configx/releases/latest

解压

tar -zxvf configx-web-1.0.1.tar.gz

安装Mysql

https://dev.mysql.com/downloads/mysql/

执行sql文件

tables_mysql.sql

修改配置文件configx.properties

# 端口
http.port=3964

# 数据库信息
datasource.host=localhost
datasource.port=3306
datasource.database=configx
datasource.username=root
datasource.password=

启动configx-web

java -jar configx-web-1.0.1.jar

configx-client安装

添加configx-client的Maven依赖

<dependency>
  <groupId>com.configx</groupId>
  <artifactId>configx-client</artifactId>
  <version>1.0.1</version>
</dependency>

先决条件

  • Java 8
  • Spring 3.1及以上版本

Configx Client

ConfigX与Spring无缝集成,支持Spring里面Environment和PropertySource的接口,对于已有的Spring应用程序的迁移成本非常低,在配置获取的接口上完全一致。

configx-client使用

添加configx-client Maven依赖

<dependency>
  <groupId>com.configx</groupId>
  <artifactId>configx-client</artifactId>
  <version>1.0.1</version>
</dependency>

在src/main/resources下增加configx.properties配置文件

configx.client.config.app=应用ID,必填
configx.client.config.env=应用环境名称,必填
configx.client.config.profiles=Profile列表,多个之间用逗号分隔,如果多个Profile存在相同的配置,则越靠前的Profile优先级越高,选填,如果为空,则只获取默认Profile下的配置
configx.client.config.uri=配置管理系统提供的API URL:http://配置管理系统host/v1/config/

使用@EnableConfigService注解开启配置管理服务

Configx Client 教程

通过一些例子来讲解Configx Client的使用。

使用Configx管理Spring属性配置

configx与Spring无缝集成,支持Spring里面Environment和PropertySource的接口,对于已有的Spring应用程序的迁移成本非常低,在配置获取的接口上完全一致。

假设我有一个TestBean,它有一个timeout属性需要注入:

public class TestBean {

    @Value("${timeout}")
    private long timeout;

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public long getTimeout() {
        return timeout;
    }
}

在configx配置管理平台中添加timeout配置项

configx既支持直接将属性作为一个配置项,也支持将属性放在属性文件中并将属性文件作为配置项。

直接将timeout属性作为配置项

如果直接将属性作为一个配置项,只需要在configx管理平台中添加一个配置项,配置名称为属性名timeout,配置值为属性值比如2000.

属性配置文件作为配置项

将timeout属性放在属性配置管理中,并将属性配置文件作为一个配置项。

跟Spring属性配置文件类似,当使用属性配置文件来配置属性时,需要指定属性配置文件名称。

在Spring中通过context:property-placeholder来指定属性配置文件。

<context:property-placeholder location="classpath:foo.properties" />

在configx管理平台中通过spring.property.sources配置项来指定属性配置文件的配置项名称

spring.property.sources=application.properties,database.properties,redis.properties

如果没有spring.property.sources配置项,则默认使用application.properties属性文件来配置属性。

在程序中使用timeout属性

在configx管理平台添加完timeout配置项后,就可以在Spring应用中跟使用普通Spring属性一样来使用timeout属性了。

基于XML的配置 在使用属性之前,首先需要通过configx扩展标签:<configx:config/>来开启configx。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:configx="http://www.configx.com/schema/configx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.configx.com/schema/configx
       http://www.configx.com/schema/configx/configx.xsd">

    <!-- 开启配置管理服务 -->
    <configx:config/>

    <bean name="testBean" class="com.configx.demo.TestBean">
        <property name="timeout" value="${timeout}"/>
    </bean>

</beans>

基于Java的配置(推荐) 在使用属性之前,首先需要通过@EnableConfigService注解来开启configx。

@Configuration
@EnableConfigService // 启动配置管理,并注册XmlConfigConverter
public class Application {

}

@Component
public class TestBean {

    @Value("${timeout}")
    private long timeout;

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public long getTimeout() {
        return timeout;
    }
}

到这里,属性注入的工作已经完成。

Spring并没有提供属性刷新的功能,为了解决这个问题,configx通过自定义的scope来解决bean的属性热修改问题。

这个自定义的scope叫version-refresh,即基于配置版本刷新,类似于spring cloud config的refresh scope,但是区别在于configx会基于配置版本自动刷新,并且做了多版本并发控制。

如果将一个bean的scope设置为version-refresh,那么当configx有新版本的配置发布时,并且bean依赖的属性在新版本有修改时,那么这个bean会重新创建并在创建时重新注入新的属性值,一个bean可能同时存在多个版本,当有新版本的bean创建时,旧版本的bean在没有任何引用的情况下configx会将其销毁。

为了将TestBean中的timeout能够热修改,只需要将bean的scope设置为version-refresh,同时设置bean依赖的属性为timeout即可。

基于XML的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:configx="http://www.configx.com/schema/configx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.configx.com/schema/configx
       http://www.configx.com/schema/configx/configx.xsd">

    <!-- 开启配置管理服务 -->
    <configx:config/>

    <bean name="testBean" class="com.configx.demo.TestBean">
        <property name="timeout" value="${timeout}"/>
        <configx:version-refresh dependsOn="timeout"/>
        <aop:scoped-proxy proxy-target-class="true"/>
    </bean>

</beans>

注意:基于XML的配置中,属性的热修改无法正常工作,这是因为在Bean定义解析阶段,spring就将${timeout}属性占位符解析成最终的值并添加到bean定义的propertyValues中,当bean创建时,直接使用的是timeout实际的值,而非$timeout}占位符。 所以尽管将testBean的scope设置为version-refresh,且设置依赖的属性为timeout,但是在timeout属性修改时,testBean会重新创建,但是使用的仍然是bean定义中的最初的timeout的值。

具体可查阅Spring源码:org.springframework.beans.factory.config.PlaceholderConfigurerSupport.doProcessProperties

所以要想bean属性支持热修改,请使用基于Java的配置。

基于Java的配置(推荐)

@Configuration
@EnableConfigService // 启动配置管理,并注册XmlConfigConverter
public class Application {

}

@Component
@VersionRefreshScope(dependsOn = {"timeout"})
public class TestBean {

    @Value("${timeout}")
    private long timeout;

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public long getTimeout() {
        return timeout;
    }
}

在TestBean类上添加@VersionRefreshScope(dependsOn = {“timeout”}),就将bean的scope设置成了version-refresh,并且设置了依赖的属性为timeout,这样当有新版本的配置发布时,如果timeout属性修改了,那么TestBean将会重新创建。

注意:TestBean的scope虽然为version-refresh,但是依然可以正常注入到其他单例的bean中,其原理是Spring为自定义scope的bean创建了一个代理,并将代理注入到其他单例bean中。

具体可参考Spring文档。

使用Configx来配置自定义文件

在项目开发中,不仅有简单的key-value属性,还会有自定义的复杂的配置文件,比如有一个学生的配置文件: students.xml:

<students>
    <student id="1" name="张三"></student>
    <student id="2" name="李四"></student>
    <student id="3" name="王五"></student>
    <student id="4" name="赵六"></student>
</students>

然后我们需要将这个配置文件映射到Spring的bean。

首先,我们需要在configx配置管理平台中创建一个students.xml的配置文件。

_images/students.png

然后,定义一个解析students.xml的解析器类。

/**
 * 通过实现ConfigBeanConverter接口自定义配置转换器,将XML文件转换成Bean
 * 我这里使用的是[simple-xml](http://simple.sourceforge.net/)框架将xml映射成Bean。
 */
public class XmlConfigConverter implements ConfigBeanConverter {

    private static final Logger LOGGER = LoggerFactory.getLogger(XmlConfigConverter.class);

    @Override
    public boolean matches(String propertyName, String propertyValue, TypeDescriptor targetType) {
        return propertyName != null && propertyName.endsWith(".xml");
    }

    @Override
    public Object convert(String propertyName, String propertyValue, TypeDescriptor targetType) {
        Class<?> targetClass = targetType.getType();
        Serializer serializer = new Persister(new AnnotationStrategy());
        try {
            return serializer.read(targetClass, propertyValue, false);
        } catch (Exception e) {
            LOGGER.error("Convert xml to " + targetClass + " error", e);
        }
        return null;
    }

}

最后,在Students类上使用注解@ConfigBean和VersionRefreshScope。

public class Student {
    @Attribute
    private String id;

    @Attribute
    private String name;
}

@ConfigBean(value="students.xml", converter=XmlConfigConverter.class)
@VersionRefreshScope
public class Students {
    @ElementList(inline = true, entry = "student")
    private List<Student> students;
}

@ConfigBean首先是一个@Component注解,另外通过value指定了bean由哪个配置名转换而来,通过converter指定具体的转换器。

@VersionRefreshScope用于当student.xml修改时,刷新Students bean,这里并不需要设置dependsOn={“students.xml”},因为对于@ConfigBean的bean,会自动把@ConfigBean的value的值当成dependsOn。

当项目中有大量的@ConfigBean时,并且使用相同的转换器来解析配置,可以在@EnableConfigService注解中通过converters来统一注册多个转换器,并不需要在每个ConfigBean上注册转换器。

@EnableConfigService(converters = {XmlConfigConverter.class}) // 启动配置管理,并注册XmlConfigConverter

Configx支持Spring国际化消息

开启Configx对Spring国际化消息的支持

  • 基于XML的配置

需要把configx相关的xml namespace加到配置文件头上。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:configx="http://www.configx.com/schema/configx"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
   http://www.configx.com/schema/configx
   http://www.configx.com/schema/configx/configx.xsd">

    <!-- 开启配置管理对message source的支持 -->
    <configx:message-source fallbackToSystemLocale="false" basenames="messages"/>

</beans>
  • 基于Java的配置(推荐)

相对于基于XML的配置,基于Java的配置是目前比较流行的方式,也是Spring Boot的默认配置方式。

@Configuration
@EnableMessageSource(fallbackToSystemLocale = false, basenames = {})    // 开启配置管理对消息国际化的支持
public class Application {

}

指定basenames

有3种方式可以指定basenames:

  • 通过@EnableMessageSource注解的basenames方法指定,或configx:message标签的basenames属性指定。
  • 在configx配置管理平台中通过spring.messages.basename配置项来指定。
  • 在Spring的environment中指定spring.messages.basename属性。

首先,在配置管理系统中,创建一个配置spring.messages.basename,内容为messages。 然后,创建一个messages.xml文件,将项目本地中的messages.xml内容复制到配置管理系统的messages.xml文件中。

_images/config_messagesource.png _images/messages.png

使用configx热修改数据源

在configx配置管理平台中增加配置

在configx配置管理平台中,创建一个文件类型的配置database.properties,内容为:

datasource.driverClassName=com.mysql.jdbc.Driver
datasource.url=jdbc:mysql://192.168.1.199:3306/configx
datasource.username=root
datasource.password=test
_images/database1.png _images/database2.png

将database.properties添加到spring.property.sources配置项中,如果没有spring.property.sources配置项,则创建一个。

_images/database3.png

程序代码如下:

/**
 * 数据源属性
 * <p>
 * Created by zouzhirong on 2017/9/26.
 */
public class DataSourceProperties {

    @Value("${datasource.driverClassName}")
    private String driverClassName;

    @Value("${datasource.url}")
    private String url;

    @Value("${datasource.username}")
    private String username;

    @Value("${datasource.password}")
    private String password;

    public String getDriverClassName() {
        return driverClassName;
    }

    public String getUrl() {
        return url;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }
}

/**
 * 数据源Bean Configuration
 * Created by zouzhirong on 2017/9/25.
 */
@Configuration
public class DataSourceConfiguration {

    @Bean
    @VersionRefreshScope(dependsOn = {"datasource.url"})
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @VersionRefreshScope(proxyMode = ScopedProxyMode.TARGET_CLASS, dependsOn = {"datasource.url"})
    public BasicDataSource dataSource(DataSourceProperties dataSourceProperties) {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        dataSource.setUrl(dataSourceProperties.getUrl());
        dataSource.setUsername(dataSourceProperties.getUsername());
        dataSource.setPassword(dataSourceProperties.getPassword());
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

}

/**
 * 数据库样例
 * 支持热修改数据源地址
 */
@Service
public class DatabaseExample implements InitializingBean {

    // inject the actual template
    @Autowired
    private JdbcTemplate jdbcTemplate;
}

将dataSourceProperties和dataSource两个bean的scope都设置为version-refresh,并且设置依赖属性为datasource.url,这样当有新版本的配置发布且datasource.url属性有更改,那么dataSourceProperties和dataSource两个bean会重新创建。

新的请求会使用新创建的dataSource,但是这时候旧的dataSource的bean并没有destory,当没有任何线程使用旧的dataSource时,configx-client会将其destory并从scope中移除,然后被gc掉,所有可能同时存在多个dataSource bean实例。 这个有点像nginx重启一样,nginx先启动新的进程用于服务新的请求,但是这时候旧的nginx进程并没有关闭,继续在服务旧的请求,直接没有任何旧的请求了,再关闭旧的nginx进程。 通过这种方式,可以实现线上热修改redis到新的地址,而并不会影响正在使用旧redis地址的请求。

使用configx热修改redis

在configx配置管理平台中增加配置

在configx配置管理平台中,创建一个文件类型的配置redis.properties,内容为:

redis.host=localhost
redis.port=6379
_images/redis1.png _images/redis2.png

将redis.properties添加到spring.property.sources配置项中,如果没有spring.property.sources配置项,则创建一个。

_images/redis3.png

程序代码如下:

public class RedisProperties {

    @Value("${redis.host}")
    private String host;

    @Value("${redis.port}")
    private int port;

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }
}

@Configuration
public class RedisConfiguration {

    @Bean
    @VersionRefreshScope(dependsOn = {"redis.host", "redis.port"})
    public RedisProperties redisProperties() {
        return new RedisProperties();
    }

    @Bean
    @VersionRefreshScope(dependsOn = {"redis.host", "redis.port"})
    public JedisConnectionFactory jedisConnFactory(RedisProperties redisProperties) {
        JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory();
        jedisConnFactory.setHostName(redisProperties.getHost());
        jedisConnFactory.setPort(redisProperties.getPort());
        jedisConnFactory.setUsePool(true);
        return jedisConnFactory;
    }

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }

}

@Service
public class RedisExample implements InitializingBean {

    // inject the actual template
    @Autowired
    private RedisTemplate<String, String> template;
}

将redisProperties和redisTemplate两个bean的scope都设置为version-refresh,并且设置依赖属性为redis.host和redis.port,这样当有新版本的配置发布且redis.host和redis.port任一属性有更改,那么redisProperties和redisTemplate两个bean会重新创建。

新的请求会使用新创建的RedisTemplate,但是这时候旧的RedisTemplate的bean并没有destory,当没有任何线程使用旧的RedisTemplate时,configx-client会将其destory并从scope中移除,然后被gc掉,所有可能同时存在多个RedisTemplate bean实例。 这个有点像nginx重启一样,nginx先启动新的进程用于服务新的请求,但是这时候旧的nginx进程并没有关闭,继续在服务旧的请求,直接没有任何旧的请求了,再关闭旧的nginx进程。 通过这种方式,可以实现线上热修改redis到新的地址,而并不会影响正在使用旧redis地址的请求。

使用configx热修改线程池

在configx配置管理平台中增加配置

在configx配置管理平台中,创建一个文件类型的配置threadpool.properties,内容为:

threadpool.corePoolSize=10
_images/threadpool1.png _images/threadpool2.png

将threadpool.properties添加到spring.property.sources配置项中,如果没有spring.property.sources配置项,则创建一个。

_images/threadpool3.png

程序代码如下:

/**
 * 线程池属性
 * Created by zouzhirong on 2017/9/26.
 */
public class ThreadPoolProperties {

    @Value("${threadpool.corePoolSize}")
    private int corePoolSize;

    public void setCorePoolSize(int corePoolSize) {
        this.corePoolSize = corePoolSize;
    }

    public int getCorePoolSize() {
        return corePoolSize;
    }

}

/**
 * 线程池Bean Configuration
 * Created by zouzhirong on 2017/9/25.
 */
@Configuration
public class ThreadPoolConfiguration {

    @Bean
    @VersionRefreshScope(dependsOn = {"threadpool.corePoolSize"})
    public ThreadPoolProperties threadPoolProperties() {
        return new ThreadPoolProperties();
    }

    @Bean
    @VersionRefreshScope(dependsOn = {"threadpool.corePoolSize"})
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolProperties threadPoolProperties) {
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor)
                Executors.newFixedThreadPool(threadPoolProperties.getCorePoolSize());
        return threadPoolExecutor;
    }

}

/**
 * 线程池样例
 * 支持热修改线程池参数
 */
@Service
public class ThreadPoolExample implements ConfigItemListener, InitializingBean {

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Override
    public void onApplicationEvent(ConfigItemChangeEvent event) {
        if (event.getItemList() != null) {
            for (ConfigItem configItem : event.getItemList()) {
                if ("threadpool.corePoolSize".equals(configItem.getName())) {

                    // threadpool.corePoolSize属性修改,需要更新ThreadPoolExecutor的corePoolSize
                    int corePoolSize = Integer.valueOf(configItem.getValue());
                    threadPoolExecutor.setCorePoolSize(corePoolSize);

                }
            }
        }
    }
}

多版本并发控制

在自动发布模式下,更改任何一个配置,都会立刻自动发布。 在审核发布模式下,有可能修改了多个配置,然后一起发布。

客户端默认情况下,每次访问配置相关的属性或者配置Bean时,都会获取最新的配置。 例如:

List<Student> studentList1 = students.getStudents();
List<Student> studentList2 = students.getStudents();

假如执行完第一行代码之后,得到了studentList1的值,然后更新到了students配置文件的内容,那么studentList2将会得到最新的值。

这在大部分情况下是正确的,但是假如配置是一个跟金钱相关的,比如买道具需要的钱的数量,如果同一次请求中,两次获取到的钱的数量不一致,就可能导致问题。

为了保证在一个“事务”中,获取到的配置都是一致的,我们在configx-client中增加了多版本控制(mvcc)支持,即在configx-client中会保存配置的多个版本,而“事务”的整个生命周期中看到的只是配置的一个版本。 还是以上面的例子为例:

List<Student> studentList1 = students.getStudents();
List<Student> studentList2 = students.getStudents();

假如执行第一行代码时,students的配置版本为1,接着更新到了students配置文件的内容,配置版本为2,configx-client中同时有两个版本的students,由于在第一行代码执行时,获取到的配置版本为1,所以执行第二行代码时,获取到的配置版本还是1,配置版本为2的更新需要等待下一次“事务”才会生效。

多版本控制为了保证同一个“事务”中,不管配置是否被修改,这个事务中看到的配置是一致的。 开启多版本控制,需要在configx.properties中添加属性:

configx.client.mvcc.enabled=true

开启了mvcc之后,需要在程序中手动清除线程的版本号信息,否则线程将一直使用第一次的版本号,不会随着配置的更新而自动更新。 清除线程中的版本号,调用以下方法:

VersionContextHolder.clearCurrentVersion();

通常在一个“事务”结束以后,需要清理线程中的版本号。 常见的“事务”比如: 1、一个Http请求,可以在Filter中清除,比如:

@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException
{
    try {
        ...
    } catch (Exception e) {
        ...
    } finally {
        VersionContextHolder.clearCurrentVersion();
    }
}

2、在线程池中执行的任务,可以在ThreadPoolExecutor的afterExecute方法中清除,比如:

@Override
protected void afterExecute(Runnable r, Throwable t) {
    VersionContextHolder.clearCurrentVersion();
    super.afterExecute(r, t);
}

如果是Spring TaskExecutor,可以使用ConcurrentTaskExecutor来自定义ThreadPoolExecutor,覆盖afterExecute方法。

如果是Spring TaskScheduler,可以覆盖ConcurrentTaskScheduler来自定义ThreadPoolExecutor,覆盖afterExecute方法。

详细请参考:

Spring Task Execution and Scheduling

TaskExecutor

ThreadPoolTaskExecutor

ConcurrentTaskExecutor

TaskScheduler

ThreadPoolTaskScheduler

ConcurrentTaskScheduler

3、自定义“事务”,可以使用try...finally来清除,比如:

try {
    ...
} finally {
    VersionContextHolder.clearCurrentVersion();
}

Configx Server

configx-web使用

打开configx配置管理平台

安装完configx-web并启动后,访问http://host:port/,host是运行configx-web的主机ip,port是configx-web的端口,默认是3964,进入configx配置管理平台。

登录

configx-web默认的管理员账号:

账号 admin
密码 admin123
_images/login.png

创建应用

进入配置管理平台后,出现应用列表界面。

_images/app_empty.png

现在还没有应用,点击创建应用。

_images/app_add.png
  • 名称:应用的名称,唯一标识一个应用,可以包括中文。
  • 描述:应用的描述信息。
  • 管理员邮箱:设置对这个应用有管理员权限的用户的邮箱列表,可以使用(,)逗号、(;)分号、( )空格、(t)Tab、(n)换行来分隔,默认会把当前创建应用的用户邮箱添加到管理员列<表中。
  • 开发者邮箱:设置对这个应用有开发者权限的用户的邮箱列表,可以使用(,)逗号、(;)分号、( )空格、(t)Tab、(n)换行来分隔,默认会把当前创建应用的用户邮箱添- 加到开发者列表中。

应用创建后,在应用列表中可以看到刚才新创建的应用。

_images/app.png

创建环境

在应用列表页面,点击应用列表右侧的“环境”按钮,进入到应用环境管理页面。

_images/env_empty.png

现在应用下没有任何环境,点击创建环境。

_images/env_add.png
  • 名称:环境的名称,唯一标识应用的一个环境,只能包含英文。
  • 别名:环境的别名,为这个环境设置别名,客户端在指定环境时,既可以使用环境名称,也可以使用环境别名。
  • 描述:环境的描述信息。
  • 顺序:设置环境的显示顺序,与功能无关,在管理系统中,在涉及到环境选择列表时,都会根据这个顺序来排序显示,值越小越靠前,值越大越靠后。

创建完环境后,就可以添加配置了。点击顶部导航条的“配置管理”,进入到配置管理页面。

_images/config_empty.png

创建文件配置

_images/config_file_add.png

创建文本配置

_images/config_text_add.png

创建数值配置

_images/config_number_add.png

创建日期配置

_images/config_date_add.png

创建时间配置

_images/config_time_add.png

创建日期时间配置

_images/config_datetime_add.png

查看配置列表

_images/config.png

架构设计