随着线上环境的复杂多变,以及业务需求动荡,我们有足够的理由需要一个配置中心来处理配置的变更问题!
但对于项目初期,往往只需要能够做到数据支持动态配置,就能够满足需求了。
本文给出一个配置组件的实现方案,希望对有这方面需求的同学有点参考!
(本实例虽然只是从数据库取值,但是其实稍微做下扩展,就可以是一个完整的配置中心了,比如将从数据库更新缓存改为使用ZK的订阅功能进行缓存更新,即可随时接受后台传过来的配置变更了)
核心实现类:
/**
* 简单数据库 k->v 字典表配置缓存工具类。
* 作用有二:
* 1. 将配置放到数据库,方便变更;
* 2. 配置查询可能很频繁, 将db数据缓存放到本地内存, 以减少数据库压力;
*
*/
@Component
@Slf4j
public class ConfigDictManager {
/**
* 配置变量映射表,能使用 hashmap 的原因是,代码逻辑上保证了只有单个线程更新该map
*/
private final Map<String, ConfigNidValueStrictMapBean> configMappings = new HashMap<>();
@Value("${server.config.nid.cache.timeout}")
private Long nidCacheTimeout;
@Resource
private SystemConfigMapper systemConfigMapper;
@Override
public String getConfigValue(String module, String configName) {
return getConfigValueOrDefault(module, configName, null);
}
@Override
public String getConfigValueOrDefault(String module, String configName, String defaultIfNull) {
if(module == null || configName == null) {
throw new CashException(Constants.System.PARAMS_INVALID, Constants.System.SYSTEM_CONFIG_PARAM_ERROR_MSG);
}
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(!setupConfigNidModuleUpdatingLock(moduleConfigs)) {
log.warn("【配置中心】配置更新异常,请确认1!");
}
updateConfigNidModuleCacheFromDatabase(moduleConfigs);
}
}
}
/**
* 非阻塞更新模块配置信息,用于非初始化时的并发操作
*
* @param moduleConfigs 配置原始值
*/
private void noneBlockingUpdateConfigNidModuleCache(ConfigNidValueStrictMapBean moduleConfigs) {
if(setupConfigNidModuleUpdatingLock(moduleConfigs)) {
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();
SystemConfig record = new SystemConfig();
record.setEnvironment(profile);
record.setModule(module);
// record.setVarName(configName);
List<SystemConfig> resultList= systemConfigMapper.selectChannelVal(record);
Map<String, String> nidKeyValuePairs = new HashMap<>();
if(resultList != null && resultList.size() > 0) {
resultList.forEach(c -> {
nidKeyValuePairs.put(c.getVarName(), c.getVarValue());
});
}
else {
log.warn("【配置中心】系统变量没有配置,{}->{}->{},请确认配置!", profile, module);
}
moduleConfigs.setUpdateTime(System.currentTimeMillis());
moduleConfigs.setNameValuePairs(nidKeyValuePairs);
if(!releaseConfigNidModuleUpdatingLock(moduleConfigs)) {
log.warn("【配置中心】更新配置缓存异常,请注意!");
}
}
/**
* 还原配置更新标识
*
* @param moduleConfigs 配置信息
* @return true: 释放成功, false: 失败,已被未知线程更新
*/
private boolean releaseConfigNidModuleUpdatingLock(ConfigNidValueStrictMapBean moduleConfigs) {
return moduleConfigs.getIsUpdating().compareAndSet(true, false);
}
/**
* 获取配置更新锁(要求:原本无人更新,可以置为更新状态)
*
* @param moduleConfigs 配置信息
* @return true: 获取成功, false: 失败
*/
private boolean setupConfigNidModuleUpdatingLock(ConfigNidValueStrictMapBean moduleConfigs) {
return moduleConfigs.getIsUpdating().compareAndSet(false, true);
}
// 获取缓存模块时使用的缓存key
private String getModuleCacheKey(String module) {
return module;
}
/**
* 检测配置缓存是否过期
*
* @param moduleConfigs 模块的缓存
* @return true|false
*/
private boolean isConfigCacheExpired(ConfigNidValueStrictMapBean moduleConfigs) {
return (System.currentTimeMillis() - nidCacheTimeout * 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台机器进来缓存初始化时,数据存在一定压力;
另外,对于配置值的维护,除了使用户线程更新外,我们还可以:
1、 用使用一个后台线程。该线程会一直定时刷新缓存,从而完全避免并发问题!但是这个线程能做的,可能就只是全量更新数据了!
2、 使用 ReentrantReadWriteLock 读写锁来实现,读缓存时任意读,更新缓存时则阻塞!
不管怎么样,要实现一个配置化的功能, 看起来很简单, 实际也很简单嘛。如果要做后台实时更新,只需要做两个 推、拉 功能即可!
唯一要注意的就是: 做到既快又准还要安全!(操作不当将可能导致HashMap死循环哦)