RocketMQ源码 NameSrv-KVConfigManager组件 巧用读写锁维护内存KV数据结构

前言

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配置存储指定的命名空间。

  1. 获取写锁,直到被其他线程中断;
  2. 从configTable获取当前命名空间下的KV配置,如果不存在命名空间,则创建一个;
  3. 将新的KV写入KV配置;
  4. 释放写锁;
  5. 将内存数据持久化到磁盘;

    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持久化到磁盘文件。

  1. 获取读锁,直到被其他线程中断;
  2. 将内存中的KV配置序列化;
  3. 将序列化后的字符串写入磁盘文件;

注意:

此处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。

  1. 获取写锁,直到被其他线程中断;
  2. 获取指定命名空间下的KV配置;
  3. 删除指定key;
  4. 释放写锁;
  5. 将内存数据持久化到磁盘;

    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配置。

  1. 获取写锁,直到被其他线程中断;
  2. 获取指定命名空间下的KV配置;
  3. 将KV配置封装到TVTable(实现了RemotingSerializable通信协议序列化类)数据结构,且进行编码序列化;
  4. 返回字节数组,且释放读锁;

    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。

  1. 获取读锁,直到被其他线程中断;
  2. 获取指定命名空间下的KV配置;
  3. 获取KV配置中制定key的value;
  4. 释放读锁;

    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数据结构。

  1. 获取读锁,直到被其他线程中断;
  2. 利用迭代器遍历configTable下命令空间配置;
  3. 利用子迭代器遍历 指定命令空间下的KV配置;
  4. 释放读锁;
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值