DBLE 2.17.08.1与MyCat 1.6.5的启动过程(5)——加载配置文件schema.xml

schema.xml的加载过程概况


com.actiontech.dble.config.loader.xml.XMLSchemaLoaderload()方法是schema.xml的加载入口。这个加载入口仅仅把schema.xml,以带DTD校验的方式加载成到内存中,形成w3c的DOM。然后依次调用loadDataHosts()loadDataNodes()loadSchemas()这三个方法,处理schema.xml中的三种一级标签,加载成com.actiontech.dble.config.model命名空间中对应的类的对象。

方法名 处理的标签 目标类
loadDataHosts() DataHostConfig
loadDataNodes() DataNodeConfig
loadSchemas() SchemaConfig

处理标签——loadDataHosts()


loadDataHosts()的使命是将标签加载成DataHostConfig对象。在逻辑结构上,标签和DataHostConfig对象完全相同。

属性 DataHostConfig属性
name String name
maxCon int maxCon
minCon int minCon
balance int balance
switchType int switchType
slaveThreshold int slaveThreshold
tempReadHostAvailable boolean tempReadHostAvailable
String heartbeatSQL
DBHostConfig[] writeHosts
Map readHosts

可以看到,相对于name这些属性,这两个property(XML概念)的处理是重点,其中需要着重解决下面两个问题:

  1. 应该加载成什么?

除了可以包含一个或多个外,在属性层面,两者基本没有差异。因此统一用com.actiontech.dble.config.model.DBHostConfig这个类来标识它们。一个DBHostConfig对象表示一个标签。

  1. 的一对多包含关系应该如何保存下来?

每个标签在DataHostConfig.writeHosts这个数组中存储,也因此会有它在数组中的序号,所以DataHostConfig.readHosts这个Map集合利用了这一点,设计成了“<在writeHosts中的序号, 这个拥有的数组>”这么一种形式来存储(虽然我觉得用存储密度更高的二维数组可能会更好)。

了解了以上概况之后,它的工作流程就很明晰了,大致是:

  1. 从schema.xml中逐个找出标签

  2. 加载的简单属性到临时变量中:name, maxCon, minCon, balance, switchType, slaveThreshold, tempReadHostAvailable和

  3. 加载的复杂属性到临时变量中

  4. 根据临时变量生成DataHostConfig对象,并登记到XMLSchemaLoader中(加入到内部Map集合中)

private void loadDataHosts(Element root) {

    // 提取schema.xml中所有的标签
    NodeList list = root.getElementsByTagName("dataHost");

    // 逐个标签进行处理
    for (int i = 0, n = list.getLength(); i < n; ++i) {
        Element element = (Element) list.item(i);

        // 加载简单属性到临时变量中:name, maxCon, minCon, balance, switchType, slaveThreshold, tempReadHostAvailable和
        String name = element.getAttribute("name");
        if (dataHosts.containsKey(name)) {
            throw new ConfigException("dataHost name " + name + "duplicated!");
        }
        int maxCon = Integer.parseInt(element.getAttribute("maxCon"));
        int minCon = Integer.parseInt(element.getAttribute("minCon"));
        final int balance = Integer.parseInt(element.getAttribute("balance"));
        String switchTypeStr = element.getAttribute("switchType");
        int switchType = switchTypeStr.equals("") ? -1 : Integer.parseInt(switchTypeStr);
        String slaveThresholdStr = element.getAttribute("slaveThreshold");
        int slaveThreshold = slaveThresholdStr.equals("") ? -1 : Integer.parseInt(slaveThresholdStr);
        String tempReadHostAvailableStr = element.getAttribute("tempReadHostAvailable");
        boolean tempReadHostAvailable = !tempReadHostAvailableStr.equals("") && Integer.parseInt(tempReadHostAvailableStr) > 0;

        final String heartbeatSQL = element.getElementsByTagName("heartbeat").item(0).getTextContent();

        // 获取当前下所有的
        NodeList writeNodes = element.getElementsByTagName("writeHost");

        // 初始化当前中,的临时变量(数组及集合)
        DBHostConfig[] writeDbConfs = new DBHostConfig[writeNodes.getLength()];
        Map readHostsMap = new HashMap<>(2);

        // 逐个进行处理
        for (int w = 0; w < writeDbConfs.length; w++) {
            Element writeNode = (Element) writeNodes.item(w);

            // 创建对应的DBHostConfig对象,并加入到临时变量的数组中
            writeDbConfs[w] = createDBHostConf(name, writeNode, maxCon, minCon);

            // 获取当前下所有的
            NodeList readNodes = writeNode.getElementsByTagName("readHost");

            if (readNodes.getLength() != 0) {

              // 创建存放当前下所有的临时数组
                DBHostConfig[] readDbConfs = new DBHostConfig[readNodes.getLength()];

                // 逐个进行处理
                for (int r = 0; r < readDbConfs.length; r++) {
                    Element readNode = (Element) readNodes.item(r);

                    // 创建对应的DBHostConfig对象,并加入到临时变量的数组中
                    readDbConfs[r] = createDBHostConf(name, readNode, maxCon, minCon);
                }

                // 将准备好的临时数组注册到临时变量(Map集合)中
                readHostsMap.put(w, readDbConfs);
            }
        }

        // 根据各属性的临时变量创建DataHostConfig
        DataHostConfig hostConf = new DataHostConfig(name,
                writeDbConfs, readHostsMap, switchType, slaveThreshold, tempReadHostAvailable);
        hostConf.setMaxCon(maxCon);
        hostConf.setMinCon(minCon);
        hostConf.setBalance(balance);
        hostConf.setHearbeatSQL(heartbeatSQL);

        // 将当前对应的DataHostConfig注册到XMLSchemaLoader的内部清单中
        dataHosts.put(hostConf.getName(), hostConf);
    }

}

当中,createDBHostConf()这个函数在DBLE和MyCat中,基本功能是一致的:

  • 获取的属性:host、url、user、password、usingDecrypt和weight
  • 检查host、url和user都不为空
  • 从host属性中分离出ip和port
  • 对password属性的内容进行RSA的解密(usingDecrypt属性为1时)

但是,由于设计思路的分歧,DBLE裁剪了MyCat中的以下功能:

  • 非MySQL数据库的支持(通过的dbType属性提供)
  • 其他JDBC Driver的支持(通过的dbDriver属性提供)
  • 无用属性filters和logTime
private DBHostConfig createDBHostConf(String dataHost, Element node, int maxCon, int minCon) {

    // 加载必须属性host、url和user
    String nodeHost = node.getAttribute("host");
    String nodeUrl = node.getAttribute("url");
    String user = node.getAttribute("user");

    String ip = null;
    int port = 0;

    // 检查必须属性是否都不为空
    if (empty(nodeHost) || empty(nodeUrl) || empty(user)) {
        throw new ConfigException(
                "dataHost " + dataHost +
                        " define error,some attributes of this element is empty: " +
                        nodeHost);
    }

    // 从host属性中分离出ip和port
    int colonIndex = nodeUrl.indexOf(':');
    ip = nodeUrl.substring(0, colonIndex).trim();
    port = Integer.parseInt(nodeUrl.substring(colonIndex + 1).trim());

    // 加载password和usingDecrypt属性
    String password = node.getAttribute("password");
    String usingDecrypt = node.getAttribute("usingDecrypt");

    // 对password属性的内容进行RSA的解密
    String passwordEncryty = DecryptUtil.dbHostDecrypt(usingDecrypt, nodeHost, user, password);

    // 创建目标DBHostConfig对象并根据各个属性赋值
    DBHostConfig conf = new DBHostConfig(nodeHost, ip, port, nodeUrl, user, passwordEncryty);
    conf.setMaxCon(maxCon);
    conf.setMinCon(minCon);

    // 读取weight属性并对DBHostConfig.weight赋值
    String weightStr = node.getAttribute("weight");
    int weight = "".equals(weightStr) ? PhysicalDBPool.WEIGHT : Integer.parseInt(weightStr);
    conf.setWeight(weight);

    // 返回准备好的DBHostConfig对象
    return conf;
}

处理标签——loadDataNodes()


loadDataHosts()相似,loadDataNodes()的使命是将标签加载成与之逻辑结构相同的Java对象。在这里,加载目标就是DataNodeConfig对象。

属性 DataNodeConfig属性
name String name
database String database
dataHost String dataHost

可以看出,相对于DataHostConfigDataNodeConfig要简单得多。但是,MyCat和DBLE为了让支持一种自制语法,loadDataNodes()的代码变得复杂了许多。

从现有代码来看,这个语法是为了减少schema.xml的编写量(并不会减少DataNodeConfig对象的个数),将多个拥有同样dataHost属性或database属性缩写成一个


















private void loadDataNodes(Element root) {

    // 提取schema.xml中所有的标签
    NodeList list = root.getElementsByTagName("dataNode");

    // 逐个标签进行处理
    for (int i = 0, n = list.getLength(); i < n; i++) {
        Element element = (Element) list.item(i);

        // 加载所有属性到临时变量中:name, dataHost和database
        String dnNamePre = element.getAttribute("name");
        String databaseStr = element.getAttribute("database");
        if (lowerCaseNames) {
            databaseStr = databaseStr.toLowerCase();
        }
        String host = element.getAttribute("dataHost");
        if (empty(dnNamePre) || empty(databaseStr) || empty(host)) {
            throw new ConfigException("dataNode " + dnNamePre + " define error ,attribute can't be empty");
        }

        // 根据用户属性中的输入,判断用户是否使用了缩写语法
        String[] dnNames = SplitUtil.split(dnNamePre, ',', '$', '-');
        String[] databases = SplitUtil.split(databaseStr, ',', '$', '-');
        String[] hostStrings = SplitUtil.split(host, ',', '$', '-');

        if (dnNames.length > 1 && dnNames.length != databases.length * hostStrings.length) {
            throw new ConfigException("dataNode " + dnNamePre +
                    " define error ,dnNames.length must be=databases.length*hostStrings.length");
        }

        if (dnNames.length > 1) {
            // 如果用户使用了缩写语法,
            // 就使用“外层dataHost,内层database”的两层循环去生生成多个DataNodeConfig,
            // 并注册到XMLSchemaLoader的内部清单中
            List mhdList = mergerHostDatabase(hostStrings, databases);
            for (int k = 0; k < dnNames.length; k++) {
                String[] hd = mhdList.get(k);
                String dnName = dnNames[k];
                String databaseName = hd[1];
                String hostName = hd[0];
                createDataNode(dnName, databaseName, hostName);
            }

        } else {
            // 如果用户没有使用缩写语法,
            // 就直接生成一个DataNodeConfig,并注册到XMLSchemaLoader的内部清单中
            createDataNode(dnNamePre, databaseStr, host);
        }

    }
}

loadDataNodes()里用到了两个辅助方法,在这里简单说明一下:

  • mergerHostDatabase()是在确定用户使用了缩写语法之后,求两个字符串属性dataHost × database(dataHost与database的叉乘)
private List mergerHostDatabase(String[] hostStrings, String[] databases) {
    List mhdList = new ArrayList<>();
    for (String hostString : hostStrings) {
        for (String database : databases) {
            String[] hd = new String[2];
            hd[0] = hostString;
            hd[1] = database;
            mhdList.add(hd);
        }
    }
    return mhdList;
}
  • createDataNode()除了创建一个DataNodeConfig对象外,还必须说明它会自动把这个新对象注册到XMLSchemaLoader的内部清单里
private void createDataNode(String dnName, String database, String host) {

    // 创建新的DataNodeConfig对象
    DataNodeConfig conf = new DataNodeConfig(dnName, database, host);

    // 注册到XMLSchemaLoader之前的检查1:名称不能与已有的重复
    if (dataNodes.containsKey(conf.getName())) {
        throw new ConfigException("dataNode " + conf.getName() + " duplicated!");
    }

    // 注册到XMLSchemaLoader之前的检查2:dataHost属性指定的DataHost必须已注册
    if (!dataHosts.containsKey(host)) {
        throw new ConfigException("dataNode " + dnName + " reference dataHost:" + host + " not exists!");
    }

    // 将新的DataNodeConfig对象注册到XMLSchemaLoader的内部清单中
    dataNodes.put(conf.getName(), conf);
}

处理标签——loadSchemas()


loadSchemas()的使命是将标签加载成SchemaConfig对象。在逻辑结构上,标签和SchemaConfig对象有很多共同的地方。

属性 SchemaConfig属性
name String name
dataNode String dataNode
sqlMaxLimit int defaultMaxLimit
Map tables

此外,标签的子标签

由于涉及到了E-R关系这种比较特殊的分片策略,导致这个子标签的处理要分成三部分:

  1. 直接加载一般属性,例如name、primaryKey之类。

  2. 读取用户指定的分片算法(rule属性)和逻辑分片(dataNode属性)的字面值,进行是否存在之类的检查后,与之前的rule.xml和loadDataNodes()的成果关联起来。

  3. 如果含有子标签,那这个

和它的构成了E-R关系,会使用processChildTables()方法来递处理可能存在的多层,并为每个创建一个比较赋值特殊的TableConfig对象——它会被赋予parentTC、joinKey和parentKey属性。

tips:loadTables()执行完后,返回来的是每个

都有自己的TableConfig对象的一个Map哈希表。如果存在E-R关系的表,还需要回到loadSchemas,由它调用XMLSchemaLoader类的其他方法来处理、整合E-R关系引入而产生的关系处理。SchemaConfig使用独立的数据结构ERTable来描述E-R关系。

private void loadSchemas(Element root) {

  // 读取所有的标签
  NodeList list = root.getElementsByTagName("schema");

    // 逐个标签进行处理
  for (int i = 0, n = list.getLength(); i < n; i++) {
    Element schemaElement = (Element) list.item(i);

        // 加载所有属性到临时变量中:name、dataNode和sqlMaxLimit
    String name = schemaElement.getAttribute("name");
    if (lowerCaseNames) {
      name = name.toLowerCase();
    }
    String dataNode = schemaElement.getAttribute("dataNode");
    String sqlMaxLimitStr = schemaElement.getAttribute("sqlMaxLimit");

        // 处理sqlMaxLimit属性:如果用户有配置sqlMaxLimit的话,就使用用户的配置值;如果没有,则设置为-1
    int sqlMaxLimit = -1;
    if (sqlMaxLimitStr != null && !sqlMaxLimitStr.isEmpty()) {
      sqlMaxLimit = Integer.parseInt(sqlMaxLimitStr);
    }

    // 读取dataNode属性,并直接加入到一个List
    if (dataNode != null && !dataNode.isEmpty()) {
      List dataNodeLst = new ArrayList<>(1);
      dataNodeLst.add(dataNode);
            // 调用checkDataNodeExists()来检查用户给这个指定的dataNode是不是都已经已经加载过的
      checkDataNodeExists(dataNodeLst);
    } else {
      dataNode = null;
    }

    // 调用loadTables()方法来加载当前里的所有
标签,每个标签加载成一个TableConfig对象,放到Map中 Map tables = loadTables(schemaElement, lowerCaseNames); if (schemas.containsKey(name)) { throw new ConfigException("schema " + name + " duplicated!"); } // if schema has no default dataNode,it must contains at least one table if (dataNode == null && tables.size() == 0) { throw new ConfigException( "schema " + name + " didn't config tables,so you must set dataNode property!"); } // 生成SchemaConfig对象(tips:当中会涉及buildERMap()方法的调用,用于创建依据父表的分布来分布子表,不关注这种用法,跳过) SchemaConfig schemaConfig = new SchemaConfig(name, dataNode, tables, sqlMaxLimit); // 用mergeFuncNodeERMap()和mergeFkERMap()对新生成的SchemaConfig对象进行优化,(tips:用于ER表,不关注这种用法,跳过) mergeFuncNodeERMap(schemaConfig); mergeFkERMap(schemaConfig); // 将优化后的`SchemaConfig`注册到`XMLSchemaLoader`中 schemas.put(name, schemaConfig); } // 处理完所有``标签后,调用`makeAllErRelations()`(tips:用于ER表,不关注这种用法,跳过) makeAllErRelations(); }

loadTables()会将

标签加载成TableConfig对象。由于当中涉及众多E-R表的处理逻辑,而笔者并不关注,所以先暂时略过,只分析其处理普通
的过程:

  1. 创建TableConfig对象

  2. 读取简单属性name、primaryKey、autoIncrement、needAddLimit、type、rule和ruleRequired

  3. 读取属性dataNode,这个属性可以一次指定多个dataNode,使用“,”、“$”或“-”来分隔

  4. 调用checkDataNodeExists()来检查用户给这个指定的dataNode是不是都已经已经加载过的

  5. 如果当前处理的

有分片函数(非全局表),通过checkRuleSuitTable()来间接调用所有分片算法的基类AbstractPartitionAlgorithm的suitableFor(),检查用户配置的dataNode属性里dataNode个数是否与这个表配置的算法的分片数量一致(tips:这个检查依赖AbstractPartitionAlgorithm.getPartitionNum()提供当前算法需要的分片数量,默认上该函数返回的-1会让suitableFor()跳过检查,所以如果要实现自己的定制分片函数的话,需要自行覆盖该类;suitableFor()函数是final,无法被覆盖)

  • 如果用户在配置

  • 的dataNodes时使用了distribute语法(distribute(xxx,xxx,xxx))的话,调用distributeDataNodes()方法进行排序,保证dataNode编号过程中,尽可能地跨位于不同的物理分片(dataHost)上——首先,一个dataHost创建一个桶(数据结构是列表),把用户的dataNode过一遍,按照它们的dataHost放进对应的桶里;然后,按顺序从每个桶里取出一个dataNode,放到最终的返回值里,直到所有桶都取空——这样排序后,返回值里相邻的两个dataNode必然位于不同的dataHost上(tips:dataHost和dataNode的最终顺序仅与用户的输入顺序有关)

  • 将初始化完成的TableConfig对象加入Map

  • 你可能感兴趣的:(DBLE 2.17.08.1与MyCat 1.6.5的启动过程(5)——加载配置文件schema.xml)