源码分析mycat1.6之网络篇----前后端连接交互设计(mycat命令处理流程)

本章将从如下3个方面剖析mycat前后端交互原理:

1、mycat握手认证阶段、命令处理阶段切换

2、mycat前后端如何交互

3、后端连接及IO线程模型。

1、mycat握手认证阶段、命令处理阶段切换

从上篇mycat前端线程模型可以看到,mycat对应用程序来说将自己伪装成mysql服务器(实现mysql通信协议)接受客户端的命令,比如查询,更新等命令。mycat前端线程模型基于主从Reactor模式,我们再简单的回顾一下其处理流程:NioAcceptor接受一个客户端连接,封装成一个FrontedConnection,将其转发到NioReactor中,NioReactor接管其读写事件的处理,一个客户端连接一旦连接成功,mycat会向客户端发送握手包完成与客户端的握手认证协议过程,该过程是通过FrontedConnection的register方法(其实是channelRegister事件方法更容易理解)和FrontedConnection的初始handler(FrontedAuthenticator)来完成的。认证信息完成后,该FrontedConnection是如何切换到命令处理模式的呢?其核心关键想必在FrontedAuthenticator中实现:其关键点如下:

protected void success(AuthPacket auth) {
        source.setAuthenticated(true);
        source.setUser(auth.user);
        source.setSchema(auth.database);
        source.setCharsetIndex(auth.charsetIndex);
        source.setHandler(new FrontendCommandHandler(source));   // @1

        if (LOGGER.isInfoEnabled()) {
            StringBuilder s = new StringBuilder();
            s.append(source).append('\'').append(auth.user).append("' login success");
            byte[] extra = auth.extra;
            if (extra != null && extra.length > 0) {
                s.append(",extra:").append(new String(extra));
            }
            LOGGER.info(s.toString());
        }

        ByteBuffer buffer = source.allocate();
        source.write(source.writeToBuffer(AUTH_OK, buffer));    // @2
        boolean clientCompress = Capabilities.CLIENT_COMPRESS==(Capabilities.CLIENT_COMPRESS & auth.clientFlags);   // @3
        boolean usingCompress= MycatServer.getInstance().getConfig().getSystem().getUseCompression()==1 ;
        if(clientCompress&&usingCompress)
        {
            source.setSupportCompress(true);
        }  
    }

代码@1,关键中的关键,从握手认证状态进入到命令处理阶段,就是改变一下FrontedConnection的handler,从上文的讲解已经知道,NIOSockerWR在读事件处理时,,每成功解一个mysql包,就会交给handler处理。切换成FrontendCommandHandler处理器,进入到命令执行阶段,就这么简单。

代码@2,发送OK报文给mysql客户端。

代码@3,从认证授权包得知客户端支持的权能标记,mycat只处理了是否支持压缩。

2、前端连接与后端连接交互

上面已经剖析了mycat是如何从握手认证协议阶段向命令执行模式转变的,前端连接与后端连接的交互入口在FrontendCommandHandler处理器,该handler处理客户端发送的命令报文,故接下来将重点讲解FrontendCommandHandler处理器,源码如下:

/**
 * 前端命令处理器
 *
 * @author mycat
 */
public class FrontendCommandHandler implements NIOHandler
{

    protected final FrontendConnection source;
    protected final CommandCount commands;

    public FrontendCommandHandler(FrontendConnection source)
    {
        this.source = source;
        this.commands = source.getProcessor().getCommands();
    }

    @Override
    public void handle(byte[] data)
    {
        if(source.getLoadDataInfileHandler()!=null&&source.getLoadDataInfileHandler().isStartLoadData())
        {
            MySQLMessage mm = new MySQLMessage(data);
            int  packetLength = mm.readUB3();
            if(packetLength+4==data.length)
            {
                source.loadDataInfileData(data);
            }
            return;
        }
        switch (data[4])
        {
            case MySQLPacket.COM_INIT_DB:
                commands.doInitDB();
                source.initDB(data);
                break;
            case MySQLPacket.COM_QUERY:
                commands.doQuery();
                source.query(data);
                break;
            case MySQLPacket.COM_PING:
                commands.doPing();
                source.ping();
                break;
            case MySQLPacket.COM_QUIT:
                commands.doQuit();
                source.close("quit cmd");
                break;
            case MySQLPacket.COM_PROCESS_KILL:
                commands.doKill();
                source.kill(data);
                break;
            case MySQLPacket.COM_STMT_PREPARE:
                commands.doStmtPrepare();
                source.stmtPrepare(data);
                break;
            case MySQLPacket.COM_STMT_SEND_LONG_DATA:
                commands.doStmtSendLongData();
                source.stmtSendLongData(data);
                break;
            case MySQLPacket.COM_STMT_RESET:
                commands.doStmtReset();
                source.stmtReset(data);
                break;
            case MySQLPacket.COM_STMT_EXECUTE:
                commands.doStmtExecute();
                source.stmtExecute(data);
                break;
            case MySQLPacket.COM_STMT_CLOSE:
                commands.doStmtClose();
                source.stmtClose(data);
                break;
            case MySQLPacket.COM_HEARTBEAT:
                commands.doHeartbeat();
                source.heartbeat(data);
                break;
            default:
                     commands.doOther();
                     source.writeErrMessage(ErrorCode.ER_UNKNOWN_COM_ERROR,
                             "Unknown command");

        }
    }

}

核心代码讲解:CommandCount ,命令执行统计,每调用一次,相关命令计数增加1。然后将命令包转发给前端连接来处理。我们就以查询命令COM_QUERY来处理,跟踪为FrontendConnection的 query方法。至于为什么这里是用switch(data[4]),这是sql命令协议包结构决定的,如果不明白请看:http://blog.csdn.net/prestigeding/article/details/70198164

2.1 FrontedConnection query( byte[] ) 方法详解

public void query(byte[] data) {

        // 取得语句
        String sql = null;        
        try {
            MySQLMessage mm = new MySQLMessage(data);
            mm.position(5);
            sql = mm.readString(charset);
        } catch (UnsupportedEncodingException e) {
            writeErrMessage(ErrorCode.ER_UNKNOWN_CHARACTER_SET, "Unknown charset '" + charset + "'");
            return;
        }      // @1    

        this.query( sql );    //@2
    }

代码@1,从COM_QUERY命令报文中解析出具体的SQL查询语句

代码@2,根据SQL进行查询。

接下来重点关注query( sql )的实现:

public void query(String sql) {

        if (sql == null || sql.length() == 0) {
            writeErrMessage(ErrorCode.ER_NOT_ALLOWED_COMMAND, "Empty SQL");
            return;
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(new StringBuilder().append(this).append(" ").append(sql).toString());
        }

        // remove last ';'
        if (sql.endsWith(";")) {
            sql = sql.substring(0, sql.length() - 1);
        }

        // 记录SQL
        this.setExecuteSql(sql);

        // 防火墙策略( SQL 黑名单/ 注入攻击)                                        // @1  start 
        if ( !privileges.checkFirewallSQLPolicy( user, sql ) ) {
            writeErrMessage(ErrorCode.ERR_WRONG_USED, 
                    "The statement is unsafe SQL, reject for user '" + user + "'");
            return;
        }        

        // DML 权限检查
        try {
            boolean isPassed = privileges.checkDmlPrivilege(user, schema, sql);
            if ( !isPassed ) {
                writeErrMessage(ErrorCode.ERR_WRONG_USED, 
                        "The statement DML privilege check is not passed, reject for user '" + user + "'");
                return;
            }
         } catch( com.alibaba.druid.sql.parser.ParserException e1) {
                writeErrMessage(ErrorCode.ERR_WRONG_USED,  e1.getMessage());
                LOGGER.error("parse exception", e1 );
                return;
         }                // @1 end

        // 执行查询
        if (queryHandler != null) {            // @2
            queryHandler.setReadOnly(privileges.isReadOnly(user));
            queryHandler.query(sql);

        } else {
            writeErrMessage(ErrorCode.ER_UNKNOWN_COM_ERROR, "Query unsupported!");
        }        
    }

该方法的实现,主要是先判断sql的合法性,然后判断该用户的执行权限,如果都没问题,交给QueryHandler去执行。

代码@1,用户权限检测,实现基于阿里开源的druid实现,在后续SQL解析专题会详细学习,目前不做详细解读。

代码@2,交给QueryHandler执行sql命令。

接下来将执行ServerQueryHandler.query方法,该方法,主要就是解析COM_QUERY的命令类型,比如SELECT语句亦或是USE语句等,本文以Select语句为例进行跟踪讲解,那select命令会被SelectHandler处理(工具类),最终会进入到ServerConnection的execute(String sql, int sqlType)

ServerConnection的execute(String sql, int sqlType)方法如下:

public void execute(String sql, int type) {
		// 连接状态检查
		if (this.isClosed()) {
			LOGGER.warn("ignore execute ,server connection is closed " + this);
			return;
		}
		// 事务状态检查
		if (txInterrupted) {
			writeErrMessage(ErrorCode.ER_YES, "Transaction error, need to rollback." + txInterrputMsg);
			return;
		}

		// 检查当前使用的DB        //@1
		String db = this.schema;
		boolean isDefault = true;
		if (db == null) {
			db = SchemaUtil.detectDefaultDb(sql, type);
			if (db == null) {
				writeErrMessage(ErrorCode.ERR_BAD_LOGICDB, "No MyCAT Database selected");
				return;
			}
			isDefault = false;
		}

                // @2 start 
		// 兼容PhpAdmin's, 支持对MySQL元数据的模拟返回
		//// TODO: 2016/5/20 支持更多information_schema特性
		if (ServerParse.SELECT == type && db.equalsIgnoreCase("information_schema")) {
			MysqlInformationSchemaHandler.handle(sql, this);
			return;
		}

		if (ServerParse.SELECT == type && sql.contains("mysql") && sql.contains("proc")) {

			SchemaUtil.SchemaInfo schemaInfo = SchemaUtil.parseSchema(sql);
			if (schemaInfo != null && "mysql".equalsIgnoreCase(schemaInfo.schema)
					&& "proc".equalsIgnoreCase(schemaInfo.table)) {

				// 兼容MySQLWorkbench
				MysqlProcHandler.handle(sql, this);
				return;
			}
		}

		SchemaConfig schema = MycatServer.getInstance().getConfig().getSchemas().get(db);
		if (schema == null) {
			writeErrMessage(ErrorCode.ERR_BAD_LOGICDB, "Unknown MyCAT Database '" + db + "'");
			return;
		}

		// fix navicat SELECT STATE AS `State`, ROUND(SUM(DURATION),7) AS
		// `Duration`, CONCAT(ROUND(SUM(DURATION)/*100,3), '%') AS `Percentage`
		// FROM INFORMATION_SCHEMA.PROFILING WHERE QUERY_ID= GROUP BY STATE
		// ORDER BY SEQ
		if (ServerParse.SELECT == type && sql.contains(" INFORMATION_SCHEMA.PROFILING ")
				&& sql.contains("CONCAT(ROUND(SUM(DURATION)/")) {
			InformationSchemaProfiling.response(this);
			return;
		}  //@2 end

		/*
		 * 当已经设置默认schema时,可以通过在sql中指定其它schema的方式执行 相关sql,已经在mysql客户端中验证。
		 * 所以在此处增加关于sql中指定Schema方式的支持。
		 */
		if (isDefault && schema.isCheckSQLSchema() && isNormalSql(type)) {
			SchemaUtil.SchemaInfo schemaInfo = SchemaUtil.parseSchema(sql);
			if (schemaInfo != null && schemaInfo.schema != null && !schemaInfo.schema.equals(db)) {
				SchemaConfig schemaConfig = MycatServer.getInstance().getConfig().getSchemas().get(schemaInfo.schema);
				if (schemaConfig != null)
					schema = schemaConfig;
			}
		}    //@3

		routeEndExecuteSQL(sql, type, schema);   //@4

	}

本文不试图详细分析每个步骤的具体实现,故只描述上面代码段的作用,详细的分析会以专题的形式讲解,比如路由解析,Schema解析等。

代码@1,解析schema。

代码@2,兼容各个客户端的数据报文格式,基于抓包工具,了解各客户端与mysql服务端的交互协议,从而编写适应性代码。

代码@3,对schema标签checkSQLschema属性的处理逻辑。

代码@4,路由并执行。继续跟踪代码@4,以便继续探究前端连接与后端连接的交互。

继续进入到ServerConnection的routeEndExecuteSQL方法:

public void routeEndExecuteSQL(String sql, int type, SchemaConfig schema) {
		// 路由计算
		RouteResultset rrs = null;
		try {
			rrs = MycatServer.getInstance().getRouterservice().route(MycatServer.getInstance().getConfig().getSystem(),
					schema, type, sql, this.charset, this);        //@1

		} catch (Exception e) {
			StringBuilder s = new StringBuilder();
			LOGGER.warn(s.append(this).append(sql).toString() + " err:" + e.toString(), e);
			String msg = e.getMessage();
			writeErrMessage(ErrorCode.ER_PARSE_ERROR, msg == null ? e.getClass().getSimpleName() : msg);
			return;
		}
		if (rrs != null) {
			// session执行
			session.execute(rrs, rrs.isSelectForUpdate() ? ServerParse.UPDATE : type);  //@2
		}
	}

计算路由,如果找到路由节点并执行着,从这里看出,接近我们的目标了。同样在该文中不会详细解读路由算法的计算,重在理解执行流程,这里将引出一个关键的对象session,见代码@2,那Session是何许人也呢?原来是NonBlockingSession对象。session初始化的地方在:

源码分析mycat1.6之网络篇----前后端连接交互设计(mycat命令处理流程)_第1张图片

也就是每一个前端连接,持有一NonBlockingSession对象。我们先关注该类一个重要的属性:

private final ConcurrentHashMap target = new ConcurrentHashMap(2, 0.75f);

首先一个NonBlockingSession持有一个前端连接(FrontedConnection,ServerConnection),然后在持有后端连接上,以每个路由节点(分片节点,datanode)为键,存放一个后端连接BackendConnection。

那就继续浏览session.execute方法源码:

@Override
    public void execute(RouteResultset rrs, int type) {

        // clear prev execute resources
        clearHandlesResources();
        if (LOGGER.isDebugEnabled()) {
            StringBuilder s = new StringBuilder();
            LOGGER.debug(s.append(source).append(rrs).toString() + " rrs ");
        }

        // 检查路由结果是否为空
        RouteResultsetNode[] nodes = rrs.getNodes();      // @1
        if (nodes == null || nodes.length == 0 || nodes[0].getName() == null || nodes[0].getName().equals("")) {
            source.writeErrMessage(ErrorCode.ER_NO_DB_ERROR,
                    "No dataNode found ,please check tables defined in schema:" + source.getSchema());
            return;
        }
        boolean autocommit = source.isAutocommit();
        final int initCount = target.size();     //@2
        if (nodes.length == 1) {         //@3
            singleNodeHandler = new SingleNodeHandler(rrs, this);
            if (this.isPrepared()) {
                singleNodeHandler.setPrepared(true);
            }

            try {
                if(initCount > 1){
                    checkDistriTransaxAndExecute(rrs,1,autocommit);
                }else{
                    singleNodeHandler.execute();     //@4
                }
            } catch (Exception e) {
                LOGGER.warn(new StringBuilder().append(source).append(rrs).toString(), e);
                source.writeErrMessage(ErrorCode.ERR_HANDLE_DATA, e.toString());
            }

        } else {

            multiNodeHandler = new MultiNodeQueryHandler(type, rrs, autocommit, this);
            if (this.isPrepared()) {
                multiNodeHandler.setPrepared(true);
            }
            try {
                if(((type == ServerParse.DELETE || type == ServerParse.INSERT || type == ServerParse.UPDATE) && !rrs.isGlobalTable() && nodes.length > 1)||initCount > 1) {
                    checkDistriTransaxAndExecute(rrs,2,autocommit);
                } else {
                    multiNodeHandler.execute();
                }
            } catch (Exception e) {
                LOGGER.warn(new StringBuilder().append(source).append(rrs).toString(), e);
                source.writeErrMessage(ErrorCode.ERR_HANDLE_DATA, e.toString());
            }
        }

        if (this.isPrepared()) {
            this.setPrepared(false);
        }
    }

我们以单节点路由信息为例来讲解与后端连接的关系,那代码的执行路劲为 @1 --》@2--》@3--》@4,进入到SingleNodeHandler的execute方法,该类是单节点的执行逻辑的抽象,持有路由分片信息RouteResultset与NonBlockingSession对象。

继续进入到SingleNodeHandler的execute方法中:

public void execute() throws Exception {
        startTime=System.currentTimeMillis();
        ServerConnection sc = session.getSource();
        this.isRunning = true;
        this.packetId = 0;
        final BackendConnection conn = session.getTarget(node);
        LOGGER.debug("rrs.getRunOnSlave() " + rrs.getRunOnSlave());
        node.setRunOnSlave(rrs.getRunOnSlave());    // 实现 master/slave注解
        LOGGER.debug("node.getRunOnSlave() " + node.getRunOnSlave());

        if (session.tryExistsCon(conn, node)) {  //@1
            _execute(conn);
        } else {   //@2
            // create new connection

            MycatConfig conf = MycatServer.getInstance().getConfig();

            LOGGER.debug("node.getRunOnSlave() " + node.getRunOnSlave());
            node.setRunOnSlave(rrs.getRunOnSlave());    // 实现 master/slave注解
            LOGGER.debug("node.getRunOnSlave() " + node.getRunOnSlave());

            PhysicalDBNode dn = conf.getDataNodes().get(node.getName());
            dn.getConnection(dn.getDatabase(), sc.isAutocommit(), node, this, node);
        }

    }

如果已经有后台连接了,就直接用后台连接执行,否则,从后端连接池中获取一个连接,这里将引出后端连接中一个重要的类:PhysicalDBNode,其getConnection为获取连接的核心实现。

如果有已经存在连接,执行SingleNodeHandler的_execute方法:

private void _execute(BackendConnection conn) {
        if (session.closed()) {
            endRunning();
            session.clearResources(true);
            return;
        }
        conn.setResponseHandler(this);    //@1
        try {
            conn.execute(node, session.getSource(), session.getSource().isAutocommit());    //@2
        } catch (Exception e1) {
            executeException(conn, e1);
            return;
        }
    }

代码@1,为后端连接设置ResponseHandler,在后端连接收到后端服务器的响应报文后,会交给该RersponseHandler,完成从后端连接到前端连接数据的返回。

代码@2,执行BackendConnection 的execute方法,完成与后端服务器的命令执行。

重点关注:BackendConnection的execute方法,该方法的职责肯定是按照mysql通信协议命令请求报文,发送到后端服务器。该方法有涉及到分布式事务的处理(XA事务的实现)

public void execute(RouteResultsetNode rrn, ServerConnection sc,
            boolean autocommit) throws UnsupportedEncodingException {
        if (!modifiedSQLExecuted && rrn.isModifySQL()) {
            modifiedSQLExecuted = true;
        }
        String xaTXID = sc.getSession2().getXaTXID();
        synAndDoExecute(xaTXID, rrn, sc.getCharsetIndex(), sc.getTxIsolation(),
                autocommit);
    }

    private void synAndDoExecute(String xaTxID, RouteResultsetNode rrn,
            int clientCharSetIndex, int clientTxIsoLation,
            boolean clientAutoCommit) {
        String xaCmd = null;

        boolean conAutoComit = this.autocommit;
        String conSchema = this.schema;
        // never executed modify sql,so auto commit
        boolean expectAutocommit = !modifiedSQLExecuted || isFromSlaveDB()
                || clientAutoCommit;
        if (expectAutocommit == false && xaTxID != null && xaStatus == TxState.TX_INITIALIZE_STATE) {
            //clientTxIsoLation = Isolations.SERIALIZABLE;
            xaCmd = "XA START " + xaTxID + ';';
            this.xaStatus = TxState.TX_STARTED_STATE;
        }
        int schemaSyn = conSchema.equals(oldSchema) ? 0 : 1;
        int charsetSyn = 0;
        if (this.charsetIndex != clientCharSetIndex) {
            //need to syn the charset of connection.
            //set current connection charset to client charset.
            //otherwise while sending commend to server the charset will not coincidence.
            setCharset(CharsetUtil.getCharset(clientCharSetIndex));
            charsetSyn = 1;
        }
        int txIsoLationSyn = (txIsolation == clientTxIsoLation) ? 0 : 1;
        int autoCommitSyn = (conAutoComit == expectAutocommit) ? 0 : 1;
        int synCount = schemaSyn + charsetSyn + txIsoLationSyn + autoCommitSyn;
        if (synCount == 0 && this.xaStatus != TxState.TX_STARTED_STATE) {
            // not need syn connection
            sendQueryCmd(rrn.getStatement());
            return;
        }
        CommandPacket schemaCmd = null;
        StringBuilder sb = new StringBuilder();
        if (schemaSyn == 1) {
            schemaCmd = getChangeSchemaCommand(conSchema);
            // getChangeSchemaCommand(sb, conSchema);
        }

        if (charsetSyn == 1) {
            getCharsetCommand(sb, clientCharSetIndex);
        }
        if (txIsoLationSyn == 1) {
            getTxIsolationCommand(sb, clientTxIsoLation);
        }
        if (autoCommitSyn == 1) {
            getAutocommitCommand(sb, expectAutocommit);
        }
        if (xaCmd != null) {
            sb.append(xaCmd);
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("con need syn ,total syn cmd " + synCount
                    + " commands " + sb.toString() + "schema change:"
                    + (schemaCmd != null) + " con:" + this);
        }
        metaDataSyned = false;
        statusSync = new StatusSync(xaCmd != null, conSchema,
                clientCharSetIndex, clientTxIsoLation, expectAutocommit,
                synCount);
        // syn schema
        if (schemaCmd != null) {
            schemaCmd.write(this);
        }
        // and our query sql to multi command at last
        sb.append(rrn.getStatement()+";");
        // syn and execute others
        this.sendQueryCmd(sb.toString());
        // waiting syn result...

    }

加上事务的处理等,最终执行this.sendQueryCmd,这一路走来,沿途有好多风景,后续会详细解读,比如schema解析,路由、后端连接获取、分布是XA事务等,然后进入到sendQueryCmd方法:

protected void sendQueryCmd(String query) {
        CommandPacket packet = new CommandPacket();
        packet.packetId = 0;
        packet.command = MySQLPacket.COM_QUERY;
        try {
            packet.arg = query.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        lastTime = TimeUtil.currentTimeMillis();
        packet.write(this);
    }

最后将查询命令按照mysql通信协议发送到服务端。最终会调用后端连接的MysqlConnection的write方法,具体实现见:

@Override
    public final void write(ByteBuffer buffer) {

        if (isSupportCompress()) {
            ByteBuffer newBuffer = CompressUtil.compressMysqlPacket(buffer, this, compressUnfinishedDataQueue);
            writeQueue.offer(newBuffer);
        } else {
            writeQueue.offer(buffer);
        }

        // if ansyn write finishe event got lock before me ,then writing
        // flag is set false but not start a write request
        // so we check again
        try {
            this.socketWR.doNextWriteCheck();
        } catch (Exception e) {
            LOGGER.warn("write err:", e);
            this.close("write err:" + e);
        }
    }

到这里为止,就完成了一条客户端查询命令经过层层关卡到到了后端连接,并发送给了后端服务器。

那还剩一个问题,前端基于主从Reactor模型完成与客户端的请求处理,那mycat与后端mysql服务器是怎么处理请求响应的呢?

要明白这个问题,请看第三部分,后端连接建立已经IO线程模型。

3、后端连接建立以及IO线程模型

从上文建立连接的地方见SingelNodeHandler.execute方法中,如果连接未创建,则调用PhysicalDBNode的getConnection方法:

SingleNodeHandler.execute方法:

 PhysicalDBNode dn = conf.getDataNodes().get(node.getName());            

dn.getConnection(dn.getDatabase(), sc.isAutocommit(), node, this, node);

3.1 源码分析PhysicalDBNode的getConnection方法

public void getConnection(String schema,boolean autoCommit, RouteResultsetNode rrs,
                            ResponseHandler handler, Object attachment) throws Exception {  //@1
        checkRequest(schema);    //@2
        if (dbPool.isInitSuccess()) {
            LOGGER.debug("rrs.getRunOnSlave() " + rrs.getRunOnSlave());
            if(rrs.getRunOnSlave() != null){        // 带有 /*db_type=master/slave*/ 注解      //@3
                // 强制走 slave
                if(rrs.getRunOnSlave()){            
                    LOGGER.debug("rrs.isHasBlanceFlag() " + rrs.isHasBlanceFlag());
                    if (rrs.isHasBlanceFlag()) {        // 带有 /*balance*/ 注解(目前好像只支持一个注解...)
                        dbPool.getReadBanlanceCon(schema,autoCommit,handler, attachment, this.database);
                    }else{    // 没有 /*balance*/ 注解
                        LOGGER.debug("rrs.isHasBlanceFlag()" + rrs.isHasBlanceFlag());
                        if(!dbPool.getReadCon(schema, autoCommit, handler, attachment, this.database)){
                            LOGGER.warn("Do not have slave connection to use, use master connection instead.");
                            PhysicalDatasource writeSource=dbPool.getSource();
                            //记录写节点写负载值
                            writeSource.setWriteCount();
                            writeSource.getConnection(schema,
                                    autoCommit, handler, attachment);
                            rrs.setRunOnSlave(false);
                            rrs.setCanRunInReadDB(false);
                        }
                    }
                }else{    // 强制走 master
                    // 默认获得的是 writeSource,也就是 走master
                    LOGGER.debug("rrs.getRunOnSlave() " + rrs.getRunOnSlave());
                    PhysicalDatasource writeSource=dbPool.getSource();
                    //记录写节点写负载值
                    writeSource.setReadCount();
                    writeSource.getConnection(schema, autoCommit,
                            handler, attachment);
                    rrs.setCanRunInReadDB(false);
                }
            }else{    // 没有  /*db_type=master/slave*/ 注解,按照原来的处理方式
                LOGGER.debug("rrs.getRunOnSlave() " + rrs.getRunOnSlave());    // null
                if (rrs.canRunnINReadDB(autoCommit)) {
                    dbPool.getRWBanlanceCon(schema,autoCommit, handler, attachment, this.database);
                } else {
                    PhysicalDatasource writeSource =dbPool.getSource();
                    //记录写节点写负载值
                    writeSource.setWriteCount();
                    writeSource.getConnection(schema, autoCommit,
                            handler, attachment);
                }
            }

        } else {
            throw new IllegalArgumentException("Invalid DataSource:" + dbPool.getActivedIndex());
            }
        }

代码@1,参数说明:

  • String schema : 当前所在的mycat逻辑schema(数据库)
  • boolean autoCommit : 是否自动提交
  • RouteResultsetNode node:路由节点信息,代表一个datanode。

ResponseHandler handler:后端连接发送请求给后端mysql,返回结果后的处理hanlder,这里是SingleNodeHandler,如果是多节点处理的话,那就是MultiNodeHandler。

创建新的连接,在如下两个时机,一个是第一次初始化后端连接池,一次当初始化的连接数不够用的时候,需要再次创建

初次初始化连接池见:代码@2,第二个见PhysicalDatasource.getConnection中,如果连接池中的连接不够用的时候,会创建新的连接。

代码@3,这里涉及到读写分离注解的处理逻辑,本文不做详细解读,在后面的专题研究再做讲解。

我们就以PhysicalDatasource.getConnection为入口,继续跟踪连接的创建过程:

public void getConnection(String schema, boolean autocommit,
            final ResponseHandler handler, final Object attachment)
            throws IOException {

        // 从当前连接map中拿取已建立好的后端连接
        BackendConnection con = this.conMap.tryTakeCon(schema, autocommit);   //  @1
        if (con != null) {

            //如果不为空,则绑定对应前端请求的handler
            takeCon(con, handler, attachment, schema);    //@2
            return;    

        } else {
            int activeCons = this.getActiveCount();// 当前最大活动连接
            if (activeCons + 1 > size) {// 下一个连接大于最大连接数
                LOGGER.error("the max activeConnnections size can not be max than maxconnections");
                throw new IOException("the max activeConnnections size can not be max than maxconnections");
            } else { // create connection
                LOGGER.info("no ilde connection in pool,create new connection for "    + this.name + " of schema " + schema);
                createNewConnection(handler, attachment, schema);   //@3
            }
        }
    }

代码@1:从PhysicalDatasource的map(连接池)中尝试获取一个连接。

代码@2:如果成功获取该连接,使用它并设置ResponseHandler等。

代码@3:调用createNewConnection创建一个新的连接

private void createNewConnection(final ResponseHandler handler,
			final Object attachment, final String schema) throws IOException {		
		// aysn create connection
		MycatServer.getInstance().getBusinessExecutor().execute(new Runnable() {
			public void run() {
				try {
					createNewConnection(new DelegateResponseHandler(handler) {    //@1
						@Override
						public void connectionError(Throwable e, BackendConnection conn) {
							handler.connectionError(e, conn);
						}

						@Override
						public void connectionAcquired(BackendConnection conn) {
							takeCon(conn, handler, attachment, schema);
						}
					}, schema);
				} catch (IOException e) {
					handler.connectionError(e, null);
				}
			}
		});
	}

在业务线程池中异步执行创建连接并绑定前端命令执行中。不得不说,这是mycat追求更快响应速度的一个优化。

createNewConnection(new DelegateResponseHandler(handler) ,该方法在PhysicalDatasouce中是一个抽象方法:

源码分析mycat1.6之网络篇----前后端连接交互设计(mycat命令处理流程)_第2张图片

我们关注MysqlDataSource实现类。

private final MySQLConnectionFactory factory;

	public MySQLDataSource(DBHostConfig config, DataHostConfig hostConfig,
			boolean isReadNode) {
		super(config, hostConfig, isReadNode);
		this.factory = new MySQLConnectionFactory();

	}

	@Override
	public void createNewConnection(ResponseHandler handler,String schema) throws IOException {
		factory.make(this, handler,schema);
	}

主要调用MySQLDataSourceFactory的make方法。

public class MySQLConnectionFactory extends BackendConnectionFactory {
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public MySQLConnection make(MySQLDataSource pool, ResponseHandler handler,
			String schema) throws IOException {

		DBHostConfig dsc = pool.getConfig();
		NetworkChannel channel = openSocketChannel(MycatServer.getInstance()
				.isAIO());   // @1

		MySQLConnection c = new MySQLConnection(channel, pool.isReadNode());
		MycatServer.getInstance().getConfig().setSocketParams(c, false);
		c.setHost(dsc.getIp());
		c.setPort(dsc.getPort());
		c.setUser(dsc.getUser());
		c.setPassword(dsc.getPassword());
		c.setSchema(schema);
		c.setHandler(new MySQLConnectionAuthenticator(c, handler));   // @2
		c.setPool(pool);
		c.setIdleTimeout(pool.getConfig().getIdleTimeout());
		if (channel instanceof AsynchronousSocketChannel) {
			((AsynchronousSocketChannel) channel).connect(
					new InetSocketAddress(dsc.getIp(), dsc.getPort()), c,
					(CompletionHandler) MycatServer.getInstance()
							.getConnector());
		} else {
			((NIOConnector) MycatServer.getInstance().getConnector())
					.postConnect(c); // @3

		}
		return c;
	}

}

代码@1,创建SocketChannel,用以与后端真实的mysql服务器连接,在这里并没有执行connect方法。

代码@2,设置handler,这里与前端连接一样,先设置用来登录授权的包,与后端服务器完成握手认证阶段。

代码@3,关键中的关键,将该connection(通道SocketChannel)放入到连接反应堆(主Reactor)中,从这里看出,后端的主Reactor由NIOConnector来担任。由此整个后端连接就创建完成,与后端连接相关的核心类归纳如下:

  • PhysicalDBNode :代表一个datanode
  • PhysicalDBPool : 代表一个writeHost节点(一个writerHost和0或多个readerhost)
  • PhysicalDatasource : 一个PhysicalDBNode的一个数据库连接池,管理后端具体的BackendConnection。

BackenConnection : 后端连接的父类。

接下来再看一下后端连接的线程模型,NioConnector的实现就是典型的NIO Client的实现。

3.2.1 重要属性说明:

public static final ConnectIdGenerator ID_GENERATOR = new ConnectIdGenerator();

private final String name;

private final Selector selector;

private final BlockingQueue connectQueue;

private long connectCount;

private final NIOReactorPool reactorPool;
  • name : NioConnector反应堆的名称
  • connectQueue:所有后端connection集合
  • reactorPool : 从Reactor,子反应堆,从mycat的启动类我们发现,此反应堆与前的连接的从反应堆共用,也就是不管是前端连接的读写事件,还是后端连接的读写事件都是在从反应堆中处理。

3.2.2 postConnect方法详解

public void postConnect(AbstractConnection c) {

connectQueue.offer(c);

selector.wakeup();

}

3.2.3 run方法详解

@Override
	public void run() {
		final Selector tSelector = this.selector;
		for (;;) {
			++connectCount;
			try {
			    tSelector.select(1000L);
				connect(tSelector);
				Set keys = tSelector.selectedKeys();
				try {
					for (SelectionKey key : keys) {
						Object att = key.attachment();
						if (att != null && key.isValid() && key.isConnectable()) {
							finishConnect(key, att);   // @1 
						} else {
							key.cancel();
						}
					}
				} finally {
					keys.clear();
				}
			} catch (Exception e) {
				LOGGER.warn(name, e);
			}
		}
	}

重点在代码@1,完成连接方法:

private void finishConnect(SelectionKey key, Object att) {
		BackendAIOConnection c = (BackendAIOConnection) att;
		try {
			if (finishConnect(c, (SocketChannel) c.channel)) {     //@1
				clearSelectionKey(key);
				c.setId(ID_GENERATOR.getId());
				NIOProcessor processor = MycatServer.getInstance()
						.nextProcessor();  
				c.setProcessor(processor);  // @2
				NIOReactor reactor = reactorPool.getNextReactor();
				reactor.postRegister(c);    //@3
				c.onConnectfinish();   // @4
			}
		} catch (Exception e) {
			clearSelectionKey(key);
			LOGGER.error("error:",e);
            c.close(e.toString());
			c.onConnectFailed(e);

		}
	}

private boolean finishConnect(AbstractConnection c, SocketChannel channel)
			throws IOException {
		if (channel.isConnectionPending()) {
			channel.finishConnect();

			c.setLocalPort(channel.socket().getLocalPort());
			return true;
		} else {
			return false;
		}
	}

代码@1:确保SocketChannel完成连接

代码@2:设置NioProccsor,该类我想在分析内存池实现时详解。

代码@3:转发到从Reactor,处理读写事件

代码@4,成功连接的事件处理方法。

本文重点阐述了mycat从认证授权模式如何切换到命令执行模式,前端连接接受命令是如何到达后端连接的,以及后端连接的线程模型,沿途风景靓丽哦,包括SQL语句类型解析、Schema解析、路由计算、分布式事务、读写分离的各个入口。

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