继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

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

HUX布斯
关注TA
已关注
手记 171
粉丝 83
获赞 377

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

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

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

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

核心实现类:

复制代码

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

复制代码

@Datapublic 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台机器进来缓存初始化时,数据存在一定压力;

 

另外,对于配置值的维护,除了使用户线程更新外,我们还可以:

  1. 用使用一个后台线程。该线程会一直定时刷新缓存,从而完全避免并发问题!但是这个线程能做的,可能就只是全量更新数据了!

  2. 使用 ReentrantReadWriteLock 读写锁来实现,读缓存时任意读,更新缓存时则阻塞!

 

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

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

作者:等你归去来

原文出处:https://www.cnblogs.com/yougewe/p/10459972.html  

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP