MyCat源码分析系列之——结果合并
更多MyCat源码分析,请戳MyCat源码分析系列
结果合并
在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实例,在MySQL服务端返回执行结果时会调用到MySQLConnecionHandler.handleData(),用于不同类型的处理派发:
protected void handleData(byte[] data) {
switch (resultStatus) {
case RESULT_STATUS_INIT:
switch (data[4]) {
case OkPacket.FIELD_COUNT:
handleOkPacket(data);
break;
case ErrorPacket.FIELD_COUNT:
handleErrorPacket(data);
break;
case RequestFilePacket.FIELD_COUNT:
handleRequestPacket(data);
break;
default:
resultStatus = RESULT_STATUS_HEADER;
header = data;
fields = new ArrayList<byte[]>((int) ByteUtil.readLength(data,
4));
}
break;
case RESULT_STATUS_HEADER:
switch (data[4]) {
case ErrorPacket.FIELD_COUNT:
resultStatus = RESULT_STATUS_INIT;
handleErrorPacket(data);
break;
case EOFPacket.FIELD_COUNT:
resultStatus = RESULT_STATUS_FIELD_EOF;
handleFieldEofPacket(data);
break;
default:
fields.add(data);
}
break;
case RESULT_STATUS_FIELD_EOF:
switch (data[4]) {
case ErrorPacket.FIELD_COUNT:
resultStatus = RESULT_STATUS_INIT;
handleErrorPacket(data);
break;
case EOFPacket.FIELD_COUNT:
resultStatus = RESULT_STATUS_INIT;
handleRowEofPacket(data);
break;
default:
handleRowPacket(data);
}
break;
default:
throw new RuntimeException("unknown status!");
}
}
上述代码片段中用红色标注的几个方法是最为核心的,其中handleOkPacket()主要用于insert/update/delete和其余返回OK包的语句返回的执行结果,而handleFieldEofPacket()、handleRowPacket()和handleRowEofPacket()用于select语句返回的执行结果。这几个方法的内部的流程其实就是分别调用了其上绑定的ResponseHandler(SingleNodeHandler或MultiNodeQueryHandler)实例对应的这几个方法。
1. 先来看单节点操作的情况,SingleNodeHandler包含的这几个方法实现如下:
public void okResponse(byte[] data, BackendConnection conn) {
boolean executeResponse = conn.syncAndExcute();
if (executeResponse) {
session.releaseConnectionIfSafe(conn, LOGGER.isDebugEnabled(),
false);
endRunning();
ServerConnection source = session.getSource();
OkPacket ok = new OkPacket();
ok.read(data);
if (rrs.isLoadData()) {
byte lastPackId = source.getLoadDataInfileHandler()
.getLastPackId();
ok.packetId = ++lastPackId;// OK_PACKET
source.getLoadDataInfileHandler().clear();
} else {
ok.packetId = ++packetId;// OK_PACKET
}
ok.serverStatus = source.isAutocommit() ? 2 : 1; recycleResources();
source.setLastInsertId(ok.insertId);
ok.write(source); //TODO: add by zhuam
//查询结果派发
QueryResult queryResult = new QueryResult(session.getSource().getUser(),
rrs.getSqlType(), rrs.getStatement(), startTime);
QueryResultDispatcher.dispatchQuery( queryResult ); }
} public void fieldEofResponse(byte[] header, List<byte[]> fields,
byte[] eof, BackendConnection conn) { //TODO: add by zhuam
//查询结果派发
QueryResult queryResult = new QueryResult(session.getSource().getUser(),
rrs.getSqlType(), rrs.getStatement(), startTime);
QueryResultDispatcher.dispatchQuery( queryResult ); header[3] = ++packetId;
ServerConnection source = session.getSource();
buffer = source.writeToBuffer(header, allocBuffer());
for (int i = 0, len = fields.size(); i < len; ++i)
{
byte[] field = fields.get(i);
field[3] = ++packetId;
buffer = source.writeToBuffer(field, buffer);
}
eof[3] = ++packetId;
buffer = source.writeToBuffer(eof, buffer); if (isDefaultNodeShowTable) {
for (String name : shardingTablesSet) {
RowDataPacket row = new RowDataPacket(1);
row.add(StringUtil.encode(name.toLowerCase(), source.getCharset()));
row.packetId = ++packetId;
buffer = row.write(buffer, source, true);
}
}
} public void rowResponse(byte[] row, BackendConnection conn) {
if(isDefaultNodeShowTable)
{
RowDataPacket rowDataPacket =new RowDataPacket(1);
rowDataPacket.read(row);
String table= StringUtil.decode(rowDataPacket.fieldValues.get(0),conn.getCharset());
if(shardingTablesSet.contains(table.toUpperCase())) return;
} row[3] = ++packetId;
buffer = session.getSource().writeToBuffer(row, allocBuffer());
} public void rowEofResponse(byte[] eof, BackendConnection conn) {
ServerConnection source = session.getSource();
conn.recordSql(source.getHost(), source.getSchema(),
node.getStatement()); // 判断是调用存储过程的话不能在这里释放链接
if (!rrs.isCallStatement()) {
session.releaseConnectionIfSafe(conn, LOGGER.isDebugEnabled(),
false);
endRunning();
} eof[3] = ++packetId;
buffer = source.writeToBuffer(eof, allocBuffer());
source.write(buffer);
}
在okResponse()方法中,首先调用了conn.syncAndExcute(),这个过程就解释了之前在SQL下发过程中提及的当某个连接现有的设置需要修改时并未等待这些修改成功返回,这儿才对此做出了判断:
public boolean syncAndExcute() {
StatusSync sync = this.statusSync;
if (sync == null) {
return true;
} else {
boolean executed = sync.synAndExecuted(this);
if (executed) {
statusSync = null;
}
return executed;
}
}
这里面又进一步依次调用了StatusSync.synAndExecuted()和updateConnectionInfo()方法:
public boolean synAndExecuted(MySQLConnection conn) {
int remains = synCmdCount.decrementAndGet();
if (remains == 0) {// syn command finished
this.updateConnectionInfo(conn);
conn.metaDataSyned = true;
return false;
} else if (remains < 0) {
return true;
}
return false;
} private void updateConnectionInfo(MySQLConnection conn)
{
conn.xaStatus = (xaStarted == true) ? 1 : 0;
if (schema != null) {
conn.schema = schema;
conn.oldSchema = conn.schema;
}
if (charsetIndex != null) {
conn.setCharset(CharsetUtil.getCharset(charsetIndex));
}
if (txtIsolation != null) {
conn.txIsolation = txtIsolation;
}
if (autocommit != null) {
conn.autocommit = autocommit;
}
}
假如当前的连接与所需连接在数据库名和字符集上存在不同,那需同步的数量为2,如果这两个修改都成功,那应该分别返回2个OK包(即触发两次SingleNodeHandler.okResponse()),在synAndExecuted()中通过对于收到的OK包数量synCmdCount进行判断,若全部收到则调用updateConnectionInfo(),将该连接的这些设置都设置为新值。该同步过程完成之后,才会真正进入到SQL语句执行返回的结果处理阶段(select/insert/update/delete)。
1.1 insert/update/delete
- okResponse():读取data字节数组,组成一个OKPacket,并调用ok.write(source)将结果写入前端连接FrontendConnection的写缓冲队列writeQueue中,真正发送给应用是由对应的NIOSocketWR从写队列中读取ByteBuffer并返回的。
1.2 select
- fieldEofResponse():元数据返回时触发,将header和元数据内容依次写入缓冲区中;
- rowResponse():行数据返回时触发,将行数据写入缓冲区中;
- rowEofResponse():行结束标志返回时触发,将EOF标志写入缓冲区,最后调用source.write(buffer)将缓冲区放入前端连接的写缓冲队列中,等待NIOSocketWR将其发送给应用。
2. 再来看多节点操作的结果合并和返回过程,MultiNodeQueryHandler负责这一过程的执行。
多节点操作和单节点操作的不同之处就在于:
1)接收来自多个MySQL节点各自发送的结果数据,可能需要对所有得到的结果进行简单的合并(顺序是不确定的,满足FIFO);
2)如果本次操作涉及聚合函数、group by、order by和limit,还需要对所有结果进行一系列归并
针对第一种情况,insert/update/delete和select的区别如下:
- insert/update/delete:这三类语句都会返回一个OK包,里面包含了最为核心的affectedRows,因此每得到一个MySQL节点发送回的affectedRows,就将其累加,当收到最后一个节点的数据后(通过decrementOkCountBy()方法判断),将结果返回给前端;
- select:每一个MySQL节点都会依次返回元数据、行数据1、行数据2...、行数据n、行结束标志位(EOF),其中元数据和行结束标志位每个节点返回上来都是一样的,但MultiNodeQueryHandler负责合并所有数据,因此实际只需要一份元数据、所有节点的行数据以及一份行结束标志位,所以在fieldEofResponse()方法中通过boolean类型的fieldsReturned判断获取第一份收到的元数据包,后续的统统丢弃,并在rowEofResponse()方法中通过decrementCountBy()方法判断是否收到了所有节点的EOF包,将最后一份的EOF写入缓冲区返回前端。
针对第二种情况,insert/update/delete与第一种情况完全一致,但select的处理由MultiNodeQueryHandler上包含的DataMergeService实例负责归并结果集(limit其实是在MultiNodeQueryHandler中实现的),我们先来看DataMergeService中包含的核心变量:
private int fieldCount; // 字段数
private RouteResultset rrs; // 路由信息
private RowDataSorter sorter; // 排序器
private RowDataPacketGrouper grouper; // 分组器
private volatile boolean hasOrderBy = false;
private MultiNodeQueryHandler multiQueryHandler;
public PackWraper END_FLAG_PACK = new PackWraper(); // 结束包
private AtomicInteger areadyAdd = new AtomicInteger();
private List<RowDataPacket> result = new Vector<RowDataPacket>(); // 归并结果队列
private static Logger LOGGER = Logger.getLogger(DataMergeService.class);
private BlockingQueue<PackWraper> packs = new LinkedBlockingQueue<PackWraper>(); // 原始数据封装队列
private ConcurrentHashMap<String, Boolean> canDiscard = new ConcurrentHashMap<String, Boolean>(); // 是否可丢弃标志位map
其中,最为重要的就是用于排序的sorter和用于分组的grouper,以及最后存放归并结果的行数据包队列result,DataMergeService中核心的方法实现如下:
public void onRowMetaData(Map<String, ColMeta> columToIndx, int fieldCount) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("field metadata inf:" + columToIndx.entrySet());
}
int[] groupColumnIndexs = null;
this.fieldCount = fieldCount;
if (rrs.getGroupByCols() != null) {
groupColumnIndexs = toColumnIndex(rrs.getGroupByCols(), columToIndx);
} if (rrs.getHavingCols() != null) {
ColMeta colMeta = columToIndx.get(rrs.getHavingCols().getLeft()
.toUpperCase());
if (colMeta != null) {
rrs.getHavingCols().setColMeta(colMeta);
}
} if (rrs.isHasAggrColumn()) {
List<MergeCol> mergCols = new LinkedList<MergeCol>();
Map<String, Integer> mergeColsMap = rrs.getMergeCols();
if (mergeColsMap != null) {
for (Map.Entry<String, Integer> mergEntry : mergeColsMap
.entrySet()) {
String colName = mergEntry.getKey().toUpperCase();
int type = mergEntry.getValue();
if (MergeCol.MERGE_AVG == type) {
ColMeta sumColMeta = columToIndx.get(colName + "SUM");
ColMeta countColMeta = columToIndx.get(colName
+ "COUNT");
if (sumColMeta != null && countColMeta != null) {
ColMeta colMeta = new ColMeta(sumColMeta.colIndex,
countColMeta.colIndex,
sumColMeta.getColType());
mergCols.add(new MergeCol(colMeta, mergEntry
.getValue()));
}
} else {
ColMeta colMeta = columToIndx.get(colName);
mergCols.add(new MergeCol(colMeta, mergEntry.getValue()));
}
}
}
// add no alias merg column
for (Map.Entry<String, ColMeta> fieldEntry : columToIndx.entrySet()) {
String colName = fieldEntry.getKey();
int result = MergeCol.tryParseAggCol(colName);
if (result != MergeCol.MERGE_UNSUPPORT
&& result != MergeCol.MERGE_NOMERGE) {
mergCols.add(new MergeCol(fieldEntry.getValue(), result));
}
}
grouper = new RowDataPacketGrouper(groupColumnIndexs,
mergCols.toArray(new MergeCol[mergCols.size()]),
rrs.getHavingCols());
}
if (rrs.getOrderByCols() != null) {
LinkedHashMap<String, Integer> orders = rrs.getOrderByCols();
OrderCol[] orderCols = new OrderCol[orders.size()];
int i = 0;
for (Map.Entry<String, Integer> entry : orders.entrySet()) {
String key = StringUtil.removeBackquote(entry.getKey()
.toUpperCase());
ColMeta colMeta = columToIndx.get(key);
if (colMeta == null) {
throw new java.lang.IllegalArgumentException(
"all columns in order by clause should be in the selected column list!"
+ entry.getKey());
}
orderCols[i++] = new OrderCol(colMeta, entry.getValue());
}
// sorter = new RowDataPacketSorter(orderCols);
RowDataSorter tmp = new RowDataSorter(orderCols);
tmp.setLimit(rrs.getLimitStart(), rrs.getLimitSize());
hasOrderBy = true;
sorter = tmp;
} else {
hasOrderBy = false;
}
MycatServer.getInstance().getBusinessExecutor().execute(this);
} public boolean onNewRecord(String dataNode, byte[] rowData) {
// 对于需要排序的数据,由于mysql传递过来的数据是有序的,
// 如果某个节点的当前数据已经不会进入,后续的数据也不会入堆
if (canDiscard.size() == rrs.getNodes().length) {
LOGGER.error("now we output to client");
packs.add(END_FLAG_PACK);
return true;
}
if (canDiscard.get(dataNode) != null) {
return true;
}
PackWraper data = new PackWraper();
data.node = dataNode;
data.data = rowData;
packs.add(data);
areadyAdd.getAndIncrement();
return false;
} public void run() {
int warningCount = 0;
EOFPacket eofp = new EOFPacket();
ByteBuffer eof = ByteBuffer.allocate(9);
BufferUtil.writeUB3(eof, eofp.calcPacketSize());
eof.put(eofp.packetId);
eof.put(eofp.fieldCount);
BufferUtil.writeUB2(eof, warningCount);
BufferUtil.writeUB2(eof, eofp.status);
ServerConnection source = multiQueryHandler.getSession().getSource(); while (!Thread.interrupted()) {
try {
PackWraper pack = packs.take();
if (pack == END_FLAG_PACK) {
break;
}
RowDataPacket row = new RowDataPacket(fieldCount);
row.read(pack.data);
if (grouper != null) {
grouper.addRow(row);
} else if (sorter != null) {
if (!sorter.addRow(row)) {
canDiscard.put(pack.node, true);
}
} else {
result.add(row);
}
} catch (Exception e) {
LOGGER.error("Merge multi data error", e);
}
}
byte[] array = eof.array();
multiQueryHandler.outputMergeResult(source, array, getResults(array));
} private List<RowDataPacket> getResults(byte[] eof) {
List<RowDataPacket> tmpResult = result;
if (this.grouper != null) {
tmpResult = grouper.getResult();
grouper = null;
}
if (sorter != null) {
// 处理grouper处理后的数据
if (tmpResult != null) {
Iterator<RowDataPacket> itor = tmpResult.iterator();
while (itor.hasNext()) {
sorter.addRow(itor.next());
itor.remove();
}
}
tmpResult = sorter.getSortedResult();
sorter = null;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("prepare mpp merge result for " + rrs.getStatement());
} return tmpResult;
}
接下来对这几个方法逐一解释:
- onRowMetaData():在MultiNodeQueryHandler.fieldEofResponse()中被调用,初始化grouper和sorter,随后利用线程池调用run()方法;
- onNewRecord():在MultiNodeQueryHandler.rowResponse()中被调用,首先判断canDiscard的长度是否等于下发的节点个数,如果是说明后续所有节点的数据都会被丢弃,往packs中放入END_FLAG_PACK终止run()中的循环(另外一处更为常规的终止循环是由MultiNodeQueryHandler.rowEofResponse()中收到全部节点的EOF包后触发的),如果当前节点在canDiscard队列中也同样忽略该节点的后续数据,随后将节点名和行数据封装成一个PackWraper实例,放入packs中;
- run():核心是一个循环,每次阻塞式地从packs中读取一个PackWraper实例并生成RowDataPacket实例,如果发现是END_FLAG_PACK就退出,接下来进行if-else判断:
1)如果需要分组则调用grouper.addRow()添加该行,由于分组是优先于排序的,因此一旦有分组需求,那排序就必须等到所有分组行为完成后才能开始(getResults()中);
2)反之,如果需要排序则调用sorter.addRow()尝试添加该行,若加入不成功说明该节点后续的数据都不可能加入成功(因为这里的排序是通过构建最大堆MaxHeap实现的,堆一旦满了就会执行元素淘汰,并且每个节点返回的数据又满足内部有序),将该节点放入canDiscard中忽略后续数据;
3)反之,直接将该行加入result中。
当循环退出时,调用MultiNodeQueryHandler.outputMergeResult(),其中会先调用getResults()获取分组数据/分组排序数据/排序数据/普通数据,MultiNodeQueryHandler.outputMergeResult()就是为了执行limit,随后将处理好的结果集依次写入缓冲区,最后返回前端,具体实现如下:
public void outputMergeResult(final ServerConnection source,
final byte[] eof, List<RowDataPacket> results) {
try {
lock.lock();
ByteBuffer buffer = session.getSource().allocate();
final RouteResultset rrs = this.dataMergeSvr.getRrs(); // 处理limit语句
int start = rrs.getLimitStart();
int end = start + rrs.getLimitSize(); if (start < 0)
start = 0; if (rrs.getLimitSize() < 0)
end = results.size(); if (end > results.size())
end = results.size(); for (int i = start; i < end; i++) {
RowDataPacket row = results.get(i);
row.packetId = ++packetId;
buffer = row.write(buffer, source, true);
} eof[3] = ++packetId;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("last packet id:" + packetId);
}
source.write(source.writeToBuffer(eof, buffer)); } catch (Exception e) {
handleDataProcessException(e);
} finally {
lock.unlock();
dataMergeSvr.clear();
}
}
为尊重原创成果,如需转载烦请注明本文出处:
http://www.cnblogs.com/fernandolee24/p/5243258.html,特此感谢
MyCat源码分析系列之——结果合并的更多相关文章
- 开源分布式数据库中间件MyCat源码分析系列
MyCat是当下很火的开源分布式数据库中间件,特意花费了一些精力研究其实现方式与内部机制,在此针对某些较为重要的源码进行粗浅的分析,希望与感兴趣的朋友交流探讨. 本源码分析系列主要针对代码实现,配置. ...
- MyCat源码分析系列之——SQL下发
更多MyCat源码分析,请戳MyCat源码分析系列 SQL下发 SQL下发指的是MyCat将解析并改造完成的SQL语句依次发送至相应的MySQL节点(datanode)的过程,该执行过程由NonBlo ...
- MyCat源码分析系列之——BufferPool与缓存机制
更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...
- MyCat源码分析系列之——前后端验证
更多MyCat源码分析,请戳MyCat源码分析系列 MyCat前端验证 MyCat的前端验证指的是应用连接MyCat时进行的用户验证过程,如使用MySQL客户端时,$ mysql -uroot -pr ...
- MyCat源码分析系列之——配置信息和启动流程
更多MyCat源码分析,请戳MyCat源码分析系列 MyCat配置信息 除了一些默认的配置参数,大多数的MyCat配置信息是通过读取若干.xml/.properties文件获取的,主要包括: 1)se ...
- [转]数据库中间件 MyCAT源码分析——跨库两表Join
1. 概述 2. 主流程 3. ShareJoin 3.1 JoinParser 3.2 ShareJoin.processSQL(...) 3.3 BatchSQLJob 3.4 ShareDBJo ...
- spring源码分析系列 (5) spring BeanFactoryPostProcessor拓展类PropertyPlaceholderConfigurer、PropertySourcesPlaceholderConfigurer解析
更多文章点击--spring源码分析系列 主要分析内容: 1.拓展类简述: 拓展类使用demo和自定义替换符号 2.继承图UML解析和源码分析 (源码基于spring 5.1.3.RELEASE分析) ...
- Spring Ioc源码分析系列--Ioc的基础知识准备
Spring Ioc源码分析系列--Ioc的基础知识准备 本系列文章代码基于Spring Framework 5.2.x Ioc的概念 在Spring里,Ioc的定义为The IoC Containe ...
- Spring Ioc源码分析系列--Bean实例化过程(一)
Spring Ioc源码分析系列--Bean实例化过程(一) 前言 上一篇文章Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理已经完成了对 ...
随机推荐
- android 使用Tabhost 发生could not create tab content because could not find view with id 错误
使用Tabhost的时候经常报:could not create tab content because could not find view with id 错误. 总结一下发生错误的原因,一般的 ...
- 如何避免git每次提交都输入密码
在ubuntu系统中,如何避免git每次提交都输入用户名和密码?操作步聚如下:1: cd 回车: 进入当前用户目录下:2: vim .git-credentials (如果没有安装vim 用其它编辑器 ...
- ASP.NET 5 RC1 升级 ASP.NET Core 1.0 RC2 记录
升级文档: Migrating from DNX to .NET Core Migrating from ASP.NET 5 RC1 to ASP.NET Core 1.0 RC2 Migrating ...
- R abalone data set
#鲍鱼数据集aburl <- 'http://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data' ab ...
- Git(1)
安装Git 完毕 (在开始菜单打开的话,打开的不是你想要的路径,切换路径很麻烦) 1.D盘新建 GitTest 文件夹 2.打开GitTest , 在空白的地方右键, 3.单击 Git Bash He ...
- windows下的命令行工具babun
什么是babun babun是windows上的一个第三方shell,在这个shell上面你可以使用几乎所有linux,unix上面的命令,他几乎可以取代windows的shell.用官方的题目说就是 ...
- 【微信SEO】公众号也能做排名?
[写于2016年8月] 最近,微信团队发出一则公告,开放公众号运营者一年内更改公众号名一次,这对不少名字起的奇葩名字(包括dkplus)的公众号来说是一件好事. 为什么说是好事呢?公众号名字直接关联到 ...
- intellij idea 13&14 插件推荐及快速上手建议
IntelliJIDEA插件安装 首页 > blog Tags : intellij IDEA插件安装 更新日期: 2015-04-29 IntelliJ IDEA插件下载地址: http:// ...
- 如何让我们的PHP在Jexus中跑起来
最近一段时间,经常看到不少的朋友在问,应该怎么设置才能够让Jexus支持PHP.其实,Jexus在很早之前就已经是可以支持PHP,像Apache或Nginx一样充当PHP的Web服务器的.不过由于没有 ...
- 再来说说 LaTeX
在我的上一篇随笔中,我提到了 Markdown.LaTeX 和 MathJax.这几个东西对目前的网络技术文章的写作.展示都有深远的影响.在上一篇中,我还给出了一份 LaTeX 语法的学习资料.在这一 ...