本章将从如下3个方面剖析mycat前后端交互原理:
1、mycat握手认证阶段、命令处理阶段切换
2、mycat前后端如何交互
3、后端连接及IO线程模型。
从上篇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只处理了是否支持压缩。
上面已经剖析了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
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初始化的地方在:
也就是每一个前端连接,持有一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线程模型。
从上文建立连接的地方见SingelNodeHandler.execute方法中,如果连接未创建,则调用PhysicalDBNode的getConnection方法:
SingleNodeHandler.execute方法:
PhysicalDBNode dn = conf.getDataNodes().get(node.getName());
dn.getConnection(dn.getDatabase(), sc.isAutocommit(), node, this, node);
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,参数说明:
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中是一个抽象方法:
我们关注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来担任。由此整个后端连接就创建完成,与后端连接相关的核心类归纳如下:
BackenConnection : 后端连接的父类。
接下来再看一下后端连接的线程模型,NioConnector的实现就是典型的NIO Client的实现。
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;
public void postConnect(AbstractConnection c) {
connectQueue.offer(c);
selector.wakeup();
}
@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解析、路由计算、分布式事务、读写分离的各个入口。