在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

ElasticSearch深度分頁詳解

OSC開源社區 ? 來源:OSC開源社區 ? 作者:何守優 ? 2022-11-17 09:53 ? 次閱讀

1 前言

ElasticSearch 是一個實時的分布式搜索與分析引擎,常用于大量非結構化數據的存儲和快速檢索場景,具有很強的擴展性。縱使其有諸多優點,在搜索領域遠超關系型數據庫,但依然存在與關系型數據庫同樣的深度分頁問題,本文就此問題做一個實踐性分析探討。

2 from + size 分頁方式

from + size 分頁方式是 ES 最基本的分頁方式,類似于關系型數據庫中的 limit 方式。from 參數表示:分頁起始位置;size 參數表示:每頁獲取數據條數。例如:

GET /wms_order_sku/_search
{
  "query": {
    "match_all": {}
  },
  "from": 10,
  "size": 20
}
該條 DSL 語句表示從搜索結果中第 10 條數據位置開始,取之后的 20 條數據作為結果返回。這種分頁方式在 ES 集群內部是如何執行的呢? 在 ES 中,搜索一般包括 2 個階段,Query 階段和 Fetch 階段,Query 階段主要確定要獲取哪些 doc,也就是返回所要獲取 doc 的 id 集合,Fetch 階段主要通過 id 獲取具體的 doc。

2.1 Query 階段

d1d8917e-65b4-11ed-8abf-dac502259ad0.png 如上圖所示,Query 階段大致分為 3 步:

第一步:Client 發送查詢請求到 Server 端,Node1 接收到請求然后創建一個大小為 from + size 的優先級隊列用來存放結果,此時 Node1 被稱為 coordinating node(協調節點);

第二步:Node1 將請求廣播到涉及的 shard 上,每個 shard 內部執行搜索請求,然后將執行結果存到自己內部的大小同樣為 from+size 的優先級隊列里;

第三步:每個 shard 將暫存的自身優先級隊列里的結果返給 Node1,Node1 拿到所有 shard 返回的結果后,對結果進行一次合并,產生一個全局的優先級隊列,存在 Node1 的優先級隊列中。(如上圖中,Node1 會拿到 (from + size) * 6 條數據,這些數據只包含 doc 的唯一標識_id 和用于排序的_score,然后 Node1 會對這些數據合并排序,選擇前 from + size 條數據存到優先級隊列);

2.2 Fetch 階段

d1e84ff6-65b4-11ed-8abf-dac502259ad0.png

如上圖所示,當 Query 階段結束后立馬進入 Fetch 階段,Fetch 階段也分為 3 步:

第一步:Node1 根據剛才合并后保存在優先級隊列中的 from+size 條數據的 id 集合,發送請求到對應的 shard 上查詢 doc 數據詳情;

第二步:各 shard 接收到查詢請求后,查詢到對應的數據詳情并返回為 Node1;(Node1 中的優先級隊列中保存了 from + size 條數據的_id,但是在 Fetch 階段并不需要取回所有數據,只需要取回從 from 到 from + size 之間的 size 條數據詳情即可,這 size 條數據可能在同一個 shard 也可能在不同的 shard,因此 Node1 使用 multi-get 來提高性能)

第三步:Node1 獲取到對應的分頁數據后,返回給 Client;

2.3 ES 示例

依據上述我們對 from + size 分頁方式兩階段的分析會發現,假如起始位置 from 或者頁條數 size 特別大時,對于數據查詢和 coordinating node 結果合并都是巨大的性能損耗。 例如:索引 wms_order_sku 有 1 億數據,分 10 個 shard 存儲,當一個請求的 from = 1000000, size = 10。在 Query 階段,每個 shard 就需要返回 1000010 條數據的_id 和_score 信息,而 coordinating node 就需要接收 10 * 1000010 條數據,拿到這些數據后需要進行全局排序取到前 1000010 條數據的_id 集合保存到 coordinating node 的優先級隊列中,后續在 Fetch 階段再去獲取那 10 條數據的詳情返回給客戶端。 分析:這個例子的執行過程中,在 Query 階段會在每個 shard 上均有巨大的查詢量,返回給 coordinating node 時需要執行大量數據的排序操作,并且保存到優先級隊列的數據量也很大,占用大量節點機器內存資源。

2.4 實現示例

d1f8b030-65b4-11ed-8abf-dac502259ad0.png

private SearchHits getSearchHits(BoolQueryBuilder queryParam, int from, int size, String orderField) {
        SearchRequestBuilder searchRequestBuilder = this.prepareSearch();
        searchRequestBuilder.setQuery(queryParam).setFrom(from).setSize(size).setExplain(false);
        if (StringUtils.isNotBlank(orderField)) {
            searchRequestBuilder.addSort(orderField, SortOrder.DESC);
        }
        log.info("getSearchHits searchBuilder:{}", searchRequestBuilder.toString());
        SearchResponse searchResponse = searchRequestBuilder.execute().actionGet();
        log.info("getSearchHits searchResponse:{}", searchResponse.toString());
        return searchResponse.getHits();
    }

2.5 小結

其實 ES 對結果窗口的返回數據有默認 10000 條的限制(參數:index.max_result_window = 10000),當 from + size 的條數大于 10000 條時 ES 提示可以通過 scroll 方式進行分頁,非常不建議調大結果窗口參數值。 d23e80b0-65b4-11ed-8abf-dac502259ad0.png

3 Scroll 分頁方式

scroll 分頁方式類似關系型數據庫中的 cursor(游標),首次查詢時會生成并緩存快照,返回給客戶端快照讀取的位置參數(scroll_id),后續每次請求都會通過 scroll_id 訪問快照實現快速查詢需要的數據,有效降低查詢和存儲的性能損耗。

3.1 執行過程

scroll 分頁方式在 Query 階段同樣也是 coordinating node 廣播查詢請求,獲取、合并、排序其他 shard 返回的數據_id 集合,不同的是 scroll 分頁方式會將返回數據_id 的集合生成快照保存到 coordinating node 上。Fetch 階段以游標的方式從生成的快照中獲取 size 條數據的_id,并去其他 shard 獲取數據詳情返回給客戶端,同時將下一次游標開始的位置標識_scroll_id 也返回。這樣下次客戶端發送獲取下一頁請求時帶上 scroll_id 標識,coordinating node 會從 scroll_id 標記的位置獲取接下來 size 條數據,同時再次返回新的游標位置標識 scroll_id,這樣依次類推直到取完所有數據。

3.2 ES 示例

第一次查詢時不需要傳入_scroll_id,只要帶上 scroll 的過期時間參數(scroll=1m)、每頁大小(size)以及需要查詢數據的自定義條件即可,查詢后不僅會返回結果數據,還會返回_scroll_id。

GET /wms_order_sku2021_10/_search?scroll=1m
{
  "query": {
    "bool": {
      "must": [
        {
          "range": {
            "shipmentOrderCreateTime": {
              "gte": "2021-10-04 0000",
              "lt": "2021-10-15 0000"
            }
          }
        }
      ]
    }
  },
  "size": 20
}
d26179d0-65b4-11ed-8abf-dac502259ad0.png

第二次查詢時不需要指定索引,在 JSON 請求體中帶上前一個查詢返回的 scroll_id,同時傳入 scroll 參數,指定刷新搜索結果的緩存時間(上一次查詢緩存 1 分鐘,本次查詢會再次重置緩存時間為 1 分鐘)
GET /_search/scroll
{
  "scroll":"1m",
  "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoIAAAAAJFQdUKFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74YxZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAiY--F4WZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJMQKhIFmw2c1hwVFk1UXppbDhZcW1za2ZzdlEAAAACRUHVCxZZRnNhOGNrRFI0eVZKSm5DbXQxTDRRAAAAAkxAqEcWbDZzWHBUWTVRemlsOFlxbXNrZnN2UQAAAAImPvhdFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACJ-MhBhZOMmYzWVVMbFIzNkdnN1FwVXVHaEd3AAAAAifjIQgWTjJmM1lVTGxSMzZHZzdRcFV1R2hHdwAAAAIn4yEHFk4yZjNZVUxsUjM2R2c3UXBVdUdoR3cAAAACJ5db8xZxeW5NRXpHOFR0eVNBOHlOcXBGbWdRAAAAAifjIQkWTjJmM1lVTGxSMzZHZzdRcFV1R2hHdwAAAAJFQdUMFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74YhZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAieXW_YWcXluTUV6RzhUdHlTQTh5TnFwRm1nUQAAAAInl1v0FnF5bk1Fekc4VHR5U0E4eU5xcEZtZ1EAAAACJ5db9RZxeW5NRXpHOFR0eVNBOHlOcXBGbWdRAAAAAkVB1Q0WWUZzYThja0RSNHlWSkpuQ210MUw0UQAAAAImPvhfFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACJ-MhChZOMmYzWVVMbFIzNkdnN1FwVXVHaEd3AAAAAkVB1REWWUZzYThja0RSNHlWSkpuQ210MUw0UQAAAAImPvhgFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACTECoShZsNnNYcFRZNVF6aWw4WXFtc2tmc3ZRAAAAAiY--GEWZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJFQdUOFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACRUHVEBZZRnNhOGNrRFI0eVZKSm5DbXQxTDRRAAAAAiY--GQWZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJFQdUPFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74ZRZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAkxAqEkWbDZzWHBUWTVRemlsOFlxbXNrZnN2UQAAAAInl1v3FnF5bk1Fekc4VHR5U0E4eU5xcEZtZ1EAAAACTECoRhZsNnNYcFRZNVF6aWw4WXFtc2tmc3ZR"
}
d286e4ae-65b4-11ed-8abf-dac502259ad0.png

3.3 實現示例

d2a34900-65b4-11ed-8abf-dac502259ad0.png

protected  Page searchPageByConditionWithScrollId(BoolQueryBuilder queryParam, Class targetClass, Page page) throws IllegalAccessException, InstantiationException, InvocationTargetException {
        SearchResponse scrollResp = null;
        String scrollId = ContextParameterHolder.get("scrollId");
        if (scrollId != null) {
            scrollResp = getTransportClient().prepareSearchScroll(scrollId).setScroll(new TimeValue(60000)).execute()
                    .actionGet();
        } else {
            logger.info("基于scroll的分頁查詢,scrollId為空");
            scrollResp = this.prepareSearch()
                    .setSearchType(SearchType.QUERY_AND_FETCH)
                    .setScroll(new TimeValue(60000))
                    .setQuery(queryParam)
                    .setSize(page.getPageSize()).execute().actionGet();
            ContextParameterHolder.set("scrollId", scrollResp.getScrollId());
        }
        SearchHit[] hits = scrollResp.getHits().getHits();
        List list = new ArrayList(hits.length);
        for (SearchHit hit : hits) {
            T instance = targetClass.newInstance();
            this.convertToBean(instance, hit);
            list.add(instance);
        }
        page.setTotalRow((int) scrollResp.getHits().getTotalHits());
        page.setResult(list);
        return page;
    }

3.4 小結

scroll 分頁方式的優點就是減少了查詢和排序的次數,避免性能損耗。缺點就是只能實現上一頁、下一頁的翻頁功能,不兼容通過頁碼查詢數據的跳頁,同時由于其在搜索初始化階段會生成快照,后續數據的變化無法及時體現在查詢結果,因此更加適合一次性批量查詢或非實時數據的分頁查詢。 啟用游標查詢時,需要注意設定期望的過期時間(scroll = 1m),以降低維持游標查詢窗口所需消耗的資源。注意這個過期時間每次查詢都會重置刷新為 1 分鐘,表示游標的閑置失效時間(第二次以后的查詢必須帶 scroll = 1m 參數才能實現)

4 Search After 分頁方式

Search After 分頁方式是 ES 5 新增的一種分頁查詢方式,其實現的思路同 Scroll 分頁方式基本一致,通過記錄上一次分頁的位置標識,來進行下一次分頁數據的查詢。相比于 Scroll 分頁方式,它的優點是可以實時體現數據的變化,解決了查詢快照導致的查詢結果延遲問題。

4.1 執行過程

Search After 方式也不支持跳頁功能,每次查詢一頁數據。第一次每個 shard 返回一頁數據(size 條),coordinating node 一共獲取到 shard 數 * size 條數據 , 接下來 coordinating node 在內存中進行排序,取出前 size 條數據作為第一頁搜索結果返回。當拉取第二頁時,不同于 Scroll 分頁方式,Search After 方式會找到第一頁數據被拉取的最大值,作為第二頁數據拉取的查詢條件。 這樣每個 shard 還是返回一頁數據(size 條),coordinating node 獲取到 shard 數 * size 條數據進行內存排序,取得前 size 條數據作為全局的第二頁搜索結果。
后續分頁查詢以此類推…

4.2 ES 示例

第一次查詢只傳入排序字段和每頁大小 size

GET /wms_order_sku2021_10/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "range": {
            "shipmentOrderCreateTime": {
              "gte": "2021-10-12 0000",
              "lt": "2021-10-15 0000"
            }
          }
        }
      ]
    }
  },
  "size": 20,
  "sort": [
    {
      "_id": {
        "order": "desc"
      }
    },{
      "shipmentOrderCreateTime":{
        "order": "desc"
      }
    }
  ]
}
d2dc8706-65b4-11ed-8abf-dac502259ad0.png

接下來每次查詢時都帶上本次查詢的最后一條數據的 _id 和 shipmentOrderCreateTime 字段,循環往復就能夠實現不斷下一頁的功能
GET /wms_order_sku2021_10/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "range": {
            "shipmentOrderCreateTime": {
              "gte": "2021-10-12 0000",
              "lt": "2021-10-15 0000"
            }
          }
        }
      ]
    }
  },
  "size": 20,
  "sort": [
    {
      "_id": {
        "order": "desc"
      }
    },{
      "shipmentOrderCreateTime":{
        "order": "desc"
      }
    }
  ],
  "search_after": ["SO-460_152-1447931043809128448-100017918838",1634077436000]
}
d2f15096-65b4-11ed-8abf-dac502259ad0.png

4.3 實現示例

d31396c4-65b4-11ed-8abf-dac502259ad0.png
d33703ca-65b4-11ed-8abf-dac502259ad0.png

public  ScrollDto queryScrollDtoByParamWithSearchAfter(
            BoolQueryBuilder queryParam, Class targetClass, int pageSize, String afterId,
            List fieldSortBuilders) {
        SearchResponse scrollResp;
        long now = System.currentTimeMillis();
        SearchRequestBuilder builder = this.prepareSearch();
        if (CollectionUtils.isNotEmpty(fieldSortBuilders)) {
            fieldSortBuilders.forEach(builder::addSort);
        }
        builder.addSort("_id", SortOrder.DESC);
        if (StringUtils.isBlank(afterId)) {
            log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁查詢,afterId為空");
            SearchRequestBuilder searchRequestBuilder = builder.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
                    .setQuery(queryParam).setSize(pageSize);
            scrollResp = searchRequestBuilder.execute()
                    .actionGet();
            log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁查詢,afterId 為空,searchRequestBuilder:{}", searchRequestBuilder);
        } else {
            log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁查詢,afterId=" + afterId);
            Object[] afterIds = JSON.parseObject(afterId, Object[].class);
            SearchRequestBuilder searchRequestBuilder = builder.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
                    .setQuery(queryParam).searchAfter(afterIds).setSize(pageSize);
            log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁查詢,searchRequestBuilder:{}", searchRequestBuilder);
            scrollResp = searchRequestBuilder.execute()
                    .actionGet();
        }
        SearchHit[] hits = scrollResp.getHits().getHits();
        log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁查詢,totalRow={}, size={}, use time:{}", scrollResp.getHits().getTotalHits(), hits.length, System.currentTimeMillis() - now);
        now = System.currentTimeMillis();

        List list = new ArrayList<>();
        if (ArrayUtils.getLength(hits) > 0) {
            list = Arrays.stream(hits)
                    .filter(Objects::nonNull)
                    .map(SearchHit::getSourceAsMap)
                    .filter(Objects::nonNull)
                    .map(JSON::toJSONString)
                    .map(e -> JSON.parseObject(e, targetClass))
                    .collect(Collectors.toList());
            afterId = JSON.toJSONString(hits[hits.length - 1].getSortValues());
        }
        log.info("es數據轉換bean,totalRow={}, size={}, use time:{}", scrollResp.getHits().getTotalHits(), hits.length, System.currentTimeMillis() - now);
        return ScrollDto.builder().scrollId(afterId).result(list).totalRow((int) scrollResp.getHits().getTotalHits()).build();
    }

4.4 小結

Search After 分頁方式采用記錄作為游標,因此 Search After 要求 doc 中至少有一條全局唯一變量(示例中使用_id 和時間戳,實際上_id 已經是全局唯一)。Search After 方式是無狀態的分頁查詢,因此數據的變更能夠及時的反映在查詢結果中,避免了 Scroll 分頁方式無法獲取最新數據變更的缺點。同時 Search After 不用維護 scroll_id 和快照,因此也節約大量資源。

5 總結思考

5.1 ES 三種分頁方式對比總結

d35670c0-65b4-11ed-8abf-dac502259ad0.png

如果數據量小(from+size 在 10000 條內),或者只關注結果集的 TopN 數據,可以使用 from/size 分頁,簡單粗暴

數據量大,深度翻頁,后臺批處理任務(數據遷移)之類的任務,使用 scroll 方式

數據量大,深度翻頁,用戶實時、高并發查詢需求,使用 search after 方式

5.2 個人思考

在一般業務查詢頁面中,大多情況都是 10-20 條數據為一頁,10000 條數據也就是 500-1000 頁。正常情況下,對于用戶來說,有極少需求翻到比較靠后的頁碼來查看數據,更多的是通過查詢條件框定一部分數據查看其詳情。因此在業務需求敲定初期,可以同業務人員商定 1w 條數據的限定,超過 1w 條的情況可以借助導出數據到 Excel 表,在 Excel 表中做具體的操作。

如果給導出中心返回大量數據的場景可以使用 Scroll 或 Search After 分頁方式,相比之下最好使用 Search After 方式,既可以保證數據的實時性,也具有很高的搜索性能。

總之,在使用 ES 時一定要避免深度分頁問題,要在跳頁功能實現和 ES 性能、資源之間做一個取舍。必要時也可以調大 max_result_window 參數,原則上不建議這么做,因為 1w 條以內 ES 基本能保持很不錯的性能,超過這個范圍深度分頁相當耗時、耗資源,因此謹慎選擇此方式。

審核編輯:湯梓紅
聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Query
    +關注

    關注

    0

    文章

    11

    瀏覽量

    9372
  • Elasticsearch
    +關注

    關注

    0

    文章

    30

    瀏覽量

    2844

原文標題:ElasticSearch深度分頁詳解

文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    Windows安裝ElasticSearch

    Windows安裝ElasticSearch
    的頭像 發表于 02-15 17:09 ?1019次閱讀
    Windows安裝<b class='flag-5'>ElasticSearch</b>

    linux安裝配置ElasticSearch之源碼安裝

    ElasticSearch是基于Lucene這個非常成熟的索引方案,另加上一些分布式的實現:集群,sharding,replication等。以下是對其采用源碼安裝的方法1.下載
    發表于 01-11 17:27

    ElasticSearch的詞條查詢

    ElasticSearch查詢 第三篇:詞條查詢
    發表于 04-30 17:03

    ElasticSearch的初步環境

    ElasticSearch最實用入門指南——初步環境
    發表于 03-31 11:32

    elasticsearch介紹PPT

    elasticsearch介紹PPT
    發表于 12-13 21:05 ?20次下載

    Jquery簡單分頁實現

    這篇文章主要介紹了Jquery簡單分頁實現方法,實例分析了jquery分頁的相關實現技巧,具有一定參考借鑒價值,需要的朋友可以參考下。
    發表于 11-28 11:55 ?1087次閱讀

    一文詳解linux的分頁模型

    也就是我們實際中編碼時遇到的內存地址并不是對應于實際內存上的地址,我們編碼中使用的地址是一個邏輯地址,會通過分段和分頁這兩個機制把它轉為物理地址。而由于linux使用的分段機制有限,可以認為
    的頭像 發表于 05-18 08:59 ?2150次閱讀
    一文<b class='flag-5'>詳解</b>linux的<b class='flag-5'>分頁</b>模型

    Elasticsearch6.1教程

    Elasticsearch6.1教程
    發表于 07-04 14:40 ?0次下載

    ElasticSearch是什么?應用場景是什么?

    ElasticSearch是什么 ElasticSearch的功能 ElasticSearch的應用場景 ElasticSearch的特點
    的頭像 發表于 10-09 18:38 ?2485次閱讀

    ElasticSearch 深度分頁實踐性分析探討

    該條 DSL 語句表示從搜索結果中第 10 條數據位置開始,取之后的 20 條數據作為結果返回。這種分頁方式在 ES 集群內部是如何執行的呢?
    發表于 11-21 11:21 ?409次閱讀

    圖文詳解Linux分頁機制

    分頁機制是 80x86 內存管理機制的第二種機制,分段機制用于把虛擬地址轉換為線性地址,而分頁機制用于把線性地址轉換為物理地址。
    發表于 05-30 09:10 ?485次閱讀
    圖文<b class='flag-5'>詳解</b>Linux<b class='flag-5'>分頁</b>機制

    聊聊分頁列表緩存設計

    這是最簡單易懂的方案,我們按照不同的分頁條件查詢出結果后,直接緩存分頁結果 。
    的頭像 發表于 06-06 18:25 ?753次閱讀
    聊聊<b class='flag-5'>分頁</b>列表緩存設計

    Elasticsearch保姆級入門

    我們需要創建一個供 Elasticsearch 和 Kibana 使用的 network。這個 network 將被用于 Elasticsearch 和 Kibana 之間的通信。
    的頭像 發表于 09-01 15:24 ?874次閱讀
    <b class='flag-5'>Elasticsearch</b>保姆級入門

    SpringBoot 連接ElasticSearch的使用方式

    在上篇 ElasticSearch 文章中,我們詳細的介紹了 ElasticSearch 的各種 api 使用。 實際的項目開發過程中,我們通常基于某些主流框架平臺進行技術開發,比如
    的頭像 發表于 10-09 10:35 ?1164次閱讀

    mybatis邏輯分頁和物理分頁的區別

    MyBatis是一個開源的Java持久層框架,它與其他ORM(對象關系映射)框架相比,具有更加靈活和高性能的特點。MyBatis提供了兩種分頁方式,即邏輯分頁和物理分頁。在本文中,我們將詳細介紹
    的頭像 發表于 12-03 14:54 ?959次閱讀
    主站蜘蛛池模板: 中文字幕11页| 永久网站色视频在线观看免费 | 欧美一卡二卡3卡4卡无卡六卡七卡科普 | 国产色婷婷精品免费视频| 好爽毛片一区二区三区四区| 国产床戏无遮掩视频播放| 一级网站在线观看| 18男女很黄的视频| 欧美性性性性性ⅹxxbbbb| 欧美极品| 殴美aⅴ| 亚洲va中文字幕无码| 三级黄色短视频| 看逼网址| 最新欧美一级视频| 欧美啊片| 天天摸天天做天天爽天天弄 | 亚洲a在线播放| 色婷婷激婷婷深爱五月老司机 | www亚洲成人| 四虎永久在线免费观看| 麦克斯奥特曼在线观看| av2014天堂网| 丁香花小说| 天天搞夜夜爽| 亚洲乱强| 在线观看国产三级| 日本三级全黄| 大尺度在线| 黄色网久久| 性满足久久久久久久久| 婷婷色激情| 国产亚洲欧美视频| 日韩精品在线第一页| 视频在线观看高清免费看| www午夜视频| jizz免费一区二区三区| 日本精品一在线观看视频| 超级狂色而且免费又超好看| 久操免费视频| 高h细节肉爽文bl1v1|