数据库路由中间件MyCat - 源代码篇(14)
5. 路由模块
真正取得RouteResultset的步骤:AbstractRouteStrategy的route方法:
Created with Rapha?l 2.1.0Start处理一些路由之前的逻辑,返回真假?return nullsql拦截器拦截(就是用户自定义拦截一些语句并改写)是否需要checkSQLschema去掉schema name设置autocommit?是否是DDL语句rrs = DDL语句路由return rrsschema有默认node并且不是show语句rrs = 查询信息路由是不是查询信息语句(如show等)rrs = AST语义解析路由yesnoyesnoyesnoyesnoyesno
对应源代码:
public RouteResultset route(SystemConfig sysConfig, SchemaConfig schema, int sqlType, String origSQL,
String charset, ServerConnection sc, LayerCachePool cachePool) throws SQLNonTransientException {
/**
* 处理一些路由之前的逻辑
* 全局序列号,父子表插入
*/
if ( beforeRouteProcess(schema, sqlType, origSQL, sc) )
return null;
/**
* SQL 语句拦截
*/
String stmt = MycatServer.getInstance().getSqlInterceptor().interceptSQL(origSQL, sqlType);
if (origSQL != stmt && LOGGER.isDebugEnabled()) {
LOGGER.debug("sql intercepted to " + stmt + " from " + origSQL);
}
//对应schema标签checkSQLschema属性,把表示schema的字符去掉
if (schema.isCheckSQLSchema()) {
stmt = RouterUtil.removeSchema(stmt, schema.getName());
}
RouteResultset rrs = new RouteResultset(stmt, sqlType);
/**
* 优化debug loaddata输出cache的日志会极大降低性能
*/
if (LOGGER.isDebugEnabled() && origSQL.startsWith(LoadData.loadDataHint)) {
rrs.setCacheAble(false);
}
/**
* rrs携带ServerConnection的autocommit状态用于在sql解析的时候遇到
* select ... for update的时候动态设定RouteResultsetNode的canRunInReadDB属性
*/
if (sc != null ) {
rrs.setAutocommit(sc.isAutocommit());
}
/**
* DDL 语句的路由
*/
if (ServerParse.DDL == sqlType) {
return RouterUtil.routeToDDLNode(rrs, sqlType, stmt, schema);
}
/**
* 检查是否有分片
*/
if (schema.isNoSharding() && ServerParse.SHOW != sqlType) {
rrs = RouterUtil.routeToSingleNode(rrs, schema.getDataNode(), stmt);
} else {
RouteResultset returnedSet = routeSystemInfo(schema, sqlType, stmt, rrs);
if (returnedSet == null) {
rrs = routeNormalSqlWithAST(schema, stmt, rrs, charset, cachePool);
}
}
return rrs;
}
5.3 路由之前的逻辑 - 判断子表插入以及全局序列号的生成:
AbstractRouteStrategy.java
/**
* 路由之前必要的处理
* 主要是全局序列号插入,还有子表插入
*/
private boolean beforeRouteProcess(SchemaConfig schema, int sqlType, String origSQL, ServerConnection sc)
throws SQLNonTransientException {
return RouterUtil.processWithMycatSeq(schema, sqlType, origSQL, sc)
|| (sqlType == ServerParse.INSERT && RouterUtil.processERChildTable(schema, origSQL, sc))
|| (sqlType == ServerParse.INSERT && RouterUtil.processInsert(schema, sqlType, origSQL, sc));
}
这里利用了Java的一个特性,||表达式,前半部分如果为真,则后半部分不会被执行。首先执行RouterUtil.processWithMycatSeq(schema, sqlType, origSQL, sc),这个方法是判断是否是显示使用全局序列号的sql语句,比如像:insert into table1(id,name) values(next value for MYCATSEQ_GLOBAL,‘test’);
如果不是,则执行(sqlType == ServerParse.INSERT && RouterUtil.processERChildTable(schema, origSQL, sc)),这个方法判断是否是子表插入:
部分代码:
String tableName = StringUtil.getTableName(origSQL).toUpperCase();
final TableConfig tc = schema.getTables().get(tableName);
//判断是否为子表,如果不是,只会返回false
if (null != tc && tc.isChildTable()) {
final RouteResultset rrs = new RouteResultset(origSQL, ServerParse.INSERT);
String joinKey = tc.getJoinKey();
//因为是Insert语句,用MySqlInsertStatement进行parse
MySqlInsertStatement insertStmt = (MySqlInsertStatement) (new MySqlStatementParser(origSQL)).parseInsert();
......
这里注意,所有类型的SQL语句都有druid对应的SQLparser,比如说这里的插入语句就用MySqlInsertStatement解析。druidparser在这节先不讲,会在 AST语义解析路由中详细讲述。
Created with Rapha?l 2.1.0Start是否为子表?利用MySqlInsertStatement进行parse语句是否包含joinKey字段是否是批量插入是否是二级子表(父表不再有父表),并且分片字段正好是joinkey字段调用routeByERParentKey(就是直接用父表规则计算出datanode)得到RouteResultSet,判断是否需要全局序列号,执行语句,返回true启动异步线程去后台分片查询出datanode,只要查询出上一级表的parentkey字段的对应值在哪个分片即可,将查询结果封装成RouteResultSet返回falseyesnoyesnoyesno
接上面代码:
//判断条件完整性,取得解析后语句列中的joinkey列的index
int joinKeyIndex = getJoinKeyIndex(insertStmt.getColumns(), joinKey);
if (joinKeyIndex == -1) {
String inf = "joinKey not provided :" + tc.getJoinKey() + "," + insertStmt;
LOGGER.warn(inf);
throw new SQLNonTransientException(inf);
}
//子表不支持批量插入
if (isMultiInsert(insertStmt)) {
String msg = "ChildTable multi insert not provided";
LOGGER.warn(msg);
throw new SQLNonTransientException(msg);
}
//取得joinkey的值
String joinKeyVal = insertStmt.getValues().getValues().get(joinKeyIndex).toString();
String sql = insertStmt.toString();
// try to route by ER parent partion key
//如果是二级子表(父表不再有父表),并且分片字段正好是joinkey字段,调用routeByERParentKey
RouteResultset theRrs = RouterUtil.routeByERParentKey(sc, schema, ServerParse.INSERT, sql, rrs, tc, joinKeyVal);
if (theRrs != null) {
boolean processedInsert=false;
//判断是否需要全局序列号
if ( sc!=null && tc.isAutoIncrement()) {
String primaryKey = tc.getPrimaryKey();
processedInsert=processInsert(sc,schema,ServerParse.INSERT,sql,tc.getName(),primaryKey);
}
if(processedInsert==false){
rrs.setFinishedRoute(true);
sc.getSession2().execute(rrs, ServerParse.INSERT);
}
return true;
}
// route by sql query root parent‘s datanode
//如果不是二级子表或者分片字段不是joinKey字段结果为空,则启动异步线程去后台分片查询出datanode
//只要查询出上一级表的parentkey字段的对应值在哪个分片即可
final String findRootTBSql = tc.getLocateRTableKeySql().toLowerCase() + joinKeyVal;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("find root parent‘s node sql " + findRootTBSql);
}
ListenableFuture<String> listenableFuture = MycatServer.getInstance().
getListeningExecutorService().submit(new Callable<String>() {
@Override
public String call() throws Exception {
FetchStoreNodeOfChildTableHandler fetchHandler = new FetchStoreNodeOfChildTableHandler();
return fetchHandler.execute(schema.getName(), findRootTBSql, tc.getRootParent().getDataNodes());
}
});
Futures.addCallback(listenableFuture, new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
//结果为空,证明上一级表中不存在那条记录,失败
if (Strings.isNullOrEmpty(result)) {
StringBuilder s = new StringBuilder();
LOGGER.warn(s.append(sc.getSession2()).append(origSQL).toString() +
" err:" + "can‘t find (root) parent sharding node for sql:" + origSQL);
sc.writeErrMessage(ErrorCode.ER_PARSE_ERROR, "can‘t find (root) parent sharding node for sql:" + origSQL);
return;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("found partion node for child table to insert " + result + " sql :" + origSQL);
}
//找到分片,进行插入(和其他的一样,需要判断是否需要全局自增ID)
boolean processedInsert=false;
if ( sc!=null && tc.isAutoIncrement()) {
try {
String primaryKey = tc.getPrimaryKey();
processedInsert=processInsert(sc,schema,ServerParse.INSERT,origSQL,tc.getName(),primaryKey);
} catch (SQLNonTransientException e) {
LOGGER.warn("sequence processInsert error,",e);
sc.writeErrMessage(ErrorCode.ER_PARSE_ERROR , "sequence processInsert error," + e.getMessage());
}
}
if(processedInsert==false){
RouteResultset executeRrs = RouterUtil.routeToSingleNode(rrs, result, origSQL);
sc.getSession2().execute(executeRrs, ServerParse.INSERT);
}
}
@Override
public void onFailure(Throwable t) {
StringBuilder s = new StringBuilder();
LOGGER.warn(s.append(sc.getSession2()).append(origSQL).toString() +
" err:" + t.getMessage());
sc.writeErrMessage(ErrorCode.ER_PARSE_ERROR, t.getMessage() + " " + s.toString());
}
}, MycatServer.getInstance().
getListeningExecutorService());
return true;
}
return false;
如果返回false,则继续执行(sqlType == ServerParse.INSERT && RouterUtil.processInsert(schema, sqlType, origSQL, sc))
这个是处理一般的SQL插入语句,将其中的自增主键字段的值改写成内置的全局ID生成器生成的id。
RouterUtil.java:
public static boolean processInsert(SchemaConfig schema, int sqlType,
String origSQL, ServerConnection sc) throws SQLNonTransientException {
String tableName = StringUtil.getTableName(origSQL).toUpperCase();
TableConfig tableConfig = schema.getTables().get(tableName);
boolean processedInsert=false;
//判断是有自增字段
if (null != tableConfig && tableConfig.isAutoIncrement()) {
String primaryKey = tableConfig.getPrimaryKey();
processedInsert=processInsert(sc,schema,sqlType,origSQL,tableName,primaryKey);
}
return processedInsert;
}
调用processInsert(sc,schema,sqlType,origSQL,tableName,primaryKey):
Created with Rapha?l 2.1.0Start是否为类似于insert into table1 select * from table2的语句抛对应异常是否为批量插入插入语句是否提供列结构?如果主键不在插入语句的fields中,则需要进一步处理yesnoyesnoyesno
public static boolean processInsert(ServerConnection sc,SchemaConfig schema,
int sqlType,String origSQL,String tableName,String primaryKey) throws SQLNonTransientException {
int firstLeftBracketIndex = origSQL.indexOf("(");
int firstRightBracketIndex = origSQL.indexOf(")");
String upperSql = origSQL.toUpperCase();
int valuesIndex = upperSql.indexOf("VALUES");
int selectIndex = upperSql.indexOf("SELECT");
int fromIndex = upperSql.indexOf("FROM");
//屏蔽insert into table1 select * from table2语句
if(firstLeftBracketIndex < 0) {
String msg = "invalid sql:" + origSQL;
LOGGER.warn(msg);
throw new SQLNonTransientException(msg);
}
//屏蔽批量插入
if(selectIndex > 0 &&fromIndex>0&&selectIndex>firstRightBracketIndex&&valuesIndex<0) {
String msg = "multi insert not provided" ;
LOGGER.warn(msg);
throw new SQLNonTransientException(msg);
}
//插入语句必须提供列结构,因为MyCat默认对于表结构无感知
if(valuesIndex + "VALUES".length() <= firstLeftBracketIndex) {
throw new SQLSyntaxErrorException("insert must provide ColumnList");
}
//如果主键不在插入语句的fields中,则需要进一步处理
boolean processedInsert=!isPKInFields(origSQL,primaryKey,firstLeftBracketIndex,firstRightBracketIndex);
if(processedInsert){
processInsert(sc,schema,sqlType,origSQL,tableName,primaryKey,firstLeftBracketIndex+1,origSQL.indexOf(‘(‘,firstRightBracketIndex)+1);
}
return processedInsert;
}
对于主键不在插入语句的fields中的SQL,需要改写。比如hotnews主键为id,插入语句为:
insert into hotnews(title) values(‘aaa‘);
需要改写成:
insert into hotnews(id, title) values(next value for MYCATSEQ_hotnews,‘aaa‘);
这个在下面这个函数实现:
private static void processInsert(ServerConnection sc, SchemaConfig schema, int sqlType, String origSQL,
String tableName, String primaryKey, int afterFirstLeftBracketIndex, int afterLastLeftBracketIndex) {
int primaryKeyLength = primaryKey.length();
int insertSegOffset = afterFirstLeftBracketIndex;
String mycatSeqPrefix = "next value for MYCATSEQ_";
int mycatSeqPrefixLength = mycatSeqPrefix.length();
int tableNameLength = tableName.length();
char[] newSQLBuf = new char[origSQL.length() + primaryKeyLength + mycatSeqPrefixLength + tableNameLength + 2];
origSQL.getChars(0, afterFirstLeftBracketIndex, newSQLBuf, 0);
primaryKey.getChars(0, primaryKeyLength, newSQLBuf, insertSegOffset);
insertSegOffset += primaryKeyLength;
newSQLBuf[insertSegOffset] = ‘,‘;
insertSegOffset++;
origSQL.getChars(afterFirstLeftBracketIndex, afterLastLeftBracketIndex, newSQLBuf, insertSegOffset);
insertSegOffset += afterLastLeftBracketIndex - afterFirstLeftBracketIndex;
mycatSeqPrefix.getChars(0, mycatSeqPrefixLength, newSQLBuf, insertSegOffset);
insertSegOffset += mycatSeqPrefixLength;
tableName.getChars(0, tableNameLength, newSQLBuf, insertSegOffset);
insertSegOffset += tableNameLength;
newSQLBuf[insertSegOffset] = ‘,‘;
insertSegOffset++;
origSQL.getChars(afterLastLeftBracketIndex, origSQL.length(), newSQLBuf, insertSegOffset);
processSQL(sc, schema, new String(newSQLBuf), sqlType);
}
最后的processSQL(sc, schema, new String(newSQLBuf), sqlType);是将语句放入执行队列:
这里MyCat考虑NIO线程吞吐量以及全局ID生成线程安全的问题,使用如下流程执行需要全局ID的SQL insert语句。
processSQL(sc, schema, new String(newSQLBuf), sqlType):
SessionSQLPair sessionSQLPair = new SessionSQLPair(sc.getSession2(), schema, sql, sqlType);
MycatServer.getInstance().getSequnceProcessor().addNewSql(sessionSQLPair);
Created with Rapha?l 2.1.0将改写的语句写入队列,当前线程返回队列线程ExecuteThread,单线程,读取队列队列中有数据?读取数据,改写sql,传回ServerConnection的routeEndExecuteSQL方法这时,不会再进入 路由之前的逻辑,而是进入AST语义解析路由yes