[轉]數據庫中間件 MyCAT源碼分析——跨庫兩表Join


  • 1. 概述java

  • 2. 主流程sql

  • 3. ShareJoinsession

    • 3.1 JoinParser併發

    • 3.2 ShareJoin.processSQL(...)app

    • 3.3 BatchSQLJob異步

    • 3.4 ShareDBJoinHandler函數

    • 3.5 ShareRowOutPutDataHandler源碼分析

  • 4. 彩蛋性能


1. 概述

MyCAT 支持跨庫表 Join,目前版本僅支持跨庫表 Join。雖然如此,已經可以知足咱們大部分的業務場景。何況,Join 過多的表可能帶來的性能問題也是很麻煩的。ui

本文主要分享:

  1. 總體流程、調用順序圖

  2. 核心代碼的分析

前置閱讀:《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 功能。

核心代碼以下:

  1. // HintCatletHandler.java

  2. public RouteResultset route(SystemConfig sysConfig, SchemaConfig schema,

  3.                           int sqlType, String realSQL, String charset, ServerConnection sc,

  4.                           LayerCachePool cachePool, String hintSQLValue, int hintSqlType, Map hintMap)

  5.       throws SQLNonTransientException {

  6.   String cateletClass = hintSQLValue;

  7.   if (LOGGER.isDebugEnabled()) {

  8.       LOGGER.debug("load catelet class:" + hintSQLValue + " to run sql " + realSQL);

  9.   }

  10.   try {

  11.       Catlet catlet = (Catlet) MycatServer.getInstance().getCatletClassLoader().getInstanceofClass(cateletClass);

  12.       catlet.route(sysConfig, schema, sqlType, realSQL, charset, sc, cachePool);

  13.       catlet.processSQL(realSQL, new EngineCtx(sc.getSession2()));

  14.   } catch (Exception e) {

  15.       LOGGER.warn("catlet error " + e);

  16.       throw new SQLNonTransientException(e);

  17.   }

  18.   return null;

  19. }

3. ShareJoin

目前支持跨庫表 Join。 ShareJoin 將 SQL 拆分紅左表 SQL 和 右表 SQL,發送給各數據節點執行,彙總數據結果進行合後返回。

僞代碼以下:

  1. // SELECT u.id, o.id FROM t_order o

  2. // INNER JOIN t_user u ON o.uid = u.id

  3. // 【順序】查詢左表

  4. String leftSQL = "SELECT o.id, u.id FROM t_order o";

  5. List leftList = dn[0].select(leftSQL) + dn[1].select(leftSQL) + ... + dn[n].select(leftsql);

  6. // 【並行】查詢右表

  7. String rightSQL = "SELECT u.id FROM t_user u WHERE u.id IN (${leftList.uid})";

  8. for (dn : dns) { // 此處是並行執行,使用回調邏輯

  9.    for (rightRecord : dn.select(rightSQL)) { // 查詢右表

  10.        // 合併結果

  11.        for (leftRecord : leftList) {

  12.            if (leftRecord.uid == rightRecord.id) {

  13.                write(leftRecord + leftRecord.uid 拼接結果);

  14.            }

  15.        }

  16.    }

  17. }

實際狀況會更加複雜,咱們接下來一點點往下看。

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 解析成 TableFilterJoinParser 根據 TableFilter 生成數據節點執行 SQL。代碼以下:

  1. // TableFilter.java

  2. public String getSQL() {

  3.   String sql = "";

  4.   // fields

  5.   for (Entry<String, String> entry : fieldAliasMap.entrySet()) {

  6.       String key = entry.getKey();

  7.       String val = entry.getValue();

  8.       if (val == null) {

  9.           sql = unionsql(sql, getFieldfrom(key), ",");

  10.       } else {

  11.           sql = unionsql(sql, getFieldfrom(key) + " as " + val, ",");

  12.       }

  13.   }

  14.   // where

  15.   if (parent == null) {    // on/where 等於號左邊的表

  16.       String parentJoinKey = getJoinKey(true);

  17.       // fix sharejoin bug:

  18.       // (AbstractConnection.java:458) -close connection,reason:program err:java.lang.IndexOutOfBoundsException:

  19.       // 緣由是左表的select列沒有包含 join 列,在獲取結果時報上面的錯誤

  20.       if (sql != null && parentJoinKey != null &&

  21.               !sql.toUpperCase().contains(parentJoinKey.trim().toUpperCase())) {

  22.           sql += ", " + parentJoinKey;

  23.       }

  24.       sql = "select " + sql + " from " + tName;

  25.       if (!(where.trim().equals(""))) {

  26.           sql += " where " + where.trim();

  27.       }

  28.   } else {    // on/where 等於號右邊邊的表

  29.       if (allField) {

  30.           sql = "select " + sql + " from " + tName;

  31.       } else {

  32.           sql = unionField("select " + joinKey, sql, ",");

  33.           sql = sql + " from " + tName;

  34.           //sql="select "+joinKey+","+sql+" from "+tName;

  35.       }

  36.       if (!(where.trim().equals(""))) {

  37.           sql += " where " + where.trim() + " and (" + joinKey + " in %s )";

  38.       } else {

  39.           sql += " where " + joinKey + " in %s ";

  40.       }

  41.   }

  42.   // order

  43.   if (!(order.trim().equals(""))) {

  44.       sql += " order by " + order.trim();

  45.   }

  46.   // limit

  47.   if (parent == null) {

  48.       if ((rowCount > 0) && (offset > 0)) {

  49.           sql += " limit" + offset + "," + rowCount;

  50.       } else {

  51.           if (rowCount > 0) {

  52.               sql += " limit " + rowCount;

  53.           }

  54.       }

  55.   }

  56.   return sql;

  57. }

  • 當 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

 

EngineCtxBatchSQLJob 封裝,提供上層兩個方法:

  1. executeNativeSQLSequnceJob :順序(非併發)在每一個數據節點執行SQL任務

  2. executeNativeSQLParallJob :併發在每一個數據節點執行SQL任務

核心代碼以下:

  1. // EngineCtx.java

  2. public void executeNativeSQLSequnceJob(String[] dataNodes, String sql,

  3.        SQLJobHandler jobHandler) {

  4.    for (String dataNode : dataNodes) {

  5.        SQLJob job = new SQLJob(jobId.incrementAndGet(), sql, dataNode,

  6.                jobHandler, this);

  7.        bachJob.addJob(job, false);

  8.    }

  9. }

  10.  

  11. public void executeNativeSQLParallJob(String[] dataNodes, String sql,

  12.        SQLJobHandler jobHandler) {

  13.    for (String dataNode : dataNodes) {

  14.        SQLJob job = new SQLJob(jobId.incrementAndGet(), sql, dataNode,

  15.                jobHandler, this);

  16.        bachJob.addJob(job, true);

  17.    }

  18. }    


BatchSQLJob 經過執行中任務列表待執行任務列表來實現順序/併發執行任務。核心代碼以下:

  1. // BatchSQLJob.java

  2. /**

  3. * 執行中任務列表

  4. */

  5. private ConcurrentHashMap<Integer, SQLJob> runningJobs = new ConcurrentHashMap<Integer, SQLJob>();

  6. /**

  7. * 待執行任務列表

  8. */

  9. private ConcurrentLinkedQueue<SQLJob> waitingJobs = new ConcurrentLinkedQueue<SQLJob>();

  10.  

  11. public void addJob(SQLJob newJob, boolean parallExecute) {

  12.   if (parallExecute) {

  13.       runJob(newJob);

  14.   } else {

  15.       waitingJobs.offer(newJob);

  16.       if (runningJobs.isEmpty()) { // 若無正在執行中的任務,則從等待隊列裏獲取任務進行執行。

  17.           SQLJob job = waitingJobs.poll();

  18.           if (job != null) {

  19.               runJob(job);

  20.           }

  21.       }

  22.   }

  23. }

  24.  

  25. public boolean jobFinished(SQLJob sqlJob) {

  26.    runningJobs.remove(sqlJob.getId());

  27.    SQLJob job = waitingJobs.poll();

  28.    if (job != null) {

  29.        runJob(job);

  30.        return false;

  31.    } else {

  32.        if (noMoreJobInput) {

  33.            return runningJobs.isEmpty() && waitingJobs.isEmpty();

  34.        } else {

  35.            return false;

  36.        }

  37.    }

  38. }

  • 順序執行時,當 runningJobs 存在執行中的任務時, #addJob(...) 時,不當即執行,添加到 waitingJobs。當 SQLJob 完成時,順序調用下一個任務。

  • 併發執行時, #addJob(...) 時,當即執行。


SQLJob SQL 異步執行任務。其 jobHandler(SQLJobHandler) 屬性,在 SQL 執行有返回結果時,會進行回調,從而實現異步執行。

ShareJoin 裏, SQLJobHandler 有兩個實現: ShareDBJoinHandlerShareRowOutPutDataHandler。前者,左邊的表執行的 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)

核心代碼以下:

  1. // ShareJoin.java

  2. private void createQryJob(int batchSize) {

  3.   int count = 0;

  4.   Map<String, byte[]> batchRows = new ConcurrentHashMap<String, byte[]>();

  5.   String theId = null;

  6.   StringBuilder sb = new StringBuilder().append('(');

  7.   String svalue = "";

  8.   for (Map.Entry<String, String> e : ids.entrySet()) {

  9.       theId = e.getKey();

  10.       byte[] rowbyte = rows.remove(theId);

  11.       if (rowbyte != null) {

  12.           batchRows.put(theId, rowbyte);

  13.       }

  14.       if (!svalue.equals(e.getValue())) {

  15.           if (joinKeyType == Fields.FIELD_TYPE_VAR_STRING

  16.                   || joinKeyType == Fields.FIELD_TYPE_STRING) { // joinkey 爲varchar

  17.               sb.append("'").append(e.getValue()).append("'").append(','); // ('digdeep','yuanfang')

  18.           } else { // 默認joinkey爲int/long

  19.               sb.append(e.getValue()).append(','); // (1,2,3)

  20.           }

  21.       }

  22.       svalue = e.getValue();

  23.       if (count++ > batchSize) {

  24.           break;

  25.       }

  26.   }

  27.   if (count == 0) {

  28.       return;

  29.   }

  30.   jointTableIsData = true;

  31.   sb.deleteCharAt(sb.length() - 1).append(')');

  32.   String sql = String.format(joinParser.getChildSQL(), sb);

  33.   getRoute(sql);

  34.   ctx.executeNativeSQLParallJob(getDataNodes(), sql, new ShareRowOutPutDataHandler(this, fields, joinindex, joinParser.getJoinRkey(), batchRows, ctx.getSession()));

  35. }

3.5 ShareRowOutPutDataHandler

ShareRowOutPutDataHandler右邊的表執行的 SQL 回調。流程以下:

 

  • #fieldEofResponse(...) :接收數據節點返回的 fields,返回 header 給 MySQL Client。

  • #rowResponse(...) :接收數據節點返回的 row,匹配左表的記錄,返回合併後返回的 row 給 MySQL Client。

  • #rowEofResponse(...) :當全部 row 都返回完後,返回 eof 給 MySQL Client。

核心代碼以下:

  1. // ShareRowOutPutDataHandler.java

  2. public boolean onRowData(String dataNode, byte[] rowData) {

  3.   RowDataPacket rowDataPkgold = ResultSetUtil.parseRowData(rowData, bfields);

  4.   //拷貝一份batchRows

  5.   Map<String, byte[]> batchRowsCopy = new ConcurrentHashMap<String, byte[]>();

  6.   batchRowsCopy.putAll(arows);

  7.   // 獲取Id字段,

  8.   String id = ByteUtil.getString(rowDataPkgold.fieldValues.get(joinR));

  9.   // 查找ID對應的A表的記錄

  10.   byte[] arow = getRow(batchRowsCopy, id, joinL);

  11.   while (arow != null) {

  12.       RowDataPacket rowDataPkg = ResultSetUtil.parseRowData(arow, afields);//ctx.getAllFields());

  13.       for (int i = 1; i < rowDataPkgold.fieldCount; i++) {

  14.           // 設置b.name 字段

  15.           byte[] bname = rowDataPkgold.fieldValues.get(i);

  16.           rowDataPkg.add(bname);

  17.           rowDataPkg.addFieldCount(1);

  18.       }

  19.       // huangyiming add

  20.       MiddlerResultHandler middlerResultHandler = session.getMiddlerResultHandler();

  21.       if (null == middlerResultHandler) {

  22.           ctx.writeRow(rowDataPkg);

  23.       } else {

  24.           if (middlerResultHandler instanceof MiddlerQueryResultHandler) {

  25.               byte[] columnData = rowDataPkg.fieldValues.get(0);

  26.               if (columnData != null && columnData.length > 0) {

  27.                   String rowValue = new String(columnData);

  28.                   middlerResultHandler.add(rowValue);

  29.               }

  30.               //}

  31.           }

  32.  

  33.       }

  34.       arow = getRow(batchRowsCopy, id, joinL);

  35.   }

  36.   return false;

  37. }

4. 彩蛋

以下是本文涉及到的核心類,有興趣的同窗能夠翻一翻。

 

ShareJoin 另外不支持的功能:

  1. 只支持 inner join,不支持 left join、right join 等等鏈接。

  2. 不支持 order by。

  3. 不支持 group by 以及 相關聚合函數。

  4. 即便 join 左表的字段未聲明爲返回 fields 也會返回。

恩,MyCAT 弱XA 源碼繼續走起!

相關文章
相關標籤/搜索