簡單配置中心元件實現參考
隨著線上環境的複雜多變,以及業務需求動盪,我們有足夠的理由需要一個配置中心來處理配置的變更問題!
但對於專案初期,往往只需要能夠做到資料支援動態配置,就能夠滿足需求了。
本文給出一個配置元件的實現方案,希望對有這方面需求的同學有點參考!
(本例項雖然只是從資料庫取值,但是其實稍微做下擴充套件,就可以是一個完整的配置中心了,比如將從資料庫更新快取改為使用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死迴圈哦)