55

简单配置中心组件实现参考

 5 years ago
source link: http://www.cnblogs.com/yougewe/p/10459972.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

随着线上环境的复杂多变,以及业务需求动荡,我们有足够的理由需要一个配置中心来处理配置的变更问题!

但对于项目初期,往往只需要能够做到数据支持动态配置,就能够满足需求了。

本文给出一个配置组件的实现方案,希望对有这方面需求的同学有点参考!

(本实例虽然只是从数据库取值,但是其实稍微做下扩展,就可以是一个完整的配置中心了,比如将从数据库更新缓存改为使用ZK的订阅功能进行缓存更新,即可随时接受后台传过来的配置变更了)

核心实现类:

/**
 * 简单数据库 k->v 字典表配置缓存工具类。
 *  作用有二: 
 *     1. 将配置放到数据库,方便变更;
 *     2. 配置查询可能很频繁, 将db数据缓存放到本地内存, 以减少数据库压力;
 *
 */
@Component
@Slf4j
public class ConfigDictManager {

    /**
     * 配置变量映射表
     */
    private final Map<String, ConfigNidValueStrictMapBean> configMappings = new HashMap<>();

    // 缓存失效时间, 可以尽量做成配置可变的
    private Long cacheTimeout = 100;

    // 从数据库取配置值的 mapper
    @Resource
    private DictConfigMapper dictConfigMapper;

    
    /**
     * 获取模块配置下的value, 未配置则返回null
     *
     * @param module 模块名
     * @param configName 配置名
     * @return 配置值, 没有配置时返回 null
     */
    public String getConfigValue(String module, String configName) {
        return getConfigValueOrDefault(module, configName, null);
    }

    
    /**
     * 获取配置值带默认值的(如果没查到配置)
     *
     * @param module 模块名
     * @param configName 配置key
     * @param defaultIfNull 默认值
     * @return 配置值或者默认值
     */
    public String getConfigValueOrDefault(String module, String configName, String defaultIfNull) {
        if(module == null || configName == null) {
            throw new RuntimeException("配置变量名不能为空!");
        }
        ConfigNidValueStrictMapBean moduleConfigs = getCachedModuleConfig(module);
        if(isConfigCacheExpired(moduleConfigs)) {
            // 首次初始化,必须同步等待,否则将出现前面几次取配置值为空情况
            if(!isModuleConfigInitialized(moduleConfigs)) {
                blockingUpdateConfigNidModuleCache(moduleConfigs);
            }
            // 不是首次更新,可以使用旧值,因对配置生效实时性不高业务需求决定
            else {
                noneBlockingUpdateConfigNidModuleCache(moduleConfigs);
            }
        }
        String value = moduleConfigs.getNameValuePairs()
                            .getOrDefault(configName, defaultIfNull);
        log.debug("【配置中心】获取配置变量: {}->{} 值为: {}, default:{}"
                    , module, configName, value, defaultIfNull);
        return value;
    }

    /**
     * 阻塞更新模块配置信息,用于初始化配置时使用
     *
     * @param moduleConfigs 配置原始值
     */
    private void blockingUpdateConfigNidModuleCache(ConfigNidValueStrictMapBean moduleConfigs) {
        synchronized (moduleConfigs) {
            if(!isModuleConfigInitialized(moduleConfigs)) {
                if(!moduleConfigs.getIsUpdating().compareAndSet(false, true)) {
                    log.warn("【配置中心】并发配置更新异常,请确认1!");
                }
                updateConfigNidModuleCacheFromDatabase(moduleConfigs);
            }
        }
    }

    /**
     * 非阻塞更新模块配置信息,用于非初始化时的并发操作
     *
     * @param moduleConfigs 配置原始值
     */
    private void noneBlockingUpdateConfigNidModuleCache(ConfigNidValueStrictMapBean moduleConfigs) {
        if((moduleConfigs.getIsUpdating().compareAndSet(false, true))) {
            updateConfigNidModuleCacheFromDatabase(moduleConfigs);
        }
    }

    /**
     * 判断是否模块数据已初始化
     *
     * @param moduleConfigs 模块外部配置
     * @return true|false
     */
    private boolean isModuleConfigInitialized(ConfigNidValueStrictMapBean moduleConfigs) {
        return moduleConfigs.getNameValuePairs() != null;
    }

    /**
     * 获取模块配置缓存,如果没有值,则先默认初始化一个key
     *
     * @param module 模块名
     * @return 模块配置
     */
    private ConfigNidValueStrictMapBean getCachedModuleConfig(String module) {
        ConfigNidValueStrictMapBean moduleConfig = configMappings.get(getModuleCacheKey(module));
        if(moduleConfig == null) {
            synchronized (configMappings) {
                if((moduleConfig = configMappings.get(getModuleCacheKey(module))) == null) {
                    String profile = SpringContextsUtil.getActiveProfile();
                    moduleConfig = new ConfigNidValueStrictMapBean();
                    moduleConfig.setModuleName(module);
                    moduleConfig.setEnvironmentProfile(profile);
                    moduleConfig.setUpdateTime(0L);         // 初始为0,必更新
                    configMappings.put(getModuleCacheKey(module), moduleConfig);
                }
            }
        }
        return moduleConfig;
    }

    /**
     * 更新nid对应的模块缓存
     *
     * @param moduleConfigs 原始缓存配置,更新后返回
     */
    private void updateConfigNidModuleCacheFromDatabase(ConfigNidValueStrictMapBean moduleConfigs) {
        String profile = SpringContextsUtil.getActiveProfile();
        String module = moduleConfigs.getModuleName();
        DictConfigEntity cond = new DictConfigEntity();
        cond.setEnv(profile);
        cond.setModule(module);
        List<DictConfigEntity> resultList= dictConfigMapper.selectByCond(cond);
        Map<String, String> nidKeyValuePairs = new HashMap<>();
        if(resultList != null && resultList.size() > 0) {
            resultList.forEach(c -> {
                nidKeyValuePairs.put(c.getVarName(), c.getVarValue());
            });
            moduleConfigs.setNameValuePairs(nidKeyValuePairs);
            moduleConfigs.setUpdateTime(System.currentTimeMillis());
            if(!moduleConfigs.getIsUpdating().compareAndSet(true, false)) {
                log.warn("【配置中心】并发更新配置缓存异常,请注意!");
            }
        }
        else {
            log.warn("【配置中心】系统变量没有配置,{}->{}->{},请确认配置!", profile, module);
        }
        moduleConfigs.setNameValuePairs(nidKeyValuePairs);
    }

    // 获取缓存模块时使用的缓存key, 默认可直接使用 模块名即可
    private String getModuleCacheKey(String module) {
        return module;
    }

    /**
     * 检测配置缓存是否过期
     *
     * @param moduleConfigs 模块的缓存
     * @return true|false
     */
    private boolean isConfigCacheExpired(ConfigNidValueStrictMapBean moduleConfigs) {
        return (System.currentTimeMillis() - cacheTimeout * 1000
                    > moduleConfigs.getUpdateTime());
    }
}

以上配置动态化实现,主要思路有几点:

1. 最终数据来源为db,可靠性高;

2. 查询db后,将数据缓存一段时间放置在本地内存中,使后续访问更快,高性能;

3. 使用双重锁检查(double-check), 避免产生多个不同缓存配置, 可以认为是个单例访问;

4. 使用 synchronized 和 volatile 保证了内存可见性, 使一个线程更新缓存后,其他线程可以立即使用;

5. 考虑到缓存的时效性要求不高, 在有一个线程在更新缓存时,其他线程仍然可以继续使用旧缓存, 直到更新线程操作完成;

6. 使用 AtomicBoolean 来做一个更新标志, 保证线程安全的同时, 也避免了使用锁;

以上实现,还差几个数据结构细节。如: 配置类数据结构; 数据表的数据结构;

我们来看下:

1. 配置类的数据结构 ConfigNidValueStrictMapBean:

@Data
public class ConfigNidValueStrictMapBean {

    /**
     * 更新标识设置为 final, 只允许更新值, 不允许外面变更实例对象
     */
    private final AtomicBoolean isUpdating = new AtomicBoolean(false);

    /**
     * 更新时间戳
     */
    private Long updateTime;

    /**
     * 配置模块名
     */
    private String moduleName;

    /**
     * 环境变量, prod, test, dev...
     */
    private String environmentProfile;

    /**
     * 配置key对应的值字典, 使用 volatile, 保证内存可见性
     */
    private volatile Map<String, String> nameValuePairs;

    public AtomicBoolean getIsUpdating() {
        return isUpdating;
    }

}

数据库配置表数据结构如下:

CREATE TABLE `t_dict_config` (
  `id` int(11) NOT NULL AUTO_INCREMENT '主键id',
  `env` varchar(20) NOT NULL DEFAULT 'test' COMMENT '运行环境 dev,test,prod',
  `module` varchar(50) NOT NULL COMMENT '模块名称(分组)',
  `config_name` varchar(50) DEFAULT NULL COMMENT '配置key',
  `config_value` varchar(500) DEFAULT '' COMMENT '配置值',
  `remark` varchar(100) DEFAULT NULL COMMENT '配置说明',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `module` (`module`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='配置字典表';

万事具备,可以开工了!

存在的问题: 1. 在集群环境中,每个机器都对应的缓存副本,可能导致数据不一致; 2. 机器重启后缓存全部消失; 3. 在n台机器进来缓存初始化时,数据存在一定压力;

另外,对于配置值的维护,除了使用户线程更新外,我们还可用使用一个后台线程。该线程会一直定时刷新缓存,从而完全避免并发问题!但是这个线程能做的,可能就只是全量更新数据了!

不管怎么样,要实现一个配置化的功能, 看起来很简单, 实际也很简单嘛。如果要做后台实时更新,只需要做两个 推、拉 功能即可!

唯一要注意的就是: 做到既快又准还要安全!(操作不当将可能导致HashMap死循环哦)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK