一个经典的使用场景,订单表(t_order)与订单明细(t_order_item),订单明细表ER分片与订单表。
代码层次一般有如下实现(OrderDao):
void createOrder( Order order);
void createOrderItem( Interger orderId, OrderItem item); //@1
void createOrderItem( Integer orderID,List items); //@2
映射文件层面(mybatis实现):
insert into t_order_item(字段列表) values (字段值)
在java代码中,多次调用createOrderItem方法,完成多个OrderItem的创建工作。
第二实现方法:
insert into t_order_item(字段列表) values (字段值)
第二种形式,在mysql协议中有个专门的词描述(multi-statment)。
在mycat分支1.6中,目前ER分片只支持第一种方式,而不支持多multi-statement支持。
源码分析:
public static boolean processERChildTable(final SchemaConfig schema, final String origSQL,
final ServerConnection sc) throws SQLNonTransientException {
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(); // @1
//判断条件完整性,取得解析后语句列中的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();
//解决bug #938,当关联字段的值为char类型时,去掉前后"'"
String realVal = joinKeyVal;
if (joinKeyVal.startsWith("'") && joinKeyVal.endsWith("'") && joinKeyVal.length() > 2) {
realVal = joinKeyVal.substring(1, joinKeyVal.length() - 1);
}
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, realVal);
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 listenableFuture = MycatServer.getInstance().
getListeningExecutorService().submit(new Callable() {
@Override
public String call() throws Exception {
FetchStoreNodeOfChildTableHandler fetchHandler = new FetchStoreNodeOfChildTableHandler();
// return fetchHandler.execute(schema.getName(), findRootTBSql, tc.getRootParent().getDataNodes());
return fetchHandler.execute(schema.getName(), findRootTBSql, tc.getRootParent().getDataNodes(), sc);
}
});
Futures.addCallback(listenableFuture, new FutureCallback() {
@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);
if(!sc.isAutocommit()) { // 处于事务下失败, 必须回滚
sc.setTxInterrupt("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;
}
问题出现在这里,,这里把传入的SQL固定认为只会有一条insert into 语句,导致一簇insert into语句,只会第一条语句生效。接下来给出修复代码,并分析一下该方法。
给出的修复代码如下:
/**
* 该方法,返回是否是ER字表
* @param schema
* @param origSQL
* @param sc
* @return
* @throws SQLNonTransientException
*
* 备注说明:
* edit by ding.w at 2017.4.28, 主要处理 CLIENT_MULTI_STATEMENTS(insert into ; insert into)的情况
* 目前仅支持mysql,并COM_QUERY请求包中的所有insert语句要么全部是er表,要么全部不是
*
*
*/
public static boolean processERChildTable(final SchemaConfig schema, final String origSQL,
final ServerConnection sc) throws SQLNonTransientException {
MySqlStatementParser parser = new MySqlStatementParser(origSQL);
List statements = parser.parseStatementList(); // @1
if(statements == null || statements.isEmpty() ) {
throw new SQLNonTransientException(String.format("无效的SQL语句:%s", origSQL));
}
boolean erFlag = false; //是否是er表
for(SQLStatement stmt : statements ) { // @2
MySqlInsertStatement insertStmt = (MySqlInsertStatement) stmt; // @3
String tableName = insertStmt.getTableName().getSimpleName().toUpperCase();
final TableConfig tc = schema.getTables().get(tableName);
if (null != tc && tc.isChildTable()) { // @4
erFlag = true;
String sql = insertStmt.toString();
final RouteResultset rrs = new RouteResultset(sql, ServerParse.INSERT);
String joinKey = tc.getJoinKey();
//因为是Insert语句,用MySqlInsertStatement进行parse
// MySqlInsertStatement insertStmt = (MySqlInsertStatement) (new MySqlStatementParser(origSQL)).parseInsert();
//判断条件完整性,取得解析后语句列中的joinkey列的index
int joinKeyIndex = getJoinKeyIndex(insertStmt.getColumns(), joinKey); // @5
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();
//解决bug #938,当关联字段的值为char类型时,去掉前后"'"
String realVal = joinKeyVal;
if (joinKeyVal.startsWith("'") && joinKeyVal.endsWith("'") && joinKeyVal.length() > 2) {
realVal = joinKeyVal.substring(1, joinKeyVal.length() - 1);
}
// try to route by ER parent partion key
//如果是二级子表(父表不再有父表),并且分片字段正好是joinkey字段,调用routeByERParentKey
RouteResultset theRrs = RouterUtil.routeByERParentKey(sc, schema, ServerParse.INSERT, sql, rrs, tc, realVal);
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); // @6
}
// return true;
//继续处理下一条
continue;
}
// route by sql query root parent's datanode
//如果不是二级子表或者分片字段不是joinKey字段结果为空,则启动异步线程去后台分片查询出datanode
//只要查询出上一级表的parentkey字段的对应值在哪个分片即可, 使用parentKey 去异步查询,然后进行分片
final String findRootTBSql = tc.getLocateRTableKeySql().toLowerCase() + joinKeyVal;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("find root parent's node sql " + findRootTBSql);
}
ListenableFuture listenableFuture = MycatServer.getInstance().
getListeningExecutorService().submit(new Callable() {
@Override
public String call() throws Exception {
FetchStoreNodeOfChildTableHandler fetchHandler = new FetchStoreNodeOfChildTableHandler();
// return fetchHandler.execute(schema.getName(), findRootTBSql, tc.getRootParent().getDataNodes());
return fetchHandler.execute(schema.getName(), findRootTBSql, tc.getRootParent().getDataNodes(), sc);
}
});
Futures.addCallback(listenableFuture, new FutureCallback() {
@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);
if(!sc.isAutocommit()) { // 处于事务下失败, 必须回滚
sc.setTxInterrupt("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());
} else if(erFlag) {
throw new SQLNonTransientException(String.format("%s包含不是ER分片的表", origSQL));
}
}
return erFlag;
}
代码@1,利用druid引擎解析SQL,支持多multi执行。这里是解决该问题的关键。
代码@2,一条语句一条语句的执行。
代码@3,能进入到这个方法,说明肯定是insert语句。
代码@4,判断是否是ER字表。
代码@5,主要利用joinKey进行路由计算。
代码@6,直接在分片上依次执行该insert语句。
修复好之后,怎么验证是正确的,先理论,然后再测试。
整个语句的执行流程如下:(参考博文:http://blog.csdn.net/prestigeding/article/details/71247101)
set autocommit=0;
insert into ;insert into 语句
commit;语句
这里中间步骤的 inisert into 语句是一个一个执行的,分发到的节点,commit语句会同样下发到相同的节点去执行吗?这里是问题的关键。mycat是这样处理这个问题的,每一个前端连接(ServerConnection)会有持有一个查询Hander(QueryHandler),每一个QueryHanlder里面会持有一个NonBlockingSession,在一次完整的命令处理过程中,NonBlockingSession会保存每个路由节点(执行过的分片节点)与后端连接的BackConnection的对应关系(ConcurrentHashMap
ction> target)。
/**
* @return previous bound connection
*/
public BackendConnection bindConnection(RouteResultsetNode key,
BackendConnection conn) {
// System.out.println("bind connection "+conn+
// " to key "+key.getName()+" on sesion "+this);
return target.put(key, conn);
}
方法时添加数据,追踪到源头(PhysicalDatasource.getConnection中方法,关键在内部调用createNewConnection,其回调方法connectionAcquired),前端连接使用后端连接的时候,会调用该方法完成该关系的维护,这样能确保commit命令到来后,发送到正确的后端连接执行。故该方法理论上这样修改问题不大。
解析来可以自行测试该方法的正确性:
备注,mycat学习测试环境搭建,请参考 http://blog.csdn.net/prestigeding/article/details/71123373
代码提交在mycat git库地址:https://github.com/MyCATApache/Mycat-Server/pull/1491
测试代码:http://git.oschina.net/zhcsoft/StudyDemo