前言
RocketMQ在启动 Namesrv的过程中会初始化 NamesrvController核心组件,NamesrvController初始化过程中 又引用了KVConfigManager配置管理组件。
在KVConfigManager配置管理组件中,使用到JDK并发包中的 ReadWriteLock读写锁,实现对内存数据结构configTable的并发控制,下面就分析一下具体实现逻辑。
源码实现
public class KVConfigManager {
private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
private final NamesrvController namesrvController;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final HashMap<String/* Namespace */, HashMap<String/* Key */, String/* Value */>> configTable =
new HashMap<String, HashMap<String, String>>();
public KVConfigManager(NamesrvController namesrvController) {
this.namesrvController = namesrvController;
}
}
上方是KVConfigManager组件的实例变量,可以看到初始化了一个 ReadWriteLock读写锁,用于进行读写并发控制。
并且维护了一个 configTable的HashMap数据结构用于存储KV配置, K是Namespace命名空间,V是一个子 HashMap数据结构,用户存储每个Namespace命名空间下的子KV配置。
public void load() {
String content = null;
try {
content = MixAll.file2String(this.namesrvController.getNamesrvConfig().getKvConfigPath());
} catch (IOException e) {
log.warn("Load KV config table exception", e);
}
if (content != null) {
KVConfigSerializeWrapper kvConfigSerializeWrapper =
KVConfigSerializeWrapper.fromJson(content, KVConfigSerializeWrapper.class);
if (null != kvConfigSerializeWrapper) {
this.configTable.putAll(kvConfigSerializeWrapper.getConfigTable());
log.info("load KV config table OK");
}
}
}
接着来看load初始化方法,Namesrv启动时,先从磁盘文件加载历史数据,然后反序列化为KV配置对象,存入内存configTable。
public void putKVConfig(final String namespace, final String key, final String value) {
try {
this.lock.writeLock().lockInterruptibly();
try {
HashMap<String, String> kvTable = this.configTable.get(namespace);
if (null == kvTable) {
kvTable = new HashMap<String, String>();
this.configTable.put(namespace, kvTable);
log.info("putKVConfig create new Namespace {}", namespace);
}
final String prev = kvTable.put(key, value);
if (null != prev) {
log.info("putKVConfig update config item, Namespace: {} Key: {} Value: {}",
namespace, key, value);
} else {
log.info("putKVConfig create new config item, Namespace: {} Key: {} Value: {}",
namespace, key, value);
}
} finally {
this.lock.writeLock().unlock();
}
} catch (InterruptedException e) {
log.error("putKVConfig InterruptedException", e);
}
this.persist();
}
接着来看putKVConfig方法,它负责将KV配置存储指定的命名空间。
- 获取写锁,直到被其他线程中断;
- 从configTable获取当前命名空间下的KV配置,如果不存在命名空间,则创建一个;
- 将新的KV写入KV配置;
- 释放写锁;
- 将内存数据持久化到磁盘;
public void persist() {
try {
this.lock.readLock().lockInterruptibly();
try {
KVConfigSerializeWrapper kvConfigSerializeWrapper = new KVConfigSerializeWrapper();
kvConfigSerializeWrapper.setConfigTable(this.configTable);
String content = kvConfigSerializeWrapper.toJson();
if (null != content) {
MixAll.string2File(content, this.namesrvController.getNamesrvConfig().getKvConfigPath());
}
} catch (IOException e) {
log.error("persist kvconfig Exception, "
+ this.namesrvController.getNamesrvConfig().getKvConfigPath(), e);
} finally {
this.lock.readLock().unlock();
}
} catch (InterruptedException e) {
log.error("persist InterruptedException", e);
}
}
接着来看persist方法,它负责将内存中的configTable持久化到磁盘文件。
- 获取读锁,直到被其他线程中断;
- 将内存中的KV配置序列化;
- 将序列化后的字符串写入磁盘文件;
注意:
此处putKVConfig + persist的过程中,虽然通过读写锁,对锁粒度做了拆分,保证了putKVConfig获取写独占锁,写入数据的安全性,但是在persist过程中并不保证,上一步写完之后能立即获取到读锁,将文件写磁盘。可能存在多个put操作完之后,多个线程一起获取到写锁,多线程的将文件写入磁盘。
优点:并发性能高,写内存安全,且刷磁盘的过程中,不影响读内存操作。
缺点:刷磁盘非并发安全操作,存在重复写。但是因为使用的数据结构是HashMap接口,重复的Key对使用并不影响,所以整体没啥问题。
public void deleteKVConfig(final String namespace, final String key) {
try {
this.lock.writeLock().lockInterruptibly();
try {
HashMap<String, String> kvTable = this.configTable.get(namespace);
if (null != kvTable) {
String value = kvTable.remove(key);
log.info("deleteKVConfig delete a config item, Namespace: {} Key: {} Value: {}",
namespace, key, value);
}
} finally {
this.lock.writeLock().unlock();
}
} catch (InterruptedException e) {
log.error("deleteKVConfig InterruptedException", e);
}
this.persist();
}
接着来看deleteKVConfig方法,他负责删除指定命名空间下的key。
- 获取写锁,直到被其他线程中断;
- 获取指定命名空间下的KV配置;
- 删除指定key;
- 释放写锁;
- 将内存数据持久化到磁盘;
public byte[] getKVListByNamespace(final String namespace) {
try {
this.lock.readLock().lockInterruptibly();
try {
HashMap<String, String> kvTable = this.configTable.get(namespace);
if (null != kvTable) {
KVTable table = new KVTable();
table.setTable(kvTable);
return table.encode();
}
} finally {
this.lock.readLock().unlock();
}
} catch (InterruptedException e) {
log.error("getKVListByNamespace InterruptedException", e);
}
return null;
}
接着来看getKVListByNamespace方法,它负责获取指定命名空间下的KV配置。
- 获取写锁,直到被其他线程中断;
- 获取指定命名空间下的KV配置;
- 将KV配置封装到TVTable(实现了RemotingSerializable通信协议序列化类)数据结构,且进行编码序列化;
- 返回字节数组,且释放读锁;
public String getKVConfig(final String namespace, final String key) {
try {
this.lock.readLock().lockInterruptibly();
try {
HashMap<String, String> kvTable = this.configTable.get(namespace);
if (null != kvTable) {
return kvTable.get(key);
}
} finally {
this.lock.readLock().unlock();
}
} catch (InterruptedException e) {
log.error("getKVConfig InterruptedException", e);
}
return null;
}
接着看getKVConfig方法,它负责获取指定命名空间下指定key的value。
- 获取读锁,直到被其他线程中断;
- 获取指定命名空间下的KV配置;
- 获取KV配置中制定key的value;
- 释放读锁;
public void printAllPeriodically() {
try {
this.lock.readLock().lockInterruptibly();
try {
log.info("--------------------------------------------------------");
{
log.info("configTable SIZE: {}", this.configTable.size());
Iterator<Entry<String, HashMap<String, String>>> it =
this.configTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, HashMap<String, String>> next = it.next();
Iterator<Entry<String, String>> itSub = next.getValue().entrySet().iterator();
while (itSub.hasNext()) {
Entry<String, String> nextSub = itSub.next();
log.info("configTable NS: {} Key: {} Value: {}", next.getKey(), nextSub.getKey(),
nextSub.getValue());
}
}
}
} finally {
this.lock.readLock().unlock();
}
} catch (InterruptedException e) {
log.error("printAllPeriodically InterruptedException", e);
}
}
接着看最后一个方法printAllPeriodically,它负责打印内存KV数据结构。
- 获取读锁,直到被其他线程中断;
- 利用迭代器遍历configTable下命令空间配置;
- 利用子迭代器遍历 指定命令空间下的KV配置;
- 释放读锁;