數(shù)據(jù)庫中間件 Sharding-JDBC 源碼分析 —— SQL 執(zhí)行

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)注~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,621評論 2 380