[转]数据库中间件 MyCAT源码分析——跨库两表Join
1. 概述
2. 主流程
3. ShareJoin
3.1 JoinParser
3.2 ShareJoin.processSQL(...)
3.3 BatchSQLJob
3.4 ShareDBJoinHandler
3.5 ShareRowOutPutDataHandler
4. 彩蛋
1. 概述
MyCAT 支持跨库表 Join,目前版本仅支持跨库两表 Join。虽然如此,已经能够满足我们大部分的业务场景。况且,Join 过多的表可能带来的性能问题也是很麻烦的。
本文主要分享:
整体流程、调用顺序图
核心代码的分析
前置阅读:《MyCAT 源码分析 —— 【单库单表】查询》。
2. 主流程
当执行跨库两表 Join SQL 时,经历的大体流程如下:

SQL 上,需要添加注解 /*!mycat:catlet=io.mycat.catlets.ShareJoin */${SQL} 。 RouteService#route(...) 解析注解 mycat:catlet 后,路由给 HintCatletHandler 作进一步处理。
HintCatletHandler 获取注解对应的 Catlet 实现类, io.mycat.catlets.ShareJoin 就是其中一种实现(目前也只有这一种实现),提供了跨库两表 Join 的功能。从类命名上看, ShareJoin 很大可能性后续会提供完整的跨库多表的 Join 功能。
核心代码如下:
// HintCatletHandler.javapublic RouteResultset route(SystemConfig sysConfig, SchemaConfig schema,int sqlType, String realSQL, String charset, ServerConnection sc,LayerCachePool cachePool, String hintSQLValue, int hintSqlType, Map hintMap)throws SQLNonTransientException {String cateletClass = hintSQLValue;if (LOGGER.isDebugEnabled()) {LOGGER.debug("load catelet class:" + hintSQLValue + " to run sql " + realSQL);}try {Catlet catlet = (Catlet) MycatServer.getInstance().getCatletClassLoader().getInstanceofClass(cateletClass);catlet.route(sysConfig, schema, sqlType, realSQL, charset, sc, cachePool);catlet.processSQL(realSQL, new EngineCtx(sc.getSession2()));} catch (Exception e) {LOGGER.warn("catlet error " + e);throw new SQLNonTransientException(e);}return null;}
3. ShareJoin
目前支持跨库两表 Join。 ShareJoin 将 SQL 拆分成左表 SQL 和 右表 SQL,发送给各数据节点执行,汇总数据结果进行合后返回。
伪代码如下:
// SELECT u.id, o.id FROM t_order o// INNER JOIN t_user u ON o.uid = u.id// 【顺序】查询左表String leftSQL = "SELECT o.id, u.id FROM t_order o";List leftList = dn[0].select(leftSQL) + dn[1].select(leftSQL) + ... + dn[n].select(leftsql);// 【并行】查询右表String rightSQL = "SELECT u.id FROM t_user u WHERE u.id IN (${leftList.uid})";for (dn : dns) { // 此处是并行执行,使用回调逻辑for (rightRecord : dn.select(rightSQL)) { // 查询右表// 合并结果for (leftRecord : leftList) {if (leftRecord.uid == rightRecord.id) {write(leftRecord + leftRecord.uid 拼接结果);}}}}
实际情况会更加复杂,我们接下来一点点往下看。
3.1 JoinParser
JoinParser 负责对 SQL 进行解析。整体流程如下:
举个例子, /*!mycat:catlet=io.mycat.catlets.ShareJoin */SELECT o.id,u.usernamefromt_order o join t_user u on o.uid=u.id; 解析后, TableFilter 结果如下:

tName :表名
tAlia :表自定义命名
where :过滤条件
order :排序条件
parenTable :左连接的 Join 的表名。
t_user表 在join属性 的parenTable为 "o",即t_order。joinParentkey :左连接的 Join 字段
joinKey :join 字段。
t_user表 在join属性 为id。join :子 tableFilter。即,该表连接的右边的表。
parent :和
join属性 相对。
看到此处,大家可能有疑问,为什么要把 SQL 解析成 TableFilter。 JoinParser 根据 TableFilter 生成数据节点执行 SQL。代码如下:
// TableFilter.javapublic String getSQL() {String sql = "";// fieldsfor (Entry<String, String> entry : fieldAliasMap.entrySet()) {String key = entry.getKey();String val = entry.getValue();if (val == null) {sql = unionsql(sql, getFieldfrom(key), ",");} else {sql = unionsql(sql, getFieldfrom(key) + " as " + val, ",");}}// whereif (parent == null) { // on/where 等于号左边的表String parentJoinKey = getJoinKey(true);// fix sharejoin bug:// (AbstractConnection.java:458) -close connection,reason:program err:java.lang.IndexOutOfBoundsException:// 原因是左表的select列没有包含 join 列,在获取结果时报上面的错误if (sql != null && parentJoinKey != null &&!sql.toUpperCase().contains(parentJoinKey.trim().toUpperCase())) {sql += ", " + parentJoinKey;}sql = "select " + sql + " from " + tName;if (!(where.trim().equals(""))) {sql += " where " + where.trim();}} else { // on/where 等于号右边边的表if (allField) {sql = "select " + sql + " from " + tName;} else {sql = unionField("select " + joinKey, sql, ",");sql = sql + " from " + tName;//sql="select "+joinKey+","+sql+" from "+tName;}if (!(where.trim().equals(""))) {sql += " where " + where.trim() + " and (" + joinKey + " in %s )";} else {sql += " where " + joinKey + " in %s ";}}// orderif (!(order.trim().equals(""))) {sql += " order by " + order.trim();}// limitif (parent == null) {if ((rowCount > 0) && (offset > 0)) {sql += " limit" + offset + "," + rowCount;} else {if (rowCount > 0) {sql += " limit " + rowCount;}}}return sql;}
当
parent为空时,即on/where 等于号左边的表。例如:selectid,uidfromt_order。当
parent不为空时,即on/where 等于号右边的表。例如:selectid,usernamefromt_userwhereidin(1,2,3)。
3.2 ShareJoin.processSQL(...)
当 SQL 解析完后,生成左边的表执行的 SQL,发送给对应的数据节点查询数据。大体流程如下:

当 SQL 为 /*!mycat:catlet=io.mycat.catlets.ShareJoin */SELECT o.id,u.usernamefromt_order o join t_user u on o.uid=u.id; 时, sql=getSql() 的返回结果为 selectid,uidfromt_order。
生成左边的表执行的 SQL 后,顺序顺序顺序发送给对应的数据节点查询数据。具体顺序查询是怎么实现的,我们来看下章 BatchSQLJob。
3.3 BatchSQLJob

EngineCtx 对 BatchSQLJob 封装,提供上层两个方法:
executeNativeSQLSequnceJob :顺序(非并发)在每个数据节点执行SQL任务
executeNativeSQLParallJob :并发在每个数据节点执行SQL任务
核心代码如下:
// EngineCtx.javapublic void executeNativeSQLSequnceJob(String[] dataNodes, String sql,SQLJobHandler jobHandler) {for (String dataNode : dataNodes) {SQLJob job = new SQLJob(jobId.incrementAndGet(), sql, dataNode,jobHandler, this);bachJob.addJob(job, false);}}public void executeNativeSQLParallJob(String[] dataNodes, String sql,SQLJobHandler jobHandler) {for (String dataNode : dataNodes) {SQLJob job = new SQLJob(jobId.incrementAndGet(), sql, dataNode,jobHandler, this);bachJob.addJob(job, true);}}
BatchSQLJob 通过执行中任务列表、待执行任务列表来实现顺序/并发执行任务。核心代码如下:
// BatchSQLJob.java/*** 执行中任务列表*/private ConcurrentHashMap<Integer, SQLJob> runningJobs = new ConcurrentHashMap<Integer, SQLJob>();/*** 待执行任务列表*/private ConcurrentLinkedQueue<SQLJob> waitingJobs = new ConcurrentLinkedQueue<SQLJob>();public void addJob(SQLJob newJob, boolean parallExecute) {if (parallExecute) {runJob(newJob);} else {waitingJobs.offer(newJob);if (runningJobs.isEmpty()) { // 若无正在执行中的任务,则从等待队列里获取任务进行执行。SQLJob job = waitingJobs.poll();if (job != null) {runJob(job);}}}}public boolean jobFinished(SQLJob sqlJob) {runningJobs.remove(sqlJob.getId());SQLJob job = waitingJobs.poll();if (job != null) {runJob(job);return false;} else {if (noMoreJobInput) {return runningJobs.isEmpty() && waitingJobs.isEmpty();} else {return false;}}}
顺序执行时,当
runningJobs存在执行中的任务时,#addJob(...)时,不立即执行,添加到waitingJobs。当SQLJob完成时,顺序调用下一个任务。并发执行时,
#addJob(...)时,立即执行。
SQLJob SQL 异步执行任务。其 jobHandler(SQLJobHandler) 属性,在 SQL 执行有返回结果时,会进行回调,从而实现异步执行。
在 ShareJoin 里, SQLJobHandler 有两个实现: ShareDBJoinHandler、 ShareRowOutPutDataHandler。前者,左边的表执行的 SQL 回调;后者,右边的表执行的 SQL 回调。

3.4 ShareDBJoinHandler
ShareDBJoinHandler,左边的表执行的 SQL 回调。流程如下:

#fieldEofResponse(...):接收数据节点返回的 fields,放入内存。#rowResponse(...):接收数据节点返回的 row,放入内存。#rowEofResponse(...):接收完一个数据节点返回所有的 row。当所有数据节点都完成 SQL 执行时,提交右边的表执行的 SQL 任务,并行执行,即图中#createQryJob(...)。
当 SQL 为 /*!mycat:catlet=io.mycat.catlets.ShareJoin */SELECT o.id,u.usernamefromt_order o join t_user u on o.uid=u.id; 时, sql=getChildSQL() 的返回结果为selectid,usernamefromt_userwhereidin(1,2,3)。
核心代码如下:
// ShareJoin.javaprivate void createQryJob(int batchSize) {int count = 0;Map<String, byte[]> batchRows = new ConcurrentHashMap<String, byte[]>();String theId = null;StringBuilder sb = new StringBuilder().append('(');String svalue = "";for (Map.Entry<String, String> e : ids.entrySet()) {theId = e.getKey();byte[] rowbyte = rows.remove(theId);if (rowbyte != null) {batchRows.put(theId, rowbyte);}if (!svalue.equals(e.getValue())) {if (joinKeyType == Fields.FIELD_TYPE_VAR_STRING|| joinKeyType == Fields.FIELD_TYPE_STRING) { // joinkey 为varcharsb.append("'").append(e.getValue()).append("'").append(','); // ('digdeep','yuanfang')} else { // 默认joinkey为int/longsb.append(e.getValue()).append(','); // (1,2,3)}}svalue = e.getValue();if (count++ > batchSize) {break;}}if (count == 0) {return;}jointTableIsData = true;sb.deleteCharAt(sb.length() - 1).append(')');String sql = String.format(joinParser.getChildSQL(), sb);getRoute(sql);ctx.executeNativeSQLParallJob(getDataNodes(), sql, new ShareRowOutPutDataHandler(this, fields, joinindex, joinParser.getJoinRkey(), batchRows, ctx.getSession()));}
3.5 ShareRowOutPutDataHandler
ShareRowOutPutDataHandler,右边的表执行的 SQL 回调。流程如下:

#fieldEofResponse(...):接收数据节点返回的 fields,返回 header 给 MySQL Client。#rowResponse(...):接收数据节点返回的 row,匹配左表的记录,返回合并后返回的 row 给 MySQL Client。#rowEofResponse(...):当所有 row 都返回完后,返回 eof 给 MySQL Client。
核心代码如下:
// ShareRowOutPutDataHandler.javapublic boolean onRowData(String dataNode, byte[] rowData) {RowDataPacket rowDataPkgold = ResultSetUtil.parseRowData(rowData, bfields);//拷贝一份batchRowsMap<String, byte[]> batchRowsCopy = new ConcurrentHashMap<String, byte[]>();batchRowsCopy.putAll(arows);// 获取Id字段,String id = ByteUtil.getString(rowDataPkgold.fieldValues.get(joinR));// 查找ID对应的A表的记录byte[] arow = getRow(batchRowsCopy, id, joinL);while (arow != null) {RowDataPacket rowDataPkg = ResultSetUtil.parseRowData(arow, afields);//ctx.getAllFields());for (int i = 1; i < rowDataPkgold.fieldCount; i++) {// 设置b.name 字段byte[] bname = rowDataPkgold.fieldValues.get(i);rowDataPkg.add(bname);rowDataPkg.addFieldCount(1);}// huangyiming addMiddlerResultHandler middlerResultHandler = session.getMiddlerResultHandler();if (null == middlerResultHandler) {ctx.writeRow(rowDataPkg);} else {if (middlerResultHandler instanceof MiddlerQueryResultHandler) {byte[] columnData = rowDataPkg.fieldValues.get(0);if (columnData != null && columnData.length > 0) {String rowValue = new String(columnData);middlerResultHandler.add(rowValue);}//}}}arow = getRow(batchRowsCopy, id, joinL);}return false;}
4. 彩蛋
如下是本文涉及到的核心类,有兴趣的同学可以翻一翻。

ShareJoin 另外不支持的功能:
只支持 inner join,不支持 left join、right join 等等连接。
不支持 order by。
不支持 group by 以及 相关聚合函数。
即使 join 左表的字段未声明为返回 fields 也会返回。
恩,MyCAT 弱XA 源码继续走起!
[转]数据库中间件 MyCAT源码分析——跨库两表Join的更多相关文章
- 开源分布式数据库中间件MyCat源码分析系列
MyCat是当下很火的开源分布式数据库中间件,特意花费了一些精力研究其实现方式与内部机制,在此针对某些较为重要的源码进行粗浅的分析,希望与感兴趣的朋友交流探讨. 本源码分析系列主要针对代码实现,配置. ...
- 数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析(三)之查询SQL
- MyCat源码分析系列之——结果合并
更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...
- MyCat源码分析系列之——SQL下发
更多MyCat源码分析,请戳MyCat源码分析系列 SQL下发 SQL下发指的是MyCat将解析并改造完成的SQL语句依次发送至相应的MySQL节点(datanode)的过程,该执行过程由NonBlo ...
- MyCat源码分析系列之——前后端验证
更多MyCat源码分析,请戳MyCat源码分析系列 MyCat前端验证 MyCat的前端验证指的是应用连接MyCat时进行的用户验证过程,如使用MySQL客户端时,$ mysql -uroot -pr ...
- MyCat源码分析系列之——配置信息和启动流程
更多MyCat源码分析,请戳MyCat源码分析系列 MyCat配置信息 除了一些默认的配置参数,大多数的MyCat配置信息是通过读取若干.xml/.properties文件获取的,主要包括: 1)se ...
- MyCat源码分析系列之——BufferPool与缓存机制
更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...
- MyCAT源码分析——分析环境部署
为了更好地了解mycat的原理,计划对mycat源码进行通读一遍,根据实际业务环境进行相关源码优化. 一.环境描述 操作系统:windows 10 x64 软件:jdk 1.7+ maven ...
- Django中间件部分源码分析
中间件源码分析 中间件简介 中间件是一个用来处理Django的请求和响应的框架级别的钩子.它是一个轻量.低级别的插件系统,用于在全局范围内改变Django的输入和输出.每个中间件组件都负责做一些特定的 ...
随机推荐
- 兼容的获取样式的函数getStyle()
想要得到某个元素的某个样式属性,可以用: <div id="div01" style="color:red">123</div> var ...
- ALV 动态行列
动态ALV显示的行列,解决部分报表需求. 运行截图如下: 选择屏幕: ALV输出效果1: ALV输出效果2:: ABAP代码: *&------------------------------ ...
- Flipping an Image
Given a binary matrix A, we want to flip the image horizontally, then invert it, and return the resu ...
- linux学习笔记:linux常用的命令
2018-11-19 常见命令快速查询一览表 命令 功能 ls 列出目录内容 cat 链接文件并打印到标准输出设备上(通常用来 ...
- jQuery轮播图--不使用插件
说明:引入jquery.min.js 将轮播图放入imgs文件夹 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitiona ...
- 第五周博客作业 <西北师范大学| 周安伟>
第五周博客作业 一,助教博客链接https://home.cnblogs.com/u/zaw-315/ 二,本周工作查阅项目汇报,班级微信群.对同学们的中期项目汇报进行查看,解决上周留言问题,对及时出 ...
- sqlserver2017 SSAS配置远程访问不成功的问题
sqlserver2017 SSAS通过IIS配置远程访问一直访问不成功的解决办法: 出现这个问题的原因从微软给出的更新包中说的就是: 从 SQL Server 2017 开始,Analysis Se ...
- zyupload四种不同的PHP上传demo
PHP结合zyupload多功能图片上传实例,支持拖拽和裁剪.可以自定义高度和宽度,类型,远程上传地址等. zyupload上传基本配置 1 $("#zyupload").zyUp ...
- selenium之 chromedriver与chrome版本映射表(更新至v2.46)
chromedriver版本 支持的Chrome版本 v2.46 v71-73 v2.45 v70-72 v2.44 v69-71 v2.43 v69-71 v2.42 v68-70 v2.41 v6 ...
- Java-Oracle数据库连接
Oracle数据库先创建一个表和添加一些数据,下面是连接数据库的具体实现.(导入jar包:ojdbc14.jar) import java.sql.Connection; import java.sq ...