schema.xml的加载过程概况
com.actiontech.dble.config.loader.xml.XMLSchemaLoader
的load()
方法是schema.xml的加载入口。这个加载入口仅仅把schema.xml,以带DTD校验的方式加载成到内存中,形成w3c的DOM。然后依次调用loadDataHosts()
、loadDataNodes()
和loadSchemas()
这三个方法,处理schema.xml中的三种一级标签,加载成com.actiontech.dble.config.model
命名空间中对应的类的对象。
方法名 | 处理的标签 | 目标类 |
---|---|---|
loadDataHosts() | <dataHost> | DataHostConfig |
loadDataNodes() | <dataNode> | DataNodeConfig |
loadSchemas() | <schema> | SchemaConfig |
处理<dataHost>标签——loadDataHosts()
loadDataHosts()
的使命是将<dataHost>
标签加载成DataHostConfig
对象。在逻辑结构上,<dataHost>
标签和DataHostConfig
对象完全相同。
<dataHost>属性 | DataHostConfig属性 |
---|---|
name | String name |
maxCon | int maxCon |
minCon | int minCon |
balance | int balance |
switchType | int switchType |
slaveThreshold | int slaveThreshold |
tempReadHostAvailable | boolean tempReadHostAvailable |
<heartbeat> | String heartbeatSQL |
<writeHost> | DBHostConfig[] writeHosts |
<readHost> | Map<Integer, DBHostConfig[]> readHosts |
可以看到,相对于name这些属性,<writeHost>
和<readHost>
这两个property(XML概念)的处理是重点,其中需要着重解决下面两个问题:
-
<writeHost>
和<readHost>
应该加载成什么?
除了<writeHost>
可以包含一个或多个<readHost>
外,在属性层面,两者基本没有差异。因此统一用com.actiontech.dble.config.model.DBHostConfig
这个类来标识它们。一个DBHostConfig
对象表示一个<writeHost>
或<readHost>
标签。
-
<writeHost>
对<readHost>
的一对多包含关系应该如何保存下来?
每个<writeHost>
标签在DataHostConfig.writeHosts
这个数组中存储,也因此会有它在数组中的序号,所以DataHostConfig.readHosts
这个Map集合利用了这一点,设计成了“<<writeHost>
在writeHosts中的序号, 这个<writeHost>
拥有的<readHost>
数组>”这么一种形式来存储(虽然我觉得用存储密度更高的二维数组可能会更好)。
了解了以上概况之后,它的工作流程就很明晰了,大致是:
从schema.xml中逐个找出
<dataHost>
标签加载
<dataHost>
的简单属性到临时变量中:name, maxCon, minCon, balance, switchType, slaveThreshold, tempReadHostAvailable和<heartbeat>加载
<dataHost>
的复杂属性<writeHost>
和<readHost>
到临时变量中根据临时变量生成
DataHostConfig
对象,并登记到XMLSchemaLoader
中(加入到内部Map集合中)
private void loadDataHosts(Element root) {
// 提取schema.xml中所有的<dataHost>标签
NodeList list = root.getElementsByTagName("dataHost");
// 逐个<dataHost>标签进行处理
for (int i = 0, n = list.getLength(); i < n; ++i) {
Element element = (Element) list.item(i);
// 加载简单属性到临时变量中:name, maxCon, minCon, balance, switchType, slaveThreshold, tempReadHostAvailable和<heartbeat>
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();
// 获取当前<dataHost>下所有的<writeHost>
NodeList writeNodes = element.getElementsByTagName("writeHost");
// 初始化当前<dataHost>中,<writeHost>和<readHost>的临时变量(数组及集合)
DBHostConfig[] writeDbConfs = new DBHostConfig[writeNodes.getLength()];
Map<Integer, DBHostConfig[]> readHostsMap = new HashMap<>(2);
// 逐个<writeHost>进行处理
for (int w = 0; w < writeDbConfs.length; w++) {
Element writeNode = (Element) writeNodes.item(w);
// 创建<writeHost>对应的DBHostConfig对象,并加入到临时变量的数组中
writeDbConfs[w] = createDBHostConf(name, writeNode, maxCon, minCon);
// 获取当前<writeHost>下所有的<readHost>
NodeList readNodes = writeNode.getElementsByTagName("readHost");
if (readNodes.getLength() != 0) {
// 创建存放当前<writeHost>下所有<readHost>的临时数组
DBHostConfig[] readDbConfs = new DBHostConfig[readNodes.getLength()];
// 逐个<readHost>进行处理
for (int r = 0; r < readDbConfs.length; r++) {
Element readNode = (Element) readNodes.item(r);
// 创建<writeHost>对应的DBHostConfig对象,并加入到临时变量的数组中
readDbConfs[r] = createDBHostConf(name, readNode, maxCon, minCon);
}
// 将准备好的临时数组注册到<readHost>临时变量(Map集合)中
readHostsMap.put(w, readDbConfs);
}
}
// 根据<dataHost>各属性的临时变量创建DataHostConfig
DataHostConfig hostConf = new DataHostConfig(name,
writeDbConfs, readHostsMap, switchType, slaveThreshold, tempReadHostAvailable);
hostConf.setMaxCon(maxCon);
hostConf.setMinCon(minCon);
hostConf.setBalance(balance);
hostConf.setHearbeatSQL(heartbeatSQL);
// 将当前<dataHost>对应的DataHostConfig注册到XMLSchemaLoader的内部清单中
dataHosts.put(hostConf.getName(), hostConf);
}
}
当中,createDBHostConf()
这个函数在DBLE和MyCat中,基本功能是一致的:
- 获取
<writeHost>
或<readHost>
的属性:host、url、user、password、usingDecrypt和weight - 检查host、url和user都不为空
- 从host属性中分离出ip和port
- 对password属性的内容进行RSA的解密(usingDecrypt属性为1时)
但是,由于设计思路的分歧,DBLE裁剪了MyCat中的以下功能:
- 非MySQL数据库的支持(通过
<dataHost>
的dbType属性提供) - 其他JDBC Driver的支持(通过
<dataHost>
的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;
}
处理<dataNode>标签——loadDataNodes()
与loadDataHosts()
相似,loadDataNodes()
的使命是将<dataNode>
标签加载成与之逻辑结构相同的Java对象。在这里,加载目标就是DataNodeConfig
对象。
<dataNode>属性 | DataNodeConfig属性 |
---|---|
name | String name |
database | String database |
dataHost | String dataHost |
可以看出,相对于<dataHost>
和DataHostConfig
,<dataNode>
和DataNodeConfig
要简单得多。但是,MyCat和DBLE为了让<dataNode>
支持一种自制语法,loadDataNodes()
的代码变得复杂了许多。
从现有代码来看,这个语法是为了减少schema.xml的编写量(并不会减少DataNodeConfig
对象的个数),将多个拥有同样dataHost属性或database属性的<dataNode>
缩写成一个<dataNode>
。
<!-- 例子1:缩写dataHost属性相同的标签 -->
<!-- 缩写前 -->
<dataNode name="dn01" dataHost="dh01" database="customer" />
<dataNode name="dn03" dataHost="dh01" database="stock" />
<!-- 缩写后 -->
<dataNode name="dn01,dn03"
dataHost="dh01"
database="customer,stock“ />
<!-- 例子2:缩写database属性相同的标签 -->
<!-- 缩写前 -->
<dataNode name="dn01" dataHost="dh01" database="customer" />
<dataNode name="dn02" dataHost="dh02" database="customer" />
<!-- 缩写后 -->
<dataNode name="dn01,dn02"
dataHost="dh01,dh02"
database="customer“ />
<!-- 例子3:缩写dataHost和database属性局部相同的标签 -->
<!-- 缩写前 -->
<dataNode name="dn01" dataHost="dh01" database="customer" />
<dataNode name="dn02" dataHost="dh02" database="customer" />
<dataNode name="dn03" dataHost="dh01" database="stock" />
<dataNode name="dn04" dataHost="dh02" database="stock" />
<!-- 缩写后 -->
<dataNode name="dn01,dn02,dn03,dn04"
dataHost="dh01,dh02"
database="customer,stock“ />
但是这个缩写语法看起来未完全实现它的设计目标。在代码和注释中可以看到,除了“,”可以作为分隔符外,设计上还支持“$”和“-”,并且应该有不同的语义。但现在这些分隔符的作用没有差别,还没有完全实现设计目标。此外,缩写会损失一部分易读性,所以我不太建议使用该缩写语法。
loadDataNodes
的工作流程大致是:
从schema.xml中逐个找出
<dataNode>
标签加载
<dataNode>
的属性到临时变量中:name, dataHost和database根据临时变量判断用户是否使用了缩写语法,从而决定是直接生成一个
DataNodeConfig
对象,还是用(dataHost, database)的两层循环了生成多个DataNodeConfig
对象将生成的一个或多个
DataHostConfig
对象登记到XMLSchemaLoader
中(加入到内部Map集合中)
private void loadDataNodes(Element root) {
// 提取schema.xml中所有的<dataNode>标签
NodeList list = root.getElementsByTagName("dataNode");
// 逐个<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<String[]> 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<String[]> mergerHostDatabase(String[] hostStrings, String[] databases) {
List<String[]> 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);
}
处理<schema>标签——loadSchemas()
loadSchemas()
的使命是将<schema>
标签加载成SchemaConfig
对象。在逻辑结构上,<schema>
标签和SchemaConfig
对象有很多共同的地方。
<schema>属性 | SchemaConfig属性 |
---|---|
name | String name |
dataNode | String dataNode |
sqlMaxLimit | int defaultMaxLimit |
<table> | Map<Integer, TableConfig> tables |
此外,<schema>
标签的子标签<table>
由于涉及到了E-R关系这种比较特殊的分片策略,导致这个子标签的处理要分成三部分:
直接加载一般属性,例如name、primaryKey之类。
读取用户指定的分片算法(rule属性)和逻辑分片(dataNode属性)的字面值,进行是否存在之类的检查后,与之前的rule.xml和
loadDataNodes()
的成果关联起来。如果含有
<childTable>
子标签,那这个<table>
和它的<childTable>
构成了E-R关系,会使用processChildTables()
方法来递处理可能存在的多层<childTable>
,并为每个<childTable>
创建一个比较赋值特殊的TableConfig
对象——它会被赋予parentTC、joinKey和parentKey属性。
tips:loadTables()
执行完后,返回来的是每个<table>
和<childTable>
都有自己的TableConfig
对象的一个Map<String, TableConfig>哈希表。如果存在E-R关系的表,还需要回到loadSchemas
,由它调用XMLSchemaLoader
类的其他方法来处理、整合E-R关系引入而产生的关系处理。SchemaConfig
使用独立的数据结构ERTable
来描述E-R关系。
private void loadSchemas(Element root) {
// 读取所有的<schema>标签
NodeList list = root.getElementsByTagName("schema");
// 逐个<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<String>
if (dataNode != null && !dataNode.isEmpty()) {
List<String> dataNodeLst = new ArrayList<>(1);
dataNodeLst.add(dataNode);
// 调用checkDataNodeExists()来检查用户给这个<schema>指定的dataNode是不是都已经已经加载过的
checkDataNodeExists(dataNodeLst);
} else {
dataNode = null;
}
// 调用loadTables()方法来加载当前<schema>里的所有<table>标签,每个标签加载成一个TableConfig对象,放到Map<String, TableConfig>中
Map<String, TableConfig> 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);
}
// 处理完所有`<schema>`标签后,调用`makeAllErRelations()`(tips:用于ER表,不关注这种用法,跳过)
makeAllErRelations();
}
loadTables()
会将<table>
标签加载成TableConfig
对象。由于当中涉及众多E-R表的处理逻辑,而笔者并不关注,所以先暂时略过,只分析其处理普通<table>
的过程:
创建
TableConfig
对象读取简单属性name、primaryKey、autoIncrement、needAddLimit、type、rule和ruleRequired
读取属性dataNode,这个属性可以一次指定多个dataNode,使用“,”、“$”或“-”来分隔
调用checkDataNodeExists()来检查用户给这个<schema>指定的dataNode是不是都已经已经加载过的
如果当前处理的
<table>
有分片函数(非全局表),通过checkRuleSuitTable()来间接调用所有分片算法的基类AbstractPartitionAlgorithm的suitableFor(),检查用户配置的dataNode属性里dataNode个数是否与这个表配置的算法的分片数量一致(tips:这个检查依赖AbstractPartitionAlgorithm.getPartitionNum()提供当前算法需要的分片数量,默认上该函数返回的-1会让suitableFor()跳过检查,所以如果要实现自己的定制分片函数的话,需要自行覆盖该类;suitableFor()函数是final,无法被覆盖)如果用户在配置
<table>
的dataNodes时使用了distribute语法(distribute(xxx,xxx,xxx))的话,调用distributeDataNodes()方法进行排序,保证dataNode编号过程中,尽可能地跨位于不同的物理分片(dataHost)上——首先,一个dataHost创建一个桶(数据结构是列表),把用户的dataNode过一遍,按照它们的dataHost放进对应的桶里;然后,按顺序从每个桶里取出一个dataNode,放到最终的返回值里,直到所有桶都取空——这样排序后,返回值里相邻的两个dataNode必然位于不同的dataHost上(tips:dataHost和dataNode的最终顺序仅与用户的输入顺序有关)将初始化完成的
TableConfig
对象加入Map<String, TableConfig>