[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;
		}
posted @ 2024-08-13 21:34  mushishi  阅读(7)  评论(0编辑  收藏  举报