源码分析MyCat----优化篇之支持ER分片多语句插入(基于1.6)

 1、ER分片支持多语句插入优化

1.1 问题抛出

一个经典的使用场景,订单表(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

 

你可能感兴趣的:(Mycat,源码研究mycat)