1. 概述
越過千山萬水(SQL 解析、SQL 路由、SQL 改寫),我們終于來到了 SQL 執(zhí)行。開森不開森?!
查詢語句的程序入口為ShardingPreparedStatement#execute
:
public boolean execute() throws SQLException {
try {
// 路由(包括了 SQL 解析、SQL 路由、SQL 改寫)
Collection<PreparedStatementUnit> preparedStatementUnits = route();
// SQL 執(zhí)行
return new PreparedStatementExecutor(
getConnection().getShardingContext().getExecutorEngine(), routeResult.getSqlStatement().getType(), preparedStatementUnits, getParameters()).execute();
} finally {
clearBatch();
}
}
前面的文章已經(jīng)講了 SQL 解析、SQL 路由、SQL 改寫,本文繼續(xù)探討 SQL 執(zhí)行。
注意:之所以未采用常用的executeQuery
,是因為它只支持返回一個結(jié)果集ResultSet
,不符合分片的場景。
2. ExecutorEngine
ExecutorEngine,SQL執(zhí)行引擎。
分表分庫,需要執(zhí)行的 SQL 數(shù)量從單條變成了多條,此時有兩種方式執(zhí)行:
- 串行執(zhí)行 SQL
- 并行執(zhí)行 SQL
前者,編碼容易,性能較差,總耗時是多條 SQL 執(zhí)行時間累加。
后者,編碼復(fù)雜,性能較好,總耗時約等于執(zhí)行時間最長的 SQL。
ExecutorEngine 當(dāng)然采用的是后者,并行執(zhí)行 SQL。
2.1 ListeningExecutorService
Guava( Java 工具庫 ) 提供的繼承自 ExecutorService 的線程服務(wù)接口,提供創(chuàng)建 ListenableFuture 功能。ListenableFuture 接口,繼承 Future 接口,有如下好處:
我們強烈地建議你在代碼中多使用 ListenableFuture 來代替 JDK 的 Future, 因為:
1. 大多數(shù) Futures 方法中需要它。
2. 轉(zhuǎn)到 ListenableFuture 編程比較容易。
3. Guava 提供的通用公共類封裝了公共的操作方方法,不需要提供 Future 和 ListenableFuture 的擴展方法。
傳統(tǒng) JDK中 的 Future 通過異步的方式計算返回結(jié)果:在多線程運算中可能在沒有結(jié)束就返回結(jié)果。
ListenableFuture 可以允許你注冊回調(diào)方法(callbacks),在運算(多線程執(zhí)行)完成的時候進行調(diào)用。這樣簡單的改進,使得可以明顯的支持更多的操作,這樣的功能在 JDK concurrent 中的 Future 是不支持的。
下文我們看 Sharding-JDBC 是如何通過 ListenableFuture 簡化并發(fā)編程的。
先看 ExecutorEngine 如何初始化 ListeningExecutorService:
public final class ExecutorEngine implements AutoCloseable {
private final ListeningExecutorService executorService;
public ExecutorEngine(final int executorSize) {
executorService = MoreExecutors.listeningDecorator(new ThreadPoolExecutor(
executorSize, executorSize, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setDaemon(true).setNameFormat("ShardingJDBC-%d").build()));
MoreExecutors.addDelayedShutdownHook(executorService, 60, TimeUnit.SECONDS);
}
...
}
- 一個分片數(shù)據(jù)源( ShardingDataSource ) 獨占 一個 SQL執(zhí)行引擎( ExecutorEngine )。
-
MoreExecutors#listeningDecorator()
創(chuàng)建 ListeningExecutorService,這樣 #submit(), #invokeAll() 可以返回 ListenableFuture。 - 默認(rèn)情況下,線程池大小為 8。可以根據(jù)實際業(yè)務(wù)需要,設(shè)置 ShardingProperties 進行調(diào)整。
-
setNameFormat()
并發(fā)編程時,一定要對線程名字做下定義,這樣排查問題會方便很多。 -
MoreExecutors#addDelayedShutdownHook()
,應(yīng)用關(guān)閉時,等待所有任務(wù)全部完成再關(guān)閉。默認(rèn)配置等待時間為 60 秒,建議將等待時間做成可配的。
2.2 關(guān)閉
數(shù)據(jù)源關(guān)閉時,會調(diào)用 ExecutorEngine 也進行關(guān)閉。
// ShardingDataSource.java
@Override
public void close() {
executorEngine.close();
}
// ExecutorEngine
@Override
public void close() {
executorService.shutdownNow();
try {
executorService.awaitTermination(5, TimeUnit.SECONDS);
} catch (final InterruptedException ignored) {
}
if (!executorService.isTerminated()) {
throw new ShardingJdbcException("ExecutorEngine can not been terminated");
}
}
- shutdownNow() 嘗試使用 Thread.interrupt() 打斷正在執(zhí)行中的任務(wù),未執(zhí)行的任務(wù)不再執(zhí)行。
- awaitTermination() 因為 #shutdownNow() 打斷不是立即結(jié)束,需要一個過程,因此這里等待了 5 秒。
- 等待 5 秒后,線程池不一定已經(jīng)關(guān)閉,此時拋出異常給上層。建議打印下日志,記錄出現(xiàn)這個情況。
2.3 執(zhí)行 SQL 任務(wù)
ExecutorEngine 對外暴露executeStatement()
, executePreparedStatement()
, executeBatch()
三個方法分別提供給 StatementExecutor、PreparedStatementExecutor、BatchPreparedStatementExecutor 調(diào)用。而這三個方法,內(nèi)部調(diào)用的都是execute()
私有方法。
// ExecutorEngine.java
private <T> List<T> execute(
final SQLType sqlType, final Collection<? extends BaseStatementUnit> baseStatementUnits,
final List<List<Object>> parameterSets, final ExecuteCallback<T> executeCallback) throws SQLException {
if (baseStatementUnits.isEmpty()) {
return Collections.emptyList();
}
OverallExecutionEvent event = new OverallExecutionEvent(sqlType, baseStatementUnits.size());
// 發(fā)布執(zhí)行之前事件
EventBusInstance.getInstance().post(event);
Iterator<? extends BaseStatementUnit> iterator = baseStatementUnits.iterator();
BaseStatementUnit firstInput = iterator.next();
// 第二個任務(wù)開始所有 SQL任務(wù) 提交線程池【異步】執(zhí)行任務(wù)
ListenableFuture<List<T>> restFutures = asyncExecute(sqlType, Lists.newArrayList(iterator), parameterSets, executeCallback);
T firstOutput;
List<T> restOutputs;
try {
// 第一個任務(wù)【同步】執(zhí)行任務(wù)
firstOutput = syncExecute(sqlType, firstInput, parameterSets, executeCallback);
// 等待第二個任務(wù)開始所有 SQL任務(wù)完成
restOutputs = restFutures.get();
//CHECKSTYLE:OFF
} catch (final Exception ex) {
//CHECKSTYLE:ON
event.setException(ex);
event.setEventExecutionType(EventExecutionType.EXECUTE_FAILURE);
// 發(fā)布執(zhí)行失敗事件
EventBusInstance.getInstance().post(event);
ExecutorExceptionHandler.handleException(ex);
return null;
}
event.setEventExecutionType(EventExecutionType.EXECUTE_SUCCESS);
// 發(fā)布執(zhí)行成功事件
EventBusInstance.getInstance().post(event);
// 返回結(jié)果
List<T> result = Lists.newLinkedList(restOutputs);
result.add(0, firstOutput);
return result;
}
第一個任務(wù)【同步】調(diào)用executeInternal()
執(zhí)行任務(wù)。
private <T> T syncExecute(final SQLType sqlType, final BaseStatementUnit baseStatementUnit, final List<List<Object>> parameterSets, final ExecuteCallback<T> executeCallback) throws Exception {
// 【同步】執(zhí)行任務(wù)
return executeInternal(sqlType, baseStatementUnit, parameterSets, executeCallback, ExecutorExceptionHandler.isExceptionThrown(), ExecutorDataMap.getDataMap());
}
第二個開始的任務(wù)提交線程池異步調(diào)用executeInternal()
執(zhí)行任務(wù)。
private <T> ListenableFuture<List<T>> asyncExecute(
final SQLType sqlType, final Collection<BaseStatementUnit> baseStatementUnits, final List<List<Object>> parameterSets, final ExecuteCallback<T> executeCallback) {
List<ListenableFuture<T>> result = new ArrayList<>(baseStatementUnits.size());
final boolean isExceptionThrown = ExecutorExceptionHandler.isExceptionThrown();
final Map<String, Object> dataMap = ExecutorDataMap.getDataMap();
for (final BaseStatementUnit each : baseStatementUnits) {
// 提交線程池【異步】執(zhí)行任務(wù)
result.add(executorService.submit(new Callable<T>() {
@Override
public T call() throws Exception {
return executeInternal(sqlType, each, parameterSets, executeCallback, isExceptionThrown, dataMap);
}
}));
}
// 返回 ListenableFuture
return Futures.allAsList(result);
}
我們注意下Futures.allAsList(result)
和 restOutputs=restFutures.get()
。神器 Guava 簡化并發(fā)編程的好處就提現(xiàn)出來了。 ListenableFuture#get()
當(dāng)所有任務(wù)都成功時,返回所有任務(wù)執(zhí)行結(jié)果;當(dāng)任何一個任務(wù)失敗時,馬上拋出異常,無需等待其他任務(wù)執(zhí)行完成。
為什么會分同步執(zhí)行和異步執(zhí)行呢?猜測,當(dāng) SQL 執(zhí)行是單表時,只要進行第一個任務(wù)的同步調(diào)用,性能更加優(yōu)秀。
// ExecutorEngine.java
private <T> T executeInternal(final SQLType sqlType, final BaseStatementUnit baseStatementUnit, final List<List<Object>> parameterSets, final ExecuteCallback<T> executeCallback,
final boolean isExceptionThrown, final Map<String, Object> dataMap) throws Exception {
synchronized (baseStatementUnit.getStatement().getConnection()) {
T result;
ExecutorExceptionHandler.setExceptionThrown(isExceptionThrown);
ExecutorDataMap.setDataMap(dataMap);
List<AbstractExecutionEvent> events = new LinkedList<>();
if (parameterSets.isEmpty()) {
// 生成 Event
events.add(getExecutionEvent(sqlType, baseStatementUnit, Collections.emptyList()));
}
for (List<Object> each : parameterSets) {
events.add(getExecutionEvent(sqlType, baseStatementUnit, each));
}
// EventBus 發(fā)布 EventExecutionType.BEFORE_EXECUTE
for (AbstractExecutionEvent event : events) {
EventBusInstance.getInstance().post(event);
}
try {
// 執(zhí)行回調(diào)函數(shù)
result = executeCallback.execute(baseStatementUnit);
} catch (final SQLException ex) {
// EventBus 發(fā)布 EventExecutionType.EXECUTE_FAILURE
for (AbstractExecutionEvent each : events) {
each.setEventExecutionType(EventExecutionType.EXECUTE_FAILURE);
each.setException(ex);
EventBusInstance.getInstance().post(each);
ExecutorExceptionHandler.handleException(ex);
}
return null;
}
// EventBus 發(fā)布 EventExecutionType.EXECUTE_SUCCESS
for (AbstractExecutionEvent each : events) {
each.setEventExecutionType(EventExecutionType.EXECUTE_SUCCESS);
EventBusInstance.getInstance().post(each);
}
return result;
}
}
result=executeCallback.execute(baseStatementUnit)
執(zhí)行回調(diào)函數(shù)。StatementExecutor,PreparedStatementExecutor,BatchPreparedStatementExecutor 通過傳遞執(zhí)行回調(diào)函數(shù)( ExecuteCallback )實現(xiàn)給 ExecutorEngine 實現(xiàn)并行執(zhí)行。
public interface ExecuteCallback<T> {
/**
* 執(zhí)行任務(wù).
*
* @param baseStatementUnit 語句對象執(zhí)行單元
* @return 處理結(jié)果
* @throws Exception 執(zhí)行期異常
*/
T execute(BaseStatementUnit baseStatementUnit) throws Exception;
}
synchronized(baseStatementUnit.getStatement().getConnection())
,這里加鎖的原因是,雖然 MySQL、Oracle 的 Connection 實現(xiàn)是線程安全的。但是數(shù)據(jù)庫連接池實現(xiàn)的 Connection 不一定是線程安全,例如 Druid 的線程池 Connection 非線程安全。
3. Executor
Executor,執(zhí)行器,目前一共有三個執(zhí)行器。不同的執(zhí)行器對應(yīng)不同的執(zhí)行單元 (BaseStatementUnit)。
執(zhí)行器類 | 執(zhí)行器名 | 執(zhí)行單元 |
---|---|---|
StatementExecutor | 靜態(tài)語句對象執(zhí)行單元 | StatementUnit |
PreparedStatementExecutor | 預(yù)編譯語句對象請求的執(zhí)行器 | PreparedStatementUnit |
BatchPreparedStatementExecutor | 批量預(yù)編譯語句對象請求的執(zhí)行器 | BatchPreparedStatementUnit |
3.1 StatementExecutor
StatementExecutor,多線程執(zhí)行靜態(tài)語句對象請求的執(zhí)行器,一共有三類方法:
- executeQuery() 執(zhí)行 SQL 查詢
public List<ResultSet> executeQuery() throws SQLException {
return executorEngine.executeStatement(sqlType, statementUnits, new ExecuteCallback<ResultSet>() {
@Override
public ResultSet execute(final BaseStatementUnit baseStatementUnit) throws Exception {
return baseStatementUnit.getStatement().executeQuery(baseStatementUnit.getSqlExecutionUnit().getSql());
}
});
}
- executeUpdate() 執(zhí)行 SQL 更新
public int executeUpdate() throws SQLException {
return executeUpdate(new Updater() {
@Override
public int executeUpdate(final Statement statement, final String sql) throws SQLException {
return statement.executeUpdate(sql);
}
});
}
- execute() 執(zhí)行 SQL
public boolean execute() throws SQLException {
return execute(new Executor() {
@Override
public boolean execute(final Statement statement, final String sql) throws SQLException {
return statement.execute(sql);
}
});
}
3.2 PreparedStatementExecutor
PreparedStatementExecutor,多線程執(zhí)行預(yù)編譯語句對象請求的執(zhí)行器。比 StatementExecutor 多了parameters
參數(shù),方法邏輯上基本一致,就不重復(fù)分享啦。
3.3 BatchPreparedStatementExecutor
BatchPreparedStatementExecutor,多線程執(zhí)行批量預(yù)編譯語句對象請求的執(zhí)行器。
// BatchPreparedStatementExecutor.java
public final class BatchPreparedStatementExecutor {
private final ExecutorEngine executorEngine;
private final DatabaseType dbType;
private final SQLType sqlType;
private final Collection<BatchPreparedStatementUnit> batchPreparedStatementUnits;
private final List<List<Object>> parameterSets;
/**
* Execute batch.
*
* @return execute results
* @throws SQLException SQL exception
*/
public int[] executeBatch() throws SQLException {
return accumulate(executorEngine.executeBatch(sqlType, batchPreparedStatementUnits, parameterSets, new ExecuteCallback<int[]>() {
@Override
public int[] execute(final BaseStatementUnit baseStatementUnit) throws Exception {
return baseStatementUnit.getStatement().executeBatch();
}
}));
}
// 計算每個語句的更新數(shù)量
private int[] accumulate(final List<int[]> results) {
int[] result = new int[parameterSets.size()];
int count = 0;
// 每個語句按照順序,讀取到其對應(yīng)的每個分片SQL影響的行數(shù)進行累加
for (BatchPreparedStatementUnit each : batchPreparedStatementUnits) {
for (Map.Entry<Integer, Integer> entry : each.getJdbcAndActualAddBatchCallTimesMap().entrySet()) {
int value = null == results.get(count) ? 0 : results.get(count)[entry.getValue()];
if (DatabaseType.Oracle == dbType) {
result[entry.getKey()] = value;
} else {
result[entry.getKey()] += value;
}
}
count++;
}
return result;
}
}
眼尖的同學(xué)會發(fā)現(xiàn),為什么有 BatchPreparedStatementExecutor,而沒有 BatchStatementExecutor 呢?目前 Sharding-JDBC 不支持 Statement 批量操作,只能進行 PreparedStatement 的批操作。
4. ExecutionEvent
AbstractExecutionEvent,SQL 執(zhí)行事件抽象接口。
public abstract class AbstractExecutionEvent {
// 事件編號
@Getter
private final String id = UUID.randomUUID().toString();
// 事件類型
@Getter
@Setter
private EventExecutionType eventExecutionType = EventExecutionType.BEFORE_EXECUTE;
@Setter
private Exception exception;
public Optional<? extends Exception> getException() {
return Optional.fromNullable(exception);
}
}
AbstractExecutionEvent 的子類關(guān)系圖為:
- DMLExecutionEvent:DML類 SQL 執(zhí)行時事件
- DQLExecutionEvent:DQL類 SQL 執(zhí)行時事件
EventExecutionType,事件觸發(fā)類型。
- BEFORE_EXECUTE:執(zhí)行前
- EXECUTE_SUCCESS:執(zhí)行成功
- EXECUTE_FAILURE:執(zhí)行失敗
4.1 EventBus
那究竟有什么用途呢? Sharding-JDBC 使用 Guava(沒錯,又是它)的 EventBus 實現(xiàn)了事件的發(fā)布和訂閱。從上文 ExecutorEngine#executeInternal()
我們可以看到每個分片 SQL 執(zhí)行的過程中會發(fā)布相應(yīng)事件:
- 執(zhí)行 SQL 前:發(fā)布類型類型為 BEFORE_EXECUTE 的事件
- 執(zhí)行 SQL 成功:發(fā)布類型類型為 EXECUTE_SUCCESS 的事件
- 執(zhí)行 SQL 失敗:發(fā)布類型類型為 EXECUTE_FAILURE 的事件
怎么訂閱事件呢(目前 Sharding-JDBC 是沒有訂閱這些事件的,只是提供了事件發(fā)布訂閱的功能而已)?非常簡單,例子如下:
EventBusInstance.getInstance().register(new Runnable() {
@Override
public void run() {
}
@Subscribe // 訂閱
@AllowConcurrentEvents // 是否允許并發(fā)執(zhí)行,即線程安全
public void listen(final DMLExecutionEvent event) { // DMLExecutionEvent
System.out.println("DMLExecutionEvent:" + event.getSql() + "\t" + event.getEventExecutionType());
}
@Subscribe // 訂閱
@AllowConcurrentEvents // 是否允許并發(fā)執(zhí)行,即線程安全
public void listen2(final DQLExecutionEvent event) { //DQLExecutionEvent
System.out.println("DQLExecutionEvent:" + event.getSql() + "\t" + event.getEventExecutionType());
}
});
- register() 任何類都可以,并非一定需要使用 Runnable 類。此處例子單純因為方便
- @Subscribe 注解在方法上,實現(xiàn)對事件的訂閱
- @AllowConcurrentEvents 注解在方法上,表示線程安全,允許并發(fā)執(zhí)行
- 方法上的參數(shù)對應(yīng)的類即是訂閱的事件。例如, #listen() 訂閱了 DMLExecutionEvent 事件
- EventBus#post() 發(fā)布事件,同步調(diào)用訂閱邏輯
5. 結(jié)語
SQL 執(zhí)行完畢之后,執(zhí)行結(jié)果封裝在ResultSet
對象中,如:
Statement stmt =con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT a, b FROM TABLE2");
多個 SQL 執(zhí)行結(jié)果就會有多個ResultSet
,必然需要進行合并。下一篇文章我們將探討 SQL 結(jié)果歸并,敬請關(guān)注~