資料庫中介軟體 Sharding-JDBC 原始碼分析 —— JDBC實現與讀寫分離

芋道原始碼_以德服人_不服就幹發表於2017-10-22

摘要: 原創出處 www.iocoder.cn/Sharding-JD… 「芋道原始碼」歡迎轉載,保留摘要,謝謝!

本文主要基於 Sharding-JDBC 1.5.0 正式版


???關注微信公眾號:【芋道原始碼】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
  3. 您對於原始碼的疑問每條留言將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢
  4. 新的原始碼解析文章實時收到通知。每週更新一篇左右
  5. 認真的原始碼交流微信群。
  6. 掘金Java群:217878901。

1. 概述

本文主要分享 JDBC讀寫分離 的實現。為什麼會把這兩個東西放在一起講呢?客戶端直連資料庫的讀寫分離主要通過獲取讀庫和寫庫的不同連線來實現,和 JDBC Connection 剛好放在一塊。

OK,我們先來看一段 Sharding-JDBC 官方對自己的定義和定位

Sharding-JDBC定位為輕量級java框架,使用客戶端直連資料庫,以jar包形式提供服務,未使用中間層,無需額外部署,無其他依賴,DBA也無需改變原有的運維方式,可理解為增強版的JDBC驅動,舊程式碼遷移成本幾乎為零。

可以看出,Sharding-JDBC 通過實現 JDBC規範,對上層提供透明化資料庫分庫分表的訪問。? 黑科技?實際我們使用的資料庫連線池也是通過這種方式實現對上層無感知的提供連線池。甚至還可以通過這種方式實現對 Lucene、MongoDB 等等的訪問。

扯遠了,下面來看看 Sharding-JDBC jdbc 包的結構:

  • unsupported:宣告不支援的資料操作方法
  • adapter:適配類,實現和分庫分表無關的方法
  • core:核心類,實現和分庫分表相關的方法

根據 core 包,可以看出分到四種我們超級熟悉的物件

  • Datasource

    -w640
    -w640

  • Connection

    -w640
    -w640

  • Statement

    -w640
    -w640

  • ResultSet

    -w640
    -w640

實現層級如下:JDBC 介面 <=(繼承)== unsupported抽象類 <=(繼承)== unsupported抽象類 <=(繼承)== core


本文內容順序

  1. unspported
  2. adapter
  3. 插入流程,分析的類:
    • ShardingDataSource
    • ShardingConnection
    • ShardingPreparedStatement(ShardingStatement 類似,不重複分析)
    • GeneratedKeysResultSet、GeneratedKeysResultSetMetaData
  4. 查詢流程,分析的類:
    • ShardingPreparedStatement
    • ShardingResultSet
  5. 讀寫分離,分析的類:
    • MasterSlaveDataSource

Sharding-JDBC 正在收集使用公司名單:傳送門
? 你的登記,會讓更多人蔘與和使用 Sharding-JDBC。傳送門
Sharding-JDBC 也會因此,能夠覆蓋更多的業務場景。傳送門
登記吧,騷年!傳送門

2. unspported 包

unspported 包內的抽象類,宣告不支援操作的資料物件,所有方法都是 throw new SQLFeatureNotSupportedException() 方式。

public abstract class AbstractUnsupportedGeneratedKeysResultSet extends AbstractUnsupportedOperationResultSet {

    @Override
    public boolean getBoolean(final int columnIndex) throws SQLException {
        throw new SQLFeatureNotSupportedException("getBoolean");
    }

    // .... 省略其它類似方法
}

public abstract class AbstractUnsupportedOperationConnection extends WrapperAdapter implements Connection {

    @Override
    public final CallableStatement prepareCall(final String sql) throws SQLException {
        throw new SQLFeatureNotSupportedException("prepareCall");
    }

   // .... 省略其它類似方法
}複製程式碼

3. adapter 包

adapter 包內的抽象類,實現和分庫分表無關的方法。

考慮到第4、5兩小節更容易理解,本小節貼的程式碼會相對多

3.1 WrapperAdapter

WrapperAdapter,JDBC Wrapper 適配類。

對 Wrapper 介面實現如下兩個方法

@Override
public final <T> T unwrap(final Class<T> iface) throws SQLException {
   if (isWrapperFor(iface)) {
       return (T) this;
   }
   throw new SQLException(String.format("[%s] cannot be unwrapped as [%s]", getClass().getName(), iface.getName()));
}

@Override
public final boolean isWrapperFor(final Class<?> iface) throws SQLException {
   return iface.isInstance(this);
}複製程式碼

提供子類 #recordMethodInvocation() 記錄方法呼叫,#replayMethodsInvocation() 回放記錄的方法呼叫


/**
* 記錄的方法陣列
*/
private final Collection<JdbcMethodInvocation> jdbcMethodInvocations = new ArrayList<>();

/**
* 記錄方法呼叫.
* 
* @param targetClass 目標類
* @param methodName 方法名稱
* @param argumentTypes 引數型別
* @param arguments 引數
*/
public final void recordMethodInvocation(final Class<?> targetClass, final String methodName, final Class<?>[] argumentTypes, final Object[] arguments) {
   try {
       jdbcMethodInvocations.add(new JdbcMethodInvocation(targetClass.getMethod(methodName, argumentTypes), arguments));
   } catch (final NoSuchMethodException ex) {
       throw new ShardingJdbcException(ex);
   }
}

/**
* 回放記錄的方法呼叫.
* 
* @param target 目標物件
*/
public final void replayMethodsInvocation(final Object target) {
   for (JdbcMethodInvocation each : jdbcMethodInvocations) {
       each.invoke(target);
   }
}複製程式碼
  • 這兩個方法有什麼用途呢?例如下文會提到的 AbstractConnectionAdapter 的 #setAutoCommit(),當它無資料庫連線時,先記錄;等獲得到資料連線後,再回放:

      // AbstractConnectionAdapter.java
      @Override
      public final void setAutoCommit(final boolean autoCommit) throws SQLException {
         this.autoCommit = autoCommit;
         if (getConnections().isEmpty()) { // 無資料連線時,記錄方法呼叫
             recordMethodInvocation(Connection.class, "setAutoCommit", new Class[] {boolean.class}, new Object[] {autoCommit});
             return;
         }
         for (Connection each : getConnections()) {
             each.setAutoCommit(autoCommit);
         }
      }複製程式碼
  • JdbcMethodInvocation,反射呼叫JDBC相關方法的工具類:

      public class JdbcMethodInvocation {
    
         /**
          * 方法
          */
         @Getter
         private final Method method;
         /**
          * 方法引數
          */
         @Getter
         private final Object[] arguments;
    
         /**
          *  呼叫方法.
          * 
          * @param target 目標物件
          */
         public void invoke(final Object target) {
             try {
                 method.invoke(target, arguments); // 反射呼叫
             } catch (final IllegalAccessException | InvocationTargetException ex) {
                 throw new ShardingJdbcException("Invoke jdbc method exception", ex);
             }
         }
      }複製程式碼

提供子類 #throwSQLExceptionIfNecessary() 丟擲異常鏈

protected void throwSQLExceptionIfNecessary(final Collection<SQLException> exceptions) throws SQLException {
   if (exceptions.isEmpty()) { // 為空不丟擲異常
       return;
   }
   SQLException ex = new SQLException();
   for (SQLException each : exceptions) {
       ex.setNextException(each); // 異常鏈
   }
   throw ex;
}複製程式碼

3.2 AbstractDataSourceAdapter

AbstractDataSourceAdapter,資料來源適配類。

直接點選連結檢視原始碼。

3.3 AbstractConnectionAdapter

AbstractConnectionAdapter,資料庫連線適配類。

我們來瞅瞅大家最關心的事務相關方法的實現。

/**
* 是否自動提交
*/
private boolean autoCommit = true;

/**
* 獲得連結
*
* @return 連結
*/
protected abstract Collection<Connection> getConnections();

@Override
public final boolean getAutoCommit() throws SQLException {
   return autoCommit;
}

@Override
public final void setAutoCommit(final boolean autoCommit) throws SQLException {
   this.autoCommit = autoCommit;
   if (getConnections().isEmpty()) { // 無資料連線時,記錄方法呼叫
       recordMethodInvocation(Connection.class, "setAutoCommit", new Class[] {boolean.class}, new Object[] {autoCommit});
       return;
   }
   for (Connection each : getConnections()) {
       each.setAutoCommit(autoCommit);
   }
}複製程式碼
  • #setAutoCommit() 呼叫時,實際會設定其所持有的 Connection 的 autoCommit 屬性
  • #getConnections() 和分庫分表相關,因而僅抽象該方法,留給子類實現
@Override
public final void commit() throws SQLException {
   for (Connection each : getConnections()) {
       each.commit();
   }
}

@Override
public final void rollback() throws SQLException {
   Collection<SQLException> exceptions = new LinkedList<>();
   for (Connection each : getConnections()) {
       try {
           each.rollback();
       } catch (final SQLException ex) {
           exceptions.add(ex);
       }
   }
   throwSQLExceptionIfNecessary(exceptions);
}複製程式碼
  • #commit()#rollback() 呼叫時,實際呼叫其所持有的 Connection 的方法
  • 異常情況下,#commit()#rollback() 處理方式不同,筆者暫時不知道答案,求證後會進行更新

    • #commit() 處理方式需要改成和 #rollback() 一樣。程式碼如下:

      @Override
      public final void commit() throws SQLException {
       Collection<SQLException> exceptions = new LinkedList<>();
       for (Connection each : getConnections()) {
           try {
               each.commit();
           } catch (final SQLException ex) {
               exceptions.add(ex);
           }
       }
       throwSQLExceptionIfNecessary(exceptions);
      }複製程式碼

事務級別和是否只讀相關程式碼如下:

/**
* 只讀
*/
private boolean readOnly = true;
/**
* 事務級別
*/
private int transactionIsolation = TRANSACTION_READ_UNCOMMITTED;

@Override
public final void setReadOnly(final boolean readOnly) throws SQLException {
   this.readOnly = readOnly;
   if (getConnections().isEmpty()) {
       recordMethodInvocation(Connection.class, "setReadOnly", new Class[] {boolean.class}, new Object[] {readOnly});
       return;
   }
   for (Connection each : getConnections()) {
       each.setReadOnly(readOnly);
   }
}

@Override
public final void setTransactionIsolation(final int level) throws SQLException {
   transactionIsolation = level;
   if (getConnections().isEmpty()) {
       recordMethodInvocation(Connection.class, "setTransactionIsolation", new Class[] {int.class}, new Object[] {level});
       return;
   }
   for (Connection each : getConnections()) {
       each.setTransactionIsolation(level);
   }
}複製程式碼

3.4 AbstractStatementAdapter

AbstractStatementAdapter,靜態語句物件適配類。

@Override
public final int getUpdateCount() throws SQLException {
   long result = 0;
   boolean hasResult = false;
   for (Statement each : getRoutedStatements()) {
       if (each.getUpdateCount() > -1) {
           hasResult = true;
       }
       result += each.getUpdateCount();
   }
   if (result > Integer.MAX_VALUE) {
       result = Integer.MAX_VALUE;
   }
   return hasResult ? Long.valueOf(result).intValue() : -1;
}

/**
* 獲取路由的靜態語句物件集合.
* 
* @return 路由的靜態語句物件集合
*/
protected abstract Collection<? extends Statement> getRoutedStatements();複製程式碼
  • #getUpdateCount() 呼叫持有的 Statement 計算更新數量
  • #getRoutedStatements() 和分庫分表相關,因而僅抽象該方法,留給子類實現

3.5 AbstractPreparedStatementAdapter

AbstractPreparedStatementAdapter,預編譯語句物件的適配類。

#recordSetParameter()實現對佔位符引數的設定

/**
* 記錄的設定引數方法陣列
*/
private final List<SetParameterMethodInvocation> setParameterMethodInvocations = new LinkedList<>();
/**
* 引數
*/
@Getter
private final List<Object> parameters = new ArrayList<>();

@Override
public final void setInt(final int parameterIndex, final int x) throws SQLException {
   setParameter(parameterIndex, x);
   recordSetParameter("setInt", new Class[]{int.class, int.class}, parameterIndex, x);
}

/**
* 記錄佔位符引數
*
* @param parameterIndex 佔位符引數位置
* @param value 引數
*/
private void setParameter(final int parameterIndex, final Object value) {
   if (parameters.size() == parameterIndex - 1) {
       parameters.add(value);
       return;
   }
   for (int i = parameters.size(); i <= parameterIndex - 1; i++) { // 用 null 填充前面未設定的位置
       parameters.add(null);
   }
   parameters.set(parameterIndex - 1, value);
}

/**
* 記錄設定引數方法呼叫
*
* @param methodName 方法名,例如 setInt、setLong 等
* @param argumentTypes 引數型別
* @param arguments 引數
*/
private void recordSetParameter(final String methodName, final Class[] argumentTypes, final Object... arguments) {
   try {
       setParameterMethodInvocations.add(new SetParameterMethodInvocation(PreparedStatement.class.getMethod(methodName, argumentTypes), arguments, arguments[1]));
   } catch (final NoSuchMethodException ex) {
       throw new ShardingJdbcException(ex);
   }
}

/**
* 回放記錄的設定引數方法呼叫
*
* @param preparedStatement 預編譯語句物件
*/
protected void replaySetParameter(final PreparedStatement preparedStatement) {
   addParameters();
   for (SetParameterMethodInvocation each : setParameterMethodInvocations) {
       updateParameterValues(each, parameters.get(each.getIndex() - 1)); // 同一個位置多次設定,值可能不一樣,需要更新下
       each.invoke(preparedStatement);
   }
}

/**
* 當使用分散式主鍵時,生成後會新增到 parameters,此時 parameters 數量多於 setParameterMethodInvocations,需要生成該分散式主鍵的 SetParameterMethodInvocation
*/
private void addParameters() {
   for (int i = setParameterMethodInvocations.size(); i < parameters.size(); i++) {
       recordSetParameter("setObject", new Class[]{int.class, Object.class}, i + 1, parameters.get(i));
   }
}

private void updateParameterValues(final SetParameterMethodInvocation setParameterMethodInvocation, final Object value) {
   if (!Objects.equals(setParameterMethodInvocation.getValue(), value)) {
       setParameterMethodInvocation.changeValueArgument(value); // 修改佔位符引數
   }
}複製程式碼
  • 邏輯類似 WrapperAdapter#recordMethodInvocation()#replayMethodsInvocation(),請認真閱讀程式碼註釋

  • SetParameterMethodInvocation,繼承 JdbcMethodInvocation,反射呼叫引數設定方法的工具類:

      public final class SetParameterMethodInvocation extends JdbcMethodInvocation {
    
          /**
           * 位置
           */
          @Getter
          private final int index;
          /**
           * 引數值
           */
          @Getter
          private final Object value;
    
          /**
           * 設定引數值.
           * 
           * @param value 引數值
           */
          public void changeValueArgument(final Object value) {
              getArguments()[1] = value;
          }
      }複製程式碼

3.6 AbstractResultSetAdapter

AbstractResultSetAdapter,代理結果集介面卡。

public abstract class AbstractResultSetAdapter extends AbstractUnsupportedOperationResultSet {
    /**
     * 結果集集合
     */
    @Getter
    private final List<ResultSet> resultSets;

    @Override
    // TODO should return sharding statement in future
    public final Statement getStatement() throws SQLException {
        return getResultSets().get(0).getStatement();
    }

    @Override
    public final ResultSetMetaData getMetaData() throws SQLException {
        return getResultSets().get(0).getMetaData();
    }

    @Override
    public int findColumn(final String columnLabel) throws SQLException {
        return getResultSets().get(0).findColumn(columnLabel);
    }

    // .... 省略其它方法
}複製程式碼

4. 插入流程

插入使用分散式主鍵例子程式碼如下:

// 程式碼僅僅是例子,生產環境下請注意異常處理和資源關閉
String sql = "INSERT INTO t_order(uid, nickname, pid) VALUES (1, '2', ?)";
DataSource dataSource = new ShardingDataSource(shardingRule);
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); // 返回主鍵需要  Statement.RETURN_GENERATED_KEYS
ps.setLong(1, 100);
ps.executeUpdate();
ResultSet rs = ps.getGeneratedKeys();
if (rs.next()) {
    System.out.println("id:" + rs.getLong(1));
}複製程式碼

呼叫 #executeUpdate() 方法,內部過程如下

是不是對上層完全透明?!我們來看看內部是怎麼實現的。

// ShardingPreparedStatement.java
@Override
public int executeUpdate() throws SQLException {
   try {
       Collection<PreparedStatementUnit> preparedStatementUnits = route();
       return new PreparedStatementExecutor(
               getShardingConnection().getShardingContext().getExecutorEngine(), getRouteResult().getSqlStatement().getType(), preparedStatementUnits, getParameters()).executeUpdate();
   } finally {
       clearBatch();
   }
}複製程式碼
  • #route() 分庫分表路由,獲得預編譯語句物件執行單元( PreparedStatementUnit )集合。

      public final class PreparedStatementUnit implements BaseStatementUnit {
          /**
           * SQL 執行單元
           */
          private final SQLExecutionUnit sqlExecutionUnit;
          /**
           * 預編譯語句物件
           */
          private final PreparedStatement statement;
      }複製程式碼
  • #executeUpdate() 呼叫執行引擎並行執行多個預編譯語句物件。執行時,最終呼叫預編譯語句物件( PreparedStatement )。我們來看一個例子:

      // PreparedStatementExecutor.java
      public int executeUpdate() {
         Context context = MetricsContext.start("ShardingPreparedStatement-executeUpdate");
         try {
             List<Integer> results = executorEngine.executePreparedStatement(sqlType, preparedStatementUnits, parameters, new ExecuteCallback<Integer>() {
    
                 @Override
                 public Integer execute(final BaseStatementUnit baseStatementUnit) throws Exception {
                     // 呼叫 PreparedStatement#executeUpdate()
                     return ((PreparedStatement) baseStatementUnit.getStatement()).executeUpdate();
                 }
             });
             return accumulate(results);
         } finally {
             MetricsContext.stop(context);
         }
      }複製程式碼

// ShardingPreparedStatement.java
private Collection<PreparedStatementUnit> route() throws SQLException {
   Collection<PreparedStatementUnit> result = new LinkedList<>();
   // 路由
   setRouteResult(routingEngine.route(getParameters()));
   // 遍歷 SQL 執行單元
   for (SQLExecutionUnit each : getRouteResult().getExecutionUnits()) {
       SQLType sqlType = getRouteResult().getSqlStatement().getType();
       Collection<PreparedStatement> preparedStatements;
       // 建立實際的 PreparedStatement
       if (SQLType.DDL == sqlType) {
           preparedStatements = generatePreparedStatementForDDL(each);
       } else {
           preparedStatements = Collections.singletonList(generatePreparedStatement(each));
       }
       getRoutedStatements().addAll(preparedStatements);
       // 回放設定佔位符引數到 PreparedStatement
       for (PreparedStatement preparedStatement : preparedStatements) {
           replaySetParameter(preparedStatement);
           result.add(new PreparedStatementUnit(each, preparedStatement));
       }
   }
   return result;
}

/**
* 建立 PreparedStatement
*
* @param sqlExecutionUnit SQL 執行單元
* @return PreparedStatement
* @throws SQLException 當 JDBC 操作發生異常時
*/
private PreparedStatement generatePreparedStatement(final SQLExecutionUnit sqlExecutionUnit) throws SQLException {
   Optional<GeneratedKey> generatedKey = getGeneratedKey();
   // 獲得連線
   Connection connection = getShardingConnection().getConnection(sqlExecutionUnit.getDataSource(), getRouteResult().getSqlStatement().getType());
   // 宣告返回主鍵
   if (isReturnGeneratedKeys() || isReturnGeneratedKeys() && generatedKey.isPresent()) {
       return connection.prepareStatement(sqlExecutionUnit.getSql(), RETURN_GENERATED_KEYS);
   }
   return connection.prepareStatement(sqlExecutionUnit.getSql(), getResultSetType(), getResultSetConcurrency(), getResultSetHoldability());
}複製程式碼
  • 呼叫 #generatePreparedStatement() 建立 PreparedStatement,後呼叫 #replaySetParameter() 回放設定佔位符引數到 PreparedStatement
  • 宣告返回主鍵 時,即 #isReturnGeneratedKeys() 返回 true 時,呼叫 connection.prepareStatement(sqlExecutionUnit.getSql(), RETURN_GENERATED_KEYS)。為什麼該方法會返回 true?上文例子 conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)

      // ShardingConnection.java
      @Override
      public PreparedStatement prepareStatement(final String sql, final String[] columnNames) throws SQLException {
       return new ShardingPreparedStatement(this, sql, Statement.RETURN_GENERATED_KEYS);
      }
    
      // ShardingPreparedStatement.java
      public ShardingPreparedStatement(final ShardingConnection shardingConnection, final String sql, final int autoGeneratedKeys) {
       this(shardingConnection, sql);
       if (RETURN_GENERATED_KEYS == autoGeneratedKeys) {
           markReturnGeneratedKeys();
       }
      }
      protected final void markReturnGeneratedKeys() {
       returnGeneratedKeys = true;
      }複製程式碼

    宣告返回主鍵後,插入執行完成,我們呼叫 #getGeneratedKeys() 可以獲得主鍵 :

      // ShardingStatement.java
      @Override
      public ResultSet getGeneratedKeys() throws SQLException {
          Optional<GeneratedKey> generatedKey = getGeneratedKey();
          // 分散式主鍵
          if (generatedKey.isPresent() && returnGeneratedKeys) {
               return new GeneratedKeysResultSet(routeResult.getGeneratedKeys().iterator(), generatedKey.get().getColumn(), this);
          }
          // 資料庫自增
          if (1 == getRoutedStatements().size()) {
              return getRoutedStatements().iterator().next().getGeneratedKeys();
          }
          return new GeneratedKeysResultSet();
      }複製程式碼
  • 呼叫 ShardingConnection#getConnection() 方法獲得該 PreparedStatement 對應的真實資料庫連線( Connection ):

      // ShardingConnection.java
      /**
       * 根據資料來源名稱獲取相應的資料庫連線.
       * 
       * @param dataSourceName 資料來源名稱
       * @param sqlType SQL語句型別
       * @return 資料庫連線
       * @throws SQLException SQL異常
       */
      public Connection getConnection(final String dataSourceName, final SQLType sqlType) throws SQLException {
          // 從連線快取中獲取連線
          Optional<Connection> connection = getCachedConnection(dataSourceName, sqlType);
          if (connection.isPresent()) {
              return connection.get();
          }
          Context metricsContext = MetricsContext.start(Joiner.on("-").join("ShardingConnection-getConnection", dataSourceName));
          //
          DataSource dataSource = shardingContext.getShardingRule().getDataSourceRule().getDataSource(dataSourceName);
          Preconditions.checkState(null != dataSource, "Missing the rule of %s in DataSourceRule", dataSourceName);
          String realDataSourceName;
          if (dataSource instanceof MasterSlaveDataSource) {
              dataSource = ((MasterSlaveDataSource) dataSource).getDataSource(sqlType);
              realDataSourceName = MasterSlaveDataSource.getDataSourceName(dataSourceName, sqlType);
          } else {
              realDataSourceName = dataSourceName;
          }
          Connection result = dataSource.getConnection();
          MetricsContext.stop(metricsContext);
          // 新增到連線快取
          connectionMap.put(realDataSourceName, result);
          // 回放 Connection 方法
          replayMethodsInvocation(result);
          return result;
      }
    
      private Optional<Connection> getCachedConnection(final String dataSourceName, final SQLType sqlType) {
          String key = connectionMap.containsKey(dataSourceName) ? dataSourceName : MasterSlaveDataSource.getDataSourceName(dataSourceName, sqlType);
          return Optional.fromNullable(connectionMap.get(key));
      }複製程式碼
    • 呼叫 #getCachedConnection() 嘗試獲得已快取的資料庫連線;如果快取中不存在,獲取到連線後會進行快取
    • 從 ShardingRule 配置的 DataSourceRule 獲取真實的資料來源( DataSource )
    • MasterSlaveDataSource 實現主從資料來源封裝,我們在下小節分享
    • 呼叫 #replayMethodsInvocation() 回放記錄的 Connection 方法

插入實現的程式碼基本分享完了,因為是不斷程式碼下鑽的方式分析,可以反向向上在理理,會更加清晰

5. 查詢流程

單純從 core 包裡的 JDBC 實現,查詢流程 #executeQuery()#execute() 基本一致,差別在於執行多結果集歸併

@Override
public ResultSet executeQuery() throws SQLException {
   ResultSet result;
   try {
       // 路由
       Collection<PreparedStatementUnit> preparedStatementUnits = route();
       // 執行
       List<ResultSet> resultSets = new PreparedStatementExecutor(
               getShardingConnection().getShardingContext().getExecutorEngine(), getRouteResult().getSqlStatement().getType(), preparedStatementUnits, getParameters()).executeQuery();
       // 結果歸併
       result = new ShardingResultSet(resultSets, new MergeEngine(
               getShardingConnection().getShardingContext().getDatabaseType(), resultSets, (SelectStatement) getRouteResult().getSqlStatement()).merge());
   } finally {
       clearBatch();
   }
   // 設定結果集
   setCurrentResultSet(result);
   return result;
}複製程式碼
  • SQL執行 感興趣的同學可以看:《Sharding-JDBC 原始碼分析 —— SQL 執行》
  • 結果歸併 感興趣的同學可以看:《Sharding-JDBC 原始碼分析 —— 結果歸併》
  • 結果歸併 #merge() 完後,建立分片結果集( ShardingResultSet )

      public final class ShardingResultSet extends AbstractResultSetAdapter {
          /**
           * 歸併結果集
           */
          private final ResultSetMerger mergeResultSet;
    
          @Override
          public int getInt(final int columnIndex) throws SQLException {
              Object result = mergeResultSet.getValue(columnIndex, int.class);
              wasNull = null == result;
              return (int) ResultSetUtil.convertValue(result, int.class);
          }
    
          @Override
          public int getInt(final String columnLabel) throws SQLException {
              Object result = mergeResultSet.getValue(columnLabel, int.class);
              wasNull = null == result;
              return (int) ResultSetUtil.convertValue(result, int.class);
          }
    
          // .... 隱藏其他類似 getXXXX() 方法
      }複製程式碼

6. 讀寫分離

建議前置閱讀:《官方文件 —— 讀寫分離》

當你有讀寫分離的需求時,將 ShardingRule 配置對應的資料來源 從 ShardingDataSource 替換成 MasterSlaveDataSource。我們來看看 MasterSlaveDataSource 的功能和實現。

支援一主多從的讀寫分離配置,可配合分庫分表使用

// MasterSlaveDataSourceFactory.java
public final class MasterSlaveDataSourceFactory {
    /**
     * 建立讀寫分離資料來源.
     * 
     * @param name 讀寫分離資料來源名稱
     * @param masterDataSource 主節點資料來源
     * @param slaveDataSource 從節點資料來源
     * @param otherSlaveDataSources 其他從節點資料來源
     * @return 讀寫分離資料來源
     */
    public static DataSource createDataSource(final String name, final DataSource masterDataSource, final DataSource slaveDataSource, final DataSource... otherSlaveDataSources) {
        return new MasterSlaveDataSource(name, masterDataSource, Lists.asList(slaveDataSource, otherSlaveDataSources));
    }
}

// MasterSlaveDataSource.java
public final class MasterSlaveDataSource extends AbstractDataSourceAdapter {
    /**
     * 資料來源名
     */
    private final String name;
    /**
     * 主資料來源
     */
    @Getter
    private final DataSource masterDataSource;
    /**
     * 從資料來源集合
     */
    @Getter
    private final List<DataSource> slaveDataSources;
}複製程式碼

同一執行緒且同一資料庫連線內,如有寫入操作,以後的讀操作均從主庫讀取,用於保證資料一致性。

// ShardingConnection.java
public Connection getConnection(final String dataSourceName, final SQLType sqlType) throws SQLException {
   // .... 省略部分程式碼
   String realDataSourceName;
   if (dataSource instanceof MasterSlaveDataSource) { // 讀寫分離
       dataSource = ((MasterSlaveDataSource) dataSource).getDataSource(sqlType);
       realDataSourceName = MasterSlaveDataSource.getDataSourceName(dataSourceName, sqlType);
   } else {
       realDataSourceName = dataSourceName;
   }
   Connection result = dataSource.getConnection();
   // .... 省略部分程式碼
}

// MasterSlaveDataSource.java
/**
* 當前執行緒是否是 DML 操作標識
*/
private static final ThreadLocal<Boolean> DML_FLAG = new ThreadLocal<Boolean>() {

   @Override
   protected Boolean initialValue() {
       return false;
   }
};
/**
* 從庫負載均衡策略
*/
private final SlaveLoadBalanceStrategy slaveLoadBalanceStrategy = new RoundRobinSlaveLoadBalanceStrategy();

/**
* 獲取主或從節點的資料來源.
*
* @param sqlType SQL型別
* @return 主或從節點的資料來源
*/
public DataSource getDataSource(final SQLType sqlType) {
   if (isMasterRoute(sqlType)) {
       DML_FLAG.set(true);
       return masterDataSource;
   }
   return slaveLoadBalanceStrategy.getDataSource(name, slaveDataSources);
}

private static boolean isMasterRoute(final SQLType sqlType) {
   return SQLType.DQL != sqlType || DML_FLAG.get() || HintManagerHolder.isMasterRouteOnly();
}複製程式碼
  • ShardingConnection 獲取到的資料來源是 MasterSlaveDataSource 時,呼叫 MasterSlaveDataSource#getConnection() 方法獲取真實的資料來源
  • 通過 #isMasterRoute() 判斷是否讀取主庫,以下三種情況會訪問主庫:
    • 非查詢語句 (DQL)
    • 資料來源在當前執行緒訪問過主庫:通過執行緒變數 DML_FLAG 實現
    • 強制主庫:程式裡呼叫 HintManager.getInstance().setMasterRouteOnly() 實現
  • 訪問從庫時,會通過負載均衡策略( SlaveLoadBalanceStrategy ) 選擇一個從庫

      // SlaveLoadBalanceStrategy.java
      public interface SlaveLoadBalanceStrategy {
    
          /**
           * 根據負載均衡策略獲取從庫資料來源.
           * 
           * @param name 讀寫分離資料來源名稱
           * @param slaveDataSources 從庫資料來源列表
           * @return 選中的從庫資料來源
           */
          DataSource getDataSource(String name, List<DataSource> slaveDataSources);
      }
    
      // RoundRobinSlaveLoadBalanceStrategy.java
      public final class RoundRobinSlaveLoadBalanceStrategy implements SlaveLoadBalanceStrategy {
    
          private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap<>();
    
          @Override
          public DataSource getDataSource(final String name, final List<DataSource> slaveDataSources) {
              AtomicInteger count = COUNT_MAP.containsKey(name) ? COUNT_MAP.get(name) : new AtomicInteger(0);
              COUNT_MAP.putIfAbsent(name, count);
              count.compareAndSet(slaveDataSources.size(), 0);
              return slaveDataSources.get(count.getAndIncrement() % slaveDataSources.size());
          }
      }複製程式碼
    • MasterSlaveDataSource 預設使用 RoundRobinSlaveLoadBalanceStrategy,暫時不支援配置
    • RoundRobinSlaveLoadBalanceStrategy,輪詢負載均衡策略,每個從節點訪問次數均衡,暫不支援資料來源故障移除

666. 彩蛋

沒有彩蛋
沒有彩
沒有

下一篇,《分散式事務(一)之最大努力型》走起。老司機,趕緊上車。

道友,分享一個朋友圈可好?不然交個道姑那敏感詞你。

相關文章