[Zebra] 分片路由和寻找分片键值的基本过程
路由规则匹配
分库分表路由规则是表+字段的维度,首先要将 sql 中的表识别出来,然后和规则进行匹配, 然后才能根据规则确定分库分表是用哪个字段、按照什么分片算法,比如 userId 8 库 128表;
zebra 中 DefaultShardRouter#router
路由器首先进行 sql 解析 SQLParsedResult parsedResult = SQLParser.parseWithCache(sql);
生成 druid ast 语法树 SQLStatement
MySqlLexer lexer = new MySqlLexer(sql);
HintCommentHandler commentHandler = new HintCommentHandler();
lexer.setCommentHandler(commentHandler);
lexer.nextToken();
SQLStatementParser parser = new MySqlStatementParser(lexer);
List<SQLStatement> stmtList = parser.parseStatementList();
if (stmtList.size() == 1) {
SQLParsedResult sqlParsedResult = parseInternal(stmtList.get(0));
sqlParsedResult.getRouterContext().setSqlhint(sqlhint);
return sqlParsedResult;
}
//.... 省略
并通过 MySqlASTVisitorAdapter 访问器提取出 sql 语句中的表名 和 字段
public class AbstractMySQLASTVisitor extends MySqlASTVisitorAdapter {
protected SQLParsedResult result;
public AbstractMySQLASTVisitor(SQLParsedResult result) {
this.result = result;
}
/** 遍历表名,存到解析结果中 **/
@Override
public boolean visit(SQLExprTableSource x) {
SQLName table = (SQLName) x.getExpr();
String simpleName = table.getSimpleName();
String tableName = simpleName.startsWith("`") ? parseTableName(simpleName) : simpleName;
result.getRouterContext().getTableSet().add(tableName);
return true;
}
private String parseTableName(String tableName) {
StringBuilder sb = new StringBuilder(tableName.length());
for (int i = 0; i < tableName.length(); ++i) {
if (tableName.charAt(i) != '`') {
sb.append(tableName.charAt(i));
}
}
return sb.toString();
}
public SQLParsedResult getResult() {
return result;
}
}
然后,就可以根据 表名 和路由规则进行匹配,获取表对应的路由规则
分片键的值
经过前面的步骤,sql 里边表的路由规则已经确定,接下来就要获取分片键的实际值, 然后根据分片算法定位到目标表的索引 比如 userId = 1 那么目标表就是 t_user_1
这是个繁琐的过程,需要遍历 SQLStatement 语法树,根据 sql 的不同类型,先找到对应的分片键 再找分片键是值, 值可能是参数化的 也可能是字面量; 如果是带子查询、联表等场景就更复杂了, 不过个人认为既然是分库分表场景 就不应该去支持太多复杂sql
这边简单看下 insert sql 分片键值查找过程
com.dianping.zebra.shard.router.DefaultShardRouter#routerOneRule
-> ShardEvalResult shardResult = tableShardRule.eval(new ShardEvalContext(parsedResult, params, optimizeIn));
-> TableShardRule#evalDimension()
ShardColumnValueUtil#eval() 解析列值
private static Collection<Object> evalInsert(SQLParsedResult parseResult, String column, List<Object> params,
boolean isBatchInsert) {
MySqlInsertStatement stmt = (MySqlInsertStatement) parseResult.getStmt();
List<SQLExpr> columns = stmt.getColumns();
List<SQLInsertStatement.ValuesClause> valuesList = stmt.getValuesList(); // 取出 insert 语句的 values() 部分
if (isBatchInsert) {
List<Object> evalList = new LinkedList<Object>();
parseBatchValueList(evalList, params, columns, valuesList, column);
return evalList;
} else {
// use the first value in the values
// 解析insert值
Set<Object> evalSet = new LinkedHashSet<Object>();
parseValueList(evalSet, params, columns, valuesList, column);
return evalSet;
}
private static void parseValueList(Set<Object> evalSet, List<Object> params, List<SQLExpr> columns,
List<SQLInsertStatement.ValuesClause> valuesList, String column) {
SQLInsertStatement.ValuesClause values = valuesList.get(0);
for (int i = 0; i < columns.size(); i++) {
SQLName columnObj = (SQLName) columns.get(i);
if (evalColumn(columnObj.getSimpleName(), column)) {
SQLExpr sqlExpr = values.getValues().get(i);
if (sqlExpr instanceof SQLVariantRefExpr) { // 如果 sql insert 分片键的值是占位符,则从参数列表中取出来
SQLVariantRefExpr ref = (SQLVariantRefExpr) sqlExpr;
evalSet.add(params.get(ref.getIndex()));
} else if (sqlExpr instanceof SQLValuableExpr) { // 如果分片键的值是字面量,则直接拿字面量的值返回
evalSet.add(((SQLValuableExpr) sqlExpr).getValue());
}
break;
}
}
}
对于 insert 语句,需要从 values() 语句部分去遍历目标分片键的位置和值, 对于 select/update/delete 以及其他类型,过程则是根据 sql 去处理
表改写
取到目标分片键的值后,就可以根据路由算法计算出最终物理表,并进行改写; druid ast中提供比较方便的 MySqlOutputVisitor 访问器,可以在遍历之前生产的语法树 SQLStatment 反向打印 sql 的过程中,对sql语句进行改写
Zebra 中 ShardRewriteTableOutputVisitor 继承 MysqlOutputVisitor 重写了 visit(SQLExprTableSource) 方法
public boolean visit(SQLExprTableSource x) {
SQLName name = (SQLName) x.getExpr();
String simpleName = name.getSimpleName();
boolean hasQuote = simpleName.charAt(0) == '`';
String tableName = hasQuote ? parseTableName(simpleName) : simpleName; // 获取逻辑表名
String finalTable = tableMapping.get(tableName); // 获取计算好的目标分表
if (finalTable != null) {
if (hasQuote) {
print0("`" + finalTable + "`"); // 替换成目标分表名
} else {
print0(finalTable);
}
} else {
x.getExpr().accept(this);
}
if (x.getAlias() != null) {
print(' ');
print0(x.getAlias());
}
for (int i = 0; i < x.getHintsSize(); ++i) {
print(' ');
x.getHints().get(i).accept(this);
}
return false;
}
本文来自博客园,作者:mushishi,转载请注明原文链接:https://www.cnblogs.com/mushishi/p/18357695