实现一个支持运行时并发修改配置生效的Configuration类

可配置性是一个好的应用程序的重要指标。我们常常需要实现类似能够运行时修改配置的功能。最近在开发一个中间层的服务程序,最终发布的方式是把代码打成jar包交给调用方使用。这个中间层服务需要一些配置信息,考虑了一下有几个基本的需求:

1. 在ja包中提供一个service-defalut.properties配置文件来提供全部的默认配置。这样的好处是尽量减少对调用方的侵入。调用方可以不提供额外的配置。

2. 调用方也可以提供一个service-site.properties配置文件来提供自定义的配置信息,可以覆盖默认配置

3. 在分布式系统中,希望提供一个在集群中全局可见的配置信息,比如可以在ZooKeeper中设置配置信息

4. 支持并发环境下运行时修改配置,并且可以立刻生效

5. 高性能访问配置信息

之前看过Hadoop的代码,Hadoop的org.apache.hadoop.conf.Configuration实现了1,2,4项需求,但是它访问配置信息的性能不高,原因是为了支持并发访问,对读写配置都采用了加锁的方式,锁的粒度是方法级的,会影响并发的性能。

大致说一下org.apache.hadoop.conf.Configuration的实现

1. 采用Properties来存储K-V的配置信息

2. 采用CopyOnWriteArrayList来保存默认的配置文件列表

3. 采用ArrayList来保存自定义的配置文件列表

4. 对上述3个共享对象的访问都采用了加锁的方式来访问,保证并发情况下的正确性

public class Configuration implements Iterable<Map.Entry<String,String>>,
                                      Writable {
      private ArrayList<Object> resources = new ArrayList<Object>();

      private static final CopyOnWriteArrayList<String> defaultResources =
    new CopyOnWriteArrayList<String>();

      private Properties properties;

       public static synchronized void addDefaultResource(String name) {
    if(!defaultResources.contains(name)) {
      defaultResources.add(name);
      for(Configuration conf : REGISTRY.keySet()) {
        if(conf.loadDefaults) {
          conf.reloadConfiguration();
        }
      }
    }
  }

      public synchronized void reloadConfiguration() {
    properties = null;                            // trigger reload
    finalParameters.clear();                      // clear site-limits
  }
 
  private synchronized void addResourceObject(Object resource) {
    resources.add(resource);                      // add to resources
    reloadConfiguration();
  }

  private synchronized Properties getProps() {
    if (properties == null) {
      properties = new Properties();
      loadResources(properties, resources, quietmode);
      if (overlay!= null) {
        properties.putAll(overlay);
        for (Map.Entry<Object,Object> item: overlay.entrySet()) {
          updatingResource.put((String) item.getKey(), UNKNOWN_RESOURCE);
        }
      }
    }
    return properties;
  }

  public String getRaw(String name) {
    return getProps().getProperty(name);
  }

  public void set(String name, String value) {
    getOverlay().setProperty(name, value);
    getProps().setProperty(name, value);
    this.updatingResource.put(name, UNKNOWN_RESOURCE);
  }

}

org.apache.hadoop.conf.Configuration 在配置的数据来源上是灵活地,可以动态的添加。它的主要问题是锁太多,读写都加锁,严重影响了并发访问的性能。

简单分析一下这个需求的场景:

1. 基于配置文件的配置信息一般是在启动服务时已经配置好了,可以用static加载的方式一次加载,是线程安全的

2. 基于运行时修改配置信息,即写配置的情况非常小,比如把配置设置在ZooKeeper中,只有在需要修改时才会运行时修改,非常少的机会会去修改配置

3. 99%以上的场景是读配置信息,最好不要加锁

基于这几个需求,我写了一个简单的Configuration实现,可以实现一下功能:

1. 灵活支持多种配置信息来源

2. 支持运行时修改配置信息,并立刻生效

3. 写配置操作保证强一致性,不会丢失写的内容。写操作需要加锁。

4. 读配置操作保证最终一致性,减少了锁的粒度,在没有写配置的情况下是无锁的。这样大大地提高了并发情况下性能

代码如下,简单测试了一下,考虑不周全的地方欢迎来拍。主要考虑并发的地方有:

1. 采用ConcurrentHashMap来取代Properties保存配置内容,提高并发下的性能和保证正确性

2. 采用volatile标识properties属性,这样保证了在reloadConfiguration时设置properties = null的操作对读操作get()立刻可见

3. get()读操作时,使用了一个引用指向properties,而不是直接使用properties,这样可以在properties被设置成null时,get操作还能读取到旧的配置,保证下一次读时能读到最新内容,这里保证了最终一致性。只有在properties == null的情况下(有配置修改),get操作才有可能加锁去加载配置

4. set()写操作时加锁,这样保证同意时候只能一个线程去修改配置。set不会重新加载配置,ConcurrentHashMap保证了set的值能立刻被get读取到。

import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 *	Configuration用来读写配置信息,支持运行时配置的修改,支持多种数据源的配置
 *  可以通过addResource()来修改配置,也可以通过set()来修改配置,保证强一致性
 *  get()保证最终一致性,通过减小锁的粒度来提高性能,运行时如果不调用addResource(),是无锁的
 */
public class Configuration {
	private static Configuration instance = new Configuration();
	private Configuration(){}
	public static Configuration getInstance(){
		return instance;
	}

	public static final String DEFAULT_CONFIG_FILE = "service-default.properties";
	public static final String SITE_CONFIG_FILE = "service-site.properties";

	private static List<String> defaultResources = new ArrayList<String>();
	private static void addDefaultResource(String name){
		if(!defaultResources.contains(name)){
			defaultResources.add(name);
			instance.reloadConfiguration();
		}
	}

	static{
		addDefaultResource(DEFAULT_CONFIG_FILE);
		addDefaultResource(SITE_CONFIG_FILE);
	}

	private List<Object> resources = new ArrayList<Object>();

	private volatile ConcurrentHashMap<String, String> properties;

	private synchronized void reloadConfiguration(){
		properties = null;
	}

	private synchronized ConcurrentHashMap<String, String> getProperites(){
        // 减小锁粒度,提高性能
            if(properties == null){
                // 不直接使用properties,防止 properties = new ConcurrentHashMap<String, String>();之后被get()直接获取到未设置的properties
                ConcurrentHashMap<String, String> props = new ConcurrentHashMap<String, String>();
                loadResources(props, resources);
                properties = props;
            }
            return properties;
        }

	// 最常用的方法, 保证最终一致性
	public String get(String key){
		// 如果get时另外线程在addResource,将指向老的properties对象,取老的配置
		ConcurrentHashMap<String, String> p = properties;
		if(p == null){
			p = getProperites();
		}
	    return p.get(key);
    }

	// set保证强一致性
    public synchronized void set(String key, String value){
		getProperites().put(key, value);
    }

	private void loadResources(ConcurrentHashMap<String, String> props, List<Object> resources){
		// 先加载default
		for(String resource: defaultResources){
			loadResource(props, resource);
		}
		// 再加载自定义
		for(Object resource: resources){
			loadResource(props, resource);
		}
	}

	private void loadResource(ConcurrentHashMap<String, String> props, Object resource){
		if(props == null){
			return;
		}

		Properties newProps = new Properties();
		if(resource instanceof String){
			URL url = ResourceLoader.getResource((String)resource);
            if(url == null){
                return;
            }
			try {
				newProps.load(url.openStream());
			} catch (Exception e) {
				// quiet
			}
		}else if(resource instanceof InputStream){
			try {
				newProps.load((InputStream)resource);
			} catch (Exception e) {
				// quiet
			}
		}else if(resource instanceof URL){
			try {
				newProps.load(((URL)resource).openStream());
			} catch (Exception e) {
				// quiet
			}
		}else if(resource instanceof Properties){
			newProps = (Properties)resource;
		}

		for(Map.Entry<Object, Object> entry: newProps.entrySet()){
			props.put(entry.getKey().toString(), entry.getValue().toString());
		}
	}

	public void addResource(String obj){
		addResourceObject(obj);
	}

	public void addResource(URL obj){
		addResourceObject(obj);
	}

	public void addResource(InputStream obj){
		addResourceObject(obj);
	}

	public void addResource(Properties obj){
		addResourceObject(obj);
	}

	private synchronized void addResourceObject(Object obj){
		if(!resources.contains(obj)){
			resources.add(obj);
		}

		reloadConfiguration();
	}

}

简单的测试代码

import java.util.Properties;

public class ConfigTest {
	public static void main(String[] args){
		final Consumer obj = new Consumer();

		new Thread(new Runnable(){

			@Override
			public void run() {
				while(true){
					System.out.println(obj.getLogFile());
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}

		}).start();

		new Thread(new Runnable(){

			@Override
			public void run() {
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				Configuration config = Configuration.getInstance();
				config.set("log.file", "/data/A-service.log");
			}

		}).start();

		new Thread(new Runnable(){

			@Override
			public void run() {
				try {
					Thread.sleep(4000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				Configuration config = Configuration.getInstance();
				Properties p = new Properties();
				p.put("log.file", "/data/B-service.log");

				config.addResource(p);
			}

		}).start();
	}

	private static class Consumer{
		Configuration config = Configuration.getInstance();

		public String getLogFile(){
			return config.get("log.file");
		}
	}
}
时间: 2024-08-11 01:45:03

实现一个支持运行时并发修改配置生效的Configuration类的相关文章

Flume NG源代码分析(二)支持执行时动态改动配置的配置模块

在上一篇中讲了Flume NG配置模块主要的接口的类,PropertiesConfigurationProvider提供了基于properties配置文件的静态配置的能力,这篇细说一下PollingPropertiesFileConfigurationProvider提供的执行时动态改动配置并生效的能力. 要实现动态改动配置文件并生效,主要有两个待实现的功能 1. 观察配置文件是否改动 2. 假设改动,将改动的内容通知给观察者 对于第一点,监控配置文件是否改动,Flume NG定义了一个File

JAVA调用系统命令或可执行程序--返回一个Runtime运行时对象,然后启动另外一个进程来执行命令

通过 java.lang.Runtime 类可以方便的调用操作系统命令,或者一个可执行程序,下面的小例子我在windows和linux分别测试过,都通过.基本原理是,首先通过 Runtime.getRuntime() 返回与当前 Java 应用程序相关的运行时对象,然后调用run.exec(cmd)  另启一个进程来执行命令(cmd为要执行的命令). 一.运行一个可执行程序 执行一个.exe的文件,或通过已安装的软件打开一个特定格式的文件,如word.chm或mp3等等. 1. 在window下

基于c++11新标准开发一个支持多线程高并发的网络库

背景 新的c++11标准出后,c++语法得到了很多的扩展,比起以往任何时候都要灵活和高效,提高了程序编码的效率,为软件开发人员节省了不少的时间. 之前我也写过基于ACE的网络服务器框架,但ACE毕竟有些臃肿,内部对象关系错综复杂,容易给人造成只见树木不见森林的错觉. 所以打算用c++11开发一个较为简洁,高效,支持高并发的网络库. 开源         花了两三周,终于把基础的结构开发完成,代码也开源在github上,网址是 https://github.com/lichuan/fly 欢迎各位

LoadRunner中获取一个场景运行时的唯一值

/* * 本代码产生一个从1970年1月1日0时开始累计以毫秒为单位的数值, * 在需要唯一值的地方使用时前缀上VuserID以保证场景运行期内该值为唯一 * (局限:不适用于脚本单次执行时间小于1毫秒的情况,当然该情况非常罕见,所以请放心使用) */ int a; int b; typedef long time_t; struct _timeb { time_t time; unsigned short millitm; short timezone; short dstflag; }; s

Unity3d运行时动态修改材质

void Start () { const string MainTexVariableName = "_MainTex"; var renders = gameObject.GetComponentsInChildren<Renderer>(); var resMat = Resources.Load<Material>("CharacterThrough"); foreach (var render in renders) { var o

解决spring 事务管理默认不支持SQLException等运行时异常

公司同事在定位一个bug时,发现spring默认的事务只支持运行时异常的回滚,对于像SQLException这样的非运行时异常,默认的事务机制不能处理,于是找了下解决的办法:    1.在捕获SQLException的时候,抛出一个RuntimeException及其子类,例如:     try {               xxx        } catch (Exception e) {               throw new ModuleException();       }

ETCD:运行时重新配置设计

原文地址:the runtime configuration design 运行时重新配置是分布式系统中最难,最容易出错的部分,尤其是在基于共识(像etcd)的系统中. 阅读并学习关于etcd的运行时重新配置命令设计和如何追溯这些错误. 两阶段配置更新保证集群安全 在etcd中,每一次运行时重新配置安全的原因是由于两阶段更新.例如,添加一个成员,首先将新配置通知集群后启动新的成员. 阶段一 通知集群关于新的配置 添加一个成员到etcd集群中,通过API调用请求将一个新成员添加到集群中.这是将新的

运行时修改TimerTask的执行周期

java.util.TimerTask类的执行周期period变量的声明如下: /** * Period in milliseconds for repeating tasks. A positive value indicates * fixed-rate execution. A negative value indicates fixed-delay execution. * A value of 0 indicates a non-repeating task. */ long peri

聊聊高并发(四)Java对象的表示模型和运行时内存表示

在继续了解Java内存模型之前,最好先理解Java对象的内存表示.在网上搜了下Java对象内存表示,说得都不够系统和到位.之前看了<Hotspot实战>一书,对JVM如何表示对象这块说得挺好,推荐一下.如果不理解JVM运行时的各种内存区域以及Java调用的过程,那么很难把Java内存模型理解到位.这个是一个比较大的主题,以后会陆续写一些JVM相关的.这里单把Java对象的内存拿出来聊聊,文中内容都基于Hotspot虚拟机. Hotspot主要是用C++写的,所以它定义的Java对象表示模型也是