Solr 6.5.1集群部署和后臺管理

  • 兩年前用過solr5.1版本的,當時只是簡單入個門,拿來在項目里建個全文索引,然后再query,其他什么也沒做,還是傻傻地自己去配Tomcat,這次做畢設因為需求而重拾solr,基于作死折騰的原則,肯定要用最新版的啦~,然后。。。還是很順利踩到坑了,schema相關配置變了。。。配置變了。。。置變了。。。變了。。。了。。。不過還是很快解決了,當然這次打算把集群高可用相關也折騰折騰、還有和mysql之間的配合和索引同步,所以新寫一篇看起來不那么low的博客出來(之前寫過一篇弱文。。。)

下載和初始化啟動

官網下載6.5.1版本的solr,解壓

目錄說明:
  • bin:啟動腳本
  • contrib:第三方貢獻拓展jar包,包括一些第三方分詞器
  • dist:一些最終生成的jar包,主要是solrj客戶端jar包
  • docs:文檔
  • example:一些樣例,example-DIH目錄下的是一些solr索引core樣例
  • server:solr服務端工作目錄,自帶集成jetty插件方式啟動solr服務器
    • solr:solr搜索引擎工作目錄,即solr/home
    • solr-webapp:solr后臺管理頁面webapp
    • start.jar:服務端啟動jar包
準備工作
  1. contrib/analysis-extras/lucene-libs:這個目錄下是第三方分詞器jar,將其拷貝到server/solr-webapp/webapp/WEB-INF/lib下供webapp使用
  2. dist/solrj-lib:這里是solrj客戶端的依賴jar包,使用maven的話用如下坐標即可
<dependency>
       <groupId>org.apache.solr</groupId>
       <artifactId>solr-solrj</artifactId>
       <version>6.5.1</version>
</dependency>
  1. example/example-DIH/solr:該目錄下自帶了初始的五個索引庫core,將其拷貝到server/solr目錄下,solr服務端啟動才會自動引入core到系統(tǒng)中(solr.xml不要復制)
  2. server/solr:步驟3拷貝過來的五個core中有conf一系列配置文件可以配置這個core相關參數(shù),后面專門細講
索引庫core配置

到配置好的server/solr目錄下,找到db目錄為例,db/conf目錄下一堆的配置文件,主要配置的為如下:

  • db/lib:存放該core需要的額外jar,比如第三方分詞器,在該目錄下的分詞器中的類可以在schema配置時直接全限定類名引入
  • db/conf/managed-schema:schema配置文件,該文件為系統(tǒng)REST API配置時系統(tǒng)的合成文件,不建議手動修改
  • db/conf/schema.xml:我自己把managed-schema復制一份重命名的,用于手動配置schema
  • db/conf/solrconfig.xml:該core的總配置文件

說明

  • 系統(tǒng)生成和手動編輯可能產生重疊,系統(tǒng)生成的編輯可能會刪除注釋或者其他幫助理解域、域類型的關鍵性的自定義內容。你可能希望通過源碼控制標記(配置)文件的版本,或者同時限制手動編輯。
    Solrconfig.xml允許Solr schema被定義為“被管理的索引模式”:只能通過Schema API修改schema。
  • managed-schema是solr開始才有的schema配置文件,它是用來避免API方式配置schema和手動修改schema配置文件之間的沖突(比如說重復字段名),因此這個文件不建議手動修改
  • 可以把managed-schema復制一份schema.xml,然后在這個文件手動配置schema
  • 這兩個schema配置文件solr系統(tǒng)如何識別呢?答案是兩個配置是不能同時生效的,系統(tǒng)默認使用managed-schema,支持REST API方式動態(tài)配置schema,適合有動態(tài)配置schema、field、fieldType之類需求的就使用這個配置,什么都不改即可。
  • 若項目沒有動態(tài)修改schema的需求,那可以使用schema.xml進行配置,在其中可以很方便根據schema配置語法進行個性化自定義,方便編碼,若要schema.xml生效,需要按照如下步驟修改db/conf/solrconfig.xml的配置:
  1. 找到如下配置(6.5.1其實默認沒有這個配置,自己直接添加就行):
<schemaFactory class="ManagedIndexSchemaFactory">
    <bool name="mutable">true</bool>
    <str name="managedSchemaResourceName">managed-schema</str>
</schemaFactory>
  1. 替換成:
<schemaFactory class="ClassicIndexSchemaFactory"/>
Schema手動配置Field和FieldType
<!-- 配置一個新field,設定類型 -->
<field name="text_smart" type="text_smart" indexed="true" stored="true" multiValued="true"/>
<!-- 配置一個新的類型,指定分詞器 -->
<fieldType name="text_smart" class="solr.TextField" positionIncrementGap="0">
    <analyzer type="index">
      <tokenizer class="org.apache.lucene.analysis.cn.smart.HMMChineseTokenizerFactory"/>
    </analyzer>
    <analyzer type="query">
       <tokenizer class="org.apache.lucene.analysis.cn.smart.HMMChineseTokenizerFactory"/>
    </analyzer>
</fieldType>
<field name="title" type="text_smart" indexed="true" stored="true" multiValued="true"/>
<field name="text_all" type="text_smart" indexed="true" stored="false" multiValued="true" />
<field name="filename" type="text_smart" indexed="true" stored="true" multiValued="true"/>
<field name="filecontent" type="text_smart" indexed="true" stored="false" multiValued="true"/>
<!-- 為多個field指定一個共同的別名,通過這個別名進行query就能夠同時對所有field進行搜索 -->
<copyField source="title" dest="text_all"/>
<copyField source="text_smart" dest="text_all"/>
<copyField source="filename" dest="text_all"/>
<copyField source="filecontent" dest="text_all"/>
<copyField source="id" dest="text_all"/>
啟動solr
bin/solr start

啟動如下:

solr-start.png

瀏覽器訪問http://localhost:8983/,如下:
solr-admin.png

關閉solr
bin/solr stop

關閉如下:


solr-stop.png

集群部署

Solr集群架構圖
solr-cloud-struct.png
  • 圖的上半部分是物理結構,一看就清楚,需要關注的是每個機器節(jié)點都可以有多個core,這個和下面的邏輯結構有比較大的聯(lián)系。
  • 下半部分邏輯結構:一個Collection就是一個完整的索引集,可以理解為一個索引文件,Shard1和Shard2就是該索引集的多個分片,就是說把完整的一個索引文件分成了多個,類似數(shù)據庫分庫分表,不過你在訪問的時候只需要訪問Collection即可,內部分片和索引所在的分片位置不需要關心,這樣對不同個分片的訪問就可以分流到不同機器上就行處理,也避免了單個索引文件無限膨脹的可能。
  • 再往上就是副本,每個Shard都可以有多個副本,類比數(shù)據庫讀寫分離,寫操作由Master節(jié)點接收,并自動同步到Slave節(jié)點,當一個Master節(jié)點宕機,就會自動選舉一個Slave節(jié)點重新承擔Master的功能,這里的每個副本就如圖所示,對應每個Solr節(jié)點的一個Core,這樣邏輯和物理之間的聯(lián)系就建立起來了。
高可用
  • SolrCloud集群由ZooKeeper進行協(xié)調,集群中所有節(jié)點使用ZooKeeper中共有的配置文件,這樣就能保證各個節(jié)點之間的配置統(tǒng)一。
  • SolrCloud節(jié)點宕機、Master選舉等都通過ZooKeeper的分布式協(xié)調實現(xiàn),所以說SolrJ的訪問地址也是ZooKeeper的地址。
SolrCloud集群部署
  • 這里比較重要了,由于Solr的版本間兼容性較差,又因為我這是當前最新版,因此在集群部署過程中遇到很多坑,主要是查的資料各種版本都有,還分成tomcat和jetty兩種部署方式,搞得我在試驗各種方案的過程中浪費了很多時間,好在最后讓我找到了一個比較可靠的博客,很詳細地用最簡單的jetty方式部署,其實從各方面性能看,jetty不比tomcat差,而且現(xiàn)在都流行直接nginx反向代理和負載均衡以及微服務,我反而更喜歡jetty了。。。
  1. ZooKeeper集群部署,這在我之前的博客有,需要的自行查看,我就直接那部署好的ZooKeeper來用而已:ZooKeeper配置和學習筆記
  2. 官網下載solr-6.5.1.tgz,解壓并到該目錄下
  3. 上傳配置文件到ZooKeeper(之后整個集群所有機器就是共用這套配置文件了):
./bin/solr zk -z localhost:2181/solr -upconfig -n solr -d example/example-DIH/solr/solr

參數(shù)說明:

-upconfig      表示把你的配置文件上傳到ZooKeeper集群
-n configName  指定這個配置的名稱,solr管理頁面會用到
-d confdir     要上傳的配置文件在本地的地址,默認使用解壓的example/example-DIH/solr/solr即可,若有配置需求的,可實現(xiàn)修改相應的配置,再上傳到ZooKeeper(配置方法同上面的單節(jié)點模式)
-z zkHost      Zookeeper集群地址
  1. 把剛才解壓的整個solr-6.5.1目錄額外復制兩份(偽分布式,你若有多臺機器可考慮真實分布式部署)
  2. 啟動solr節(jié)點服務:
./bin/solr start -c -m 1g -z localhost:2181/solr -p 8983

參數(shù)說明:

-c:cloud模式啟動
-m:最大內存使用
-z:zookeeper集群地址
-p:啟動服務端口
  1. 相同方式啟動另外兩個節(jié)點:(端口號錯開)
solr-2/bin/solr start -c -m 1g -z localhost:2181/solr -p 8984
solr-2/bin/solr start -c -m 1g -z localhost:2181/solr -p 8985
  1. 瀏覽器訪問:http://localhost:8983/solr/#/
  2. 如圖創(chuàng)建Collection:


    solr6.0.1-collections-addCollection.png
  • 配置參數(shù)說明:
name: 待創(chuàng)建Collection的名稱
config set: collection在zookeeper中的配置目錄
numShards: 分片的數(shù)量
replicationFactor: 復制副本的數(shù)量
maxShardsPerNode:默認值為1,注意三個數(shù)值:numShards、replicationFactor、liveSolrNode,一個正常的solrCloud集群不容許同一個liveSolrNode上部署同一個shard的多個replic,因此當maxShardsPerNode=1時,numShards*replicationFactor>liveSolrNode時,報錯。因此正確時因滿足以下條件:
numShards*replicationFactor<liveSolrNode*maxShardsPerNode
  1. 查看各個節(jié)點和分片的關系圖:


    solr-6.5.1-collection-graph.png
  • 至此,SolrCloud集群部署完成。
  • 可以看到solr2這個Collection有shard1、shard2、shard3三個分片,每個分片有兩個副本,一個Master、一個Slave,黑點為當前處于Master的副本節(jié)點。
  • 這時候我們隨便打開一個solr節(jié)點的目錄server/solr,如下:


    solr-6.5.1-node-date.png
  • 可知,剛才管理界面創(chuàng)建的Collection節(jié)點的分片以core的形式存在于每個節(jié)點的core列表目錄下,進入其中一個文件夾查看:


    solr-6.5.1-node-date-dir.png
  • 只有core.properties和data,上面的單節(jié)點模式每個core目錄下還有l(wèi)ib和conf的,但是這里沒有,下面圖示說明:


    Solr-Cloud-Core.png

客戶端訪問操作

單節(jié)點訪問
/**
 * Solrj客戶端工具類,實現(xiàn)對Solr服務的創(chuàng)建索引和查找操作
 *
 * @author linyuqiang 2017/05/10
 */
public class SolrClient {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    /**
     * Solr本地地址
     */
    private String url = "http://localhost:8983/solr/db";
    //查詢字符串key
    private String searchKey = "text_all";
    private boolean highlighting = true;
    private String highlightingPre = "<em>";
    private String highlightingPost = "</em>";
    private String highlightingField = "content";
    // Solr客戶端對象
    private HttpSolrClient client;

    public SolrClient open() {
        // 1.創(chuàng)建SolrServer對象,以下這兩個是線程安全的SolrServer實現(xiàn)類
        // CommonsHttpSolrServer 基于Http協(xié)議進行C/S數(shù)據交互
        // EmbeddedSolrServer
        // 內嵌式,只要設定好solr的home目錄即可實現(xiàn)和solr的交互,不需要開啟solr的服務器,本地交互
        client = new HttpSolrClient(url);
        client.setSoTimeout(10000); // socket read timeout
        client.setConnectionTimeout(1000);
        client.setDefaultMaxConnectionsPerHost(100);
        client.setMaxTotalConnections(100);
        client.setFollowRedirects(false); // defaults to false
        // allowCompression defaults to false.
        // Server side must support gzip or deflate for this to have any effect.
        client.setAllowCompression(true);
        return this;
    }

    /**
     * 查找,分頁查詢
     *
     * @param qString 查找字符串
     * @return 返回查詢結果列表
     */
    public List<SolrjMessage> search(String qString, Integer pageNum, Integer pageSiez) {
        try {
            SolrQuery query = new SolrQuery(searchKey + ":" + qString);// 查詢字符串
            query.setStart((pageNum - 1) * pageSiez);// 設置查詢開始下標
            query.setRows(pageSiez);// 查詢行數(shù)

            //高亮配置
            query.setHighlight(highlighting);
            query.setHighlightSimplePre(highlightingPre);
            query.setHighlightSimplePost(highlightingPost);
            query.addHighlightField(highlightingField);

            QueryResponse response = client.query(query);// 獲取查詢返回對象

            SolrDocumentList docs = response.getResults();// 獲取查詢得到的所有Document
            //查詢高亮信息
            Map<String, Map<String, List<String>>> highlightings = response.getHighlighting();
            List<SolrjMessage> list = new ArrayList<SolrjMessage>();
            for (SolrDocument doc : docs) {
                // 獲取每個Document的詳細信息
                SolrjMessage message = new SolrjMessage();
                message.setId((String) doc.getFieldValue("id"));
                message.setUrl((String) doc.getFieldValue("url"));

                //高亮信息
                List<String> hights = highlightings.get(message.getId() + "").get(highlightingField);
                StringBuilder highlighting = new StringBuilder();
                for (int i = 0; i < hights.size(); i++) {
                    highlighting.append(hights.get(i)).append(" ");
                }
                message.setHighlighting(highlighting.toString());

                list.add(message);
            }

            //其他查詢信息
            NamedList responseHeader = response.getResponseHeader();
            SimpleOrderedMap params = (SimpleOrderedMap) responseHeader.get("params");
            NamedList<Object> responseResponse = response.getResponse();
            SolrDocumentList responseBean = (SolrDocumentList) responseResponse.get("response");

            int qTime = response.getQTime();
            int status = response.getStatus();
            Object q = params.get("q");
            Object fq = params.get("fq");
            Object numFound = responseBean.getNumFound();
            Object start = responseBean.getStart();
            return list;
        } catch (SolrServerException e) {
            logger.error("solr服務異常", e);
            throw new RuntimeException("solr服務異常", e);
        } catch (IOException e) {
            logger.error("solr服務IO異常", e);
            throw new RuntimeException("solr服務IO異常", e);
        }
    }

    /**
     * Solr創(chuàng)建索引
     *
     * @param message 創(chuàng)建索引的文件
     */
    public SolrClient createIndex(SolrjMessage message) {
        SolrInputDocument doc = new SolrInputDocument();
        doc.addField("id", message.getId());
        doc.addField("url", message.getUrl());
        doc.addField("content", message.getContent());
        try {
            client.add(doc);
            client.commit();
        } catch (SolrServerException e) {
            logger.error("solr服務異常", e);
            throw new RuntimeException("solr服務異常", e);
        } catch (IOException e) {
            logger.error("solr服務IO異常", e);
            throw new RuntimeException("solr服務IO異常", e);
        }
        return this;
    }

    public void deleteIndex() throws IOException, SolrServerException {
        UpdateResponse response = client.deleteByQuery("docName:testCatcher");
        client.commit();
        int status = response.getStatus();
        System.out.println("status = " + status);
    }
}
集群訪問
  • client對象的初始化方式改變,其他創(chuàng)建索引、刪除索引、搜索的代碼都和單節(jié)點一樣。
    /**
     * ZooKeeper地址
     */
    private String zookeeperUrl = "{zookeeperHost}:2181/solr";
    //查詢字符串key
    private String searchKey = "name";
    private boolean highlighting = true;
    private String highlightingPre = "<em>";
    private String highlightingPost = "</em>";
    private String highlightingField = "content";
    // Solr客戶端對象
    private CloudSolrClient client;

    public CloudSolrClient1 open() {
        // 1.創(chuàng)建SolrServer對象,以下這兩個是線程安全的SolrServer實現(xiàn)類
        // CommonsHttpSolrServer 基于Http協(xié)議進行C/S數(shù)據交互
        // EmbeddedSolrServer
        // 內嵌式,只要設定好solr的home目錄即可實現(xiàn)和solr的交互,不需要開啟solr的服務器,本地交互
        client = new CloudSolrClient(zookeeperUrl);
        client.setSoTimeout(10000); // socket read timeout
        //指定Collection名稱
        client.setDefaultCollection("solr");
        client.setZkClientTimeout(30000);
        client.setZkConnectTimeout(30000);
        client.setSoTimeout(30000);
        return this;
    }

管理界面使用說明

  • Collection管理界面,可以添加、刪除、查看、別名Collection


    Collection-manager.png
  • 選中其中一個Collection可以進行操作的菜單欄


    solr-6.5.1-overview.png
  • 分詞器測試頁面,可以對自己配置的分詞器手動輸入字符串進行分詞結果測試


    solr-6.5.1-analysis.png
  • 手動輸入document的測試頁面,使用不多


    solr-6.5.1-document.png
  • 查看當前Collection的配置文件列表,使用不多


    solr-6.5.1-files.png
  • 搜索查詢測試頁面,支持分頁、排序、高亮、過濾等查詢參數(shù)的配置,執(zhí)行按鈕點擊之后可以在右邊頁面查看查詢的結果信息


    solr-6.5.1-query.png
  • Schema管理頁面,可以增加、刪除、查看三種類型的Field


    solr-6.5.1-schema.png
  • 選中其中一個Core可以進行操作的菜單欄


    solr-6.5.1-core.png

數(shù)據庫索引同步

  1. solrconfig.xml配置數(shù)據導入加載類:
<requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
        <lst name="defaults">
            <str name="config">solr-data-config.xml</str>
        </lst>
</requestHandler>
  • 要放在<requestHandler name="/select" class="solr.SearchHandler">之前
  1. 相同目錄下創(chuàng)建solr-data-config.xml配置數(shù)據庫連接信息:
<dataConfig>
  <dataSource name="source1" type="JdbcDataSource" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/doc_searcher" user="root" password="" batchSize="-1" />  
  <document>  
        <entity name="test" pk="id"  dataSource="source1"   
                query="select * from  test"  
                 deltaImportQuery="select * from test where id='${dih.delta.id}'"  
                deltaQuery="select id from test where create_time > '${dataimporter.last_index_time}'">  
          <field column="id" name="id"/>  
            <field column="name" name="name"/>  
            <field column="create_time" name="createTime"/>  
     </entity>  
  </document>
</dataConfig>
  • 很簡單,照抄小改就行,用jdbc四要素進行連接,delta相關參數(shù)是增量導入用的,solr內部維護好上次導入的id和時間戳,這次增量導入就根據內部維護的時間戳進行對比,把變化的部分重新導入到solr。
  • 需要注意的是<field>標簽的name屬性要在solr的該core的schema進行相應的配置,保證這些field是存在的才能正常運行。
  1. server/solr-webapp/webapp/WEB-INF/lib放入mysql的jdbc驅動包
  2. 清空該Core中的所有數(shù)據,或者新建一個全新的Core
  3. 到solr管理頁面導入數(shù)據即可:


    solr-6.5.1-dataimport.png
其他用到的命令
  • 本地配置同步到ZooKeeper:(一個個文件同步)
./server/scripts/cloud-scripts/zkcli.sh -zkhost localhost:2181 -cmd putfile /solr/configs/solr/solrconfig.xml  example/example-DIH/solr/solr/conf/solrconfig.xml
  • 重新加載solr集群的節(jié)點配置:(同步ZooKeeper之后免重啟集群,重新加載配置信息)
http://localhost:8983/solr/admin/collections?action=RELOAD&name=solr
  • 從數(shù)據源導入數(shù)據到Solr
http://localhost:8983/solr/solr2/dataimport?command=full-import&commit=ture

感謝

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

推薦閱讀更多精彩內容