前言
上一篇文章中,我写了一个简单的配置管理类,实现了COW类似的功能,每次获取的配置基于最新版本,底层使用不可变Map保存当前配置,每次写操作都需要复制Map,其时间复杂度为o(n),如果将底层实现改成可持久化数据结构,比如PMap(底层使用HMAT)不仅保留了不可变、线程安全等能力,还能获得等效常量时间复杂度。
此外,我给的例子使用了Map<String, String>,实际上,配置通常需要解析成不同的类型,本文也给出一个实现,使用到了Caffeine 缓存,你可以理解成使用了固定大小的ConcurrentLinkedHashMap,通过大小限制实现驱逐,避免内存泄露。这里主要考虑了系统中多个版本共存,导致配置存在多版本。
实现细节
新实现类如下,后文有详细分析
class ConfigManager {
private volatile PMap<String, String> config = HashPMap.empty(IntTreePMap.empty());
private final ReentrantLock updateLock = new ReentrantLock();
/**
* 全量更新配置(原子操作)
* @param newConfig 新的配置映射
*/
public void updateAll(Map<String, String> newConfig) {
updateLock.lock();
try {
// 创建新的不可变配置快照
config = config.plusAll(newConfig);
} finally {
updateLock.unlock();
}
}
/**
* 增量更新单个配置项
*/
public void update(String key, String value) {
updateLock.lock();
try {
config = config.plus(key, value);
} finally {
updateLock.unlock();
}
}
/**
* 获取当前配置值(无锁读取)
* @param key 配置键
* @return 配置值(不存在时返回 null)
*/
public String get(String key) {
return config.get(key);
}
/**
* 获取配置快照(线程安全的不可变视图)
* @return 当前配置的不可变副本
*/
public PMap<String, String> getSnapshot() {
return config; // 直接返回不可变对象引用
}
record ConfigKey(String key, String value) {}
final Cache<ConfigKey, Object> configCache = Caffeine.newBuilder().maximumSize(1000).build();
@SuppressWarnings("unchecked")
public <T> T getConfig(String key, Function<String, T> parser) {
String value = config.get(key);
if (value == null) {
return null;
}
return (T) configCache.get(new ConfigKey(key, value), k -> parser.apply(value));
}
}
1. 线程安全和并发控制
FeatureFlagManager 使用 ReentrantLock 来确保配置映射的线程安全更新。此锁对于在多个线程尝试同时更新配置时保持数据一致性至关重要。
private final ReentrantLock updateLock = new ReentrantLock();
全量配置更新
updateAll 方法允许对整个配置映射进行原子更新。通过锁定更新过程,确保在更新操作期间没有其他线程干扰。
public void updateAll(Map<String, String> newConfig) {
updateLock.lock();
try {
config = config.plusAll(newConfig);
} finally {
updateLock.unlock();
}
}
增量配置更新
对于更新单个配置项,使用 update 方法。它遵循相同的锁定机制以确保原子性。
public void update(String key, String value) {
updateLock.lock();
try {
config = config.plus(key, value);
} finally {
updateLock.unlock();
}
}
2. 使用持久化数据结构实现不可变性
配置存储在一个持久化映射 (PMap) 中,该映射提供配置的不可变快照。这种方法确保任何读取操作都是线程安全的,无需锁定。PMap实现了Map接口,同时提供了函数式”修改“方法(对象不可变的,返回结果为新对象)。
private volatile PMap<String, String> config = HashPMap.empty(IntTreePMap.empty());
3. 高效的配置检索
get 方法提供了无锁的方式来检索配置值,利用了 PMap 的不可变性。
public String get(String key) {
return config.get(key);
}
实现配置解析功能
为了提高性能,ConfigManager 使用 Caffeine 缓存来存储解析后的配置值。此缓存减少了重复解析配置值的开销。
final Cache<ConfigKey, Object> configCache = Caffeine.newBuilder().maximumSize(1000).build();
Cache 中的key选择 record ConfigKey(String key, String value) {}, 尽量不要使用 String 类型,因为组合String(比如使用格式为"value")每次查询都需要创建临时String并计算hashcode,性能开销大一些,同时可读性比较差;使用 record 可以避免以上问题。
getConfig 方法检索并解析配置值,将结果缓存以供将来访问。这里的 get 等效于 ComputeIfAbsent。
public <T> T getConfig(String key, Function<String, T> parser) {
String value = config.get(key);
if (value == null) {
return null;
}
return (T) configCache.get(new ConfigKey(key, value), k -> parser.apply(value));
}
结论与总结
- 通过结合持久化数据结构和COW思想,可以实现动态配置管理,实现配置高效且安全地更新和访问。
- 通过缓存可以避免重复计算,提高系统性能。
- 使用缓存需要注意内存泄露的风险和缓存驱逐的策略。你也可以使用不同的驱逐策略。
- 配置版本管理常常涉及 feature toggle、AB测试、权限、灰度等内容,需要考虑到配置具有不同的时效性和可变性。
- 配置版本管理利于排查问题,特别是涉及微服务场景时,可以通过版本号关联trxId,方便排查问题。本文中可以改视图为
record ConfigView(long configId, PMap<String, String> config) {}实现。 - 很多实现没有考虑请求中配置的不变性,导致业务可能出现异常并且难以排查。使用本文实现结合TTL可以实现请求级配置管理,避免业务异常。