ElasticSearch深度分頁詳解

来源:https://www.cnblogs.com/Jcloud/archive/2022/11/15/16889830.html
-Advertisement-
Play Games

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


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階段

 

如上圖所示,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階段

 

如上圖所示,當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 實現示例

 

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方式進行分頁,非常不建議調大結果視窗參數值。

 

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 00:00:00",
              "lt": "2021-10-15 00:00:00"
            }
          }
        }
      ]
    }
  },
  "size": 20
}

 

 

第二次查詢時不需要指定索引,在JSON請求體中帶上前一個查詢返回的scroll_id,同時傳入scroll參數,指定刷新搜索結果的緩存時間(上一次查詢緩存1分鐘,本次查詢會再次重置緩存時間為1分鐘)

GET /_search/scroll
{
  "scroll":"1m",
  "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoIAAAAAJFQdUKFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74YxZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAiY--F4WZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJMQKhIFmw2c1hwVFk1UXppbDhZcW1za2ZzdlEAAAACRUHVCxZZRnNhOGNrRFI0eVZKSm5DbXQxTDRRAAAAAkxAqEcWbDZzWHBUWTVRemlsOFlxbXNrZnN2UQAAAAImPvhdFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACJ-MhBhZOMmYzWVVMbFIzNkdnN1FwVXVHaEd3AAAAAifjIQgWTjJmM1lVTGxSMzZHZzdRcFV1R2hHdwAAAAIn4yEHFk4yZjNZVUxsUjM2R2c3UXBVdUdoR3cAAAACJ5db8xZxeW5NRXpHOFR0eVNBOHlOcXBGbWdRAAAAAifjIQkWTjJmM1lVTGxSMzZHZzdRcFV1R2hHdwAAAAJFQdUMFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74YhZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAieXW_YWcXluTUV6RzhUdHlTQTh5TnFwRm1nUQAAAAInl1v0FnF5bk1Fekc4VHR5U0E4eU5xcEZtZ1EAAAACJ5db9RZxeW5NRXpHOFR0eVNBOHlOcXBGbWdRAAAAAkVB1Q0WWUZzYThja0RSNHlWSkpuQ210MUw0UQAAAAImPvhfFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACJ-MhChZOMmYzWVVMbFIzNkdnN1FwVXVHaEd3AAAAAkVB1REWWUZzYThja0RSNHlWSkpuQ210MUw0UQAAAAImPvhgFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACTECoShZsNnNYcFRZNVF6aWw4WXFtc2tmc3ZRAAAAAiY--GEWZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJFQdUOFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACRUHVEBZZRnNhOGNrRFI0eVZKSm5DbXQxTDRRAAAAAiY--GQWZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJFQdUPFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74ZRZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAkxAqEkWbDZzWHBUWTVRemlsOFlxbXNrZnN2UQAAAAInl1v3FnF5bk1Fekc4VHR5U0E4eU5xcEZtZ1EAAAACTECoRhZsNnNYcFRZNVF6aWw4WXFtc2tmc3ZR"
}
 

 

 

3.3 實現示例

 

protected <T> Page<T> searchPageByConditionWithScrollId(BoolQueryBuilder queryParam, Class<T> targetClass, Page<T> 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<T> list = new ArrayList<T>(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 00:00:00",
              "lt": "2021-10-15 00:00:00"
            }
          }
        }
      ]
    }
  },
  "size": 20,
  "sort": [
    {
      "_id": {
        "order": "desc"
      }
    },{
      "shipmentOrderCreateTime":{
        "order": "desc"
      }
    }
  ]
}

 

 

 

接下來每次查詢時都帶上本次查詢的最後一條數據的 _id 和 shipmentOrderCreateTime欄位,迴圈往複就能夠實現不斷下一頁的功能

GET /wms_order_sku2021_10/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "range": {
            "shipmentOrderCreateTime": {
              "gte": "2021-10-12 00:00:00",
              "lt": "2021-10-15 00:00:00"
            }
          }
        }
      ]
    }
  },
  "size": 20,
  "sort": [
    {
      "_id": {
        "order": "desc"
      }
    },{
      "shipmentOrderCreateTime":{
        "order": "desc"
      }
    }
  ],
  "search_after": ["SO-460_152-1447931043809128448-100017918838",1634077436000]
}

 

 

4.3 實現示例

 

 

public <T> ScrollDto<T> queryScrollDtoByParamWithSearchAfter(
            BoolQueryBuilder queryParam, Class<T> targetClass, int pageSize, String afterId,
            List<FieldSortBuilder> 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<T> 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.<T>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三種分頁方式對比總結

 

  • 如果數據量小(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基本能保持很不錯的性能,超過這個範圍深度分頁相當耗時、耗資源,因此謹慎選擇此方式。

作者:何守優


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • windows10系統“無法安裝Windows沙盒:在固件中禁用了虛擬化支持”,另外操作無法安裝hyoer-v該固件中的虛擬化支持被禁用問題。 ####解決辦法: 我這裡以聯想拯救者r720筆記本電腦為例,開啟cpu虛擬化: 1、打開聯想筆記本電腦,重新啟動電腦,在啟動的時候快速按鍵盤上的F2按鍵, ...
  • //源文件 static uint32_t fac_us = 0; // us延時倍乘數 /** * @brief 初始化延遲函數 * 當使用ucos的時候,此函數會初始化ucos的時鐘節拍 * SYSTICK的時鐘固定為AHB時鐘的1/8 * @param SYSCLK 系統時鐘頻率 */ voi ...
  • //源文件 void LedPhyConfig() { RCC->AHB1ENR |= (1<<1); //使能GPIOB //LD1 GPIOB->MODER |= (1<<0*2); //輸出模式 GPIOB->OTYPER &= ~(1<<0); //推輓 GPIOB->OSPEEDR |= ...
  • 背景:內網環境伺服器不能直接安裝工具或服務,可以用一臺外網伺服器同步阿裡雲的yum倉庫,作為本地倉庫 搭建本地yum倉庫 編輯yum配置文件,開啟緩存使用功能,設置緩存路徑 cp /etc/yum.conf /etc/yum.conf.bak vim /etc/yum.conf cachedir=/ ...
  • SingleStore(前身 MemSQL)是一個為數據密集型應用設計的雲原生資料庫。它是一個分散式的關係型 SQL 資料庫管理系統(RDBMS),具有 ANSI SQL 支持,它以數據攝入、交易處理和查詢處理的速度而聞名。SingleStore 主要存儲關係型數據,但也可以存儲 JSON 數據、圖 ...
  • 一、Installing ClickHouse-22.10.2.11 on openEuler 1 地址 https://clickhouse.com https://packages.clickhouse.com https://github.com/ClickHouse/ClickHouse 2 ...
  • 資料庫用戶通常依賴隔離級別來確保數據一致性,但很多資料庫卻並未達到其所表明的級別。主要原因是:一方面,資料庫開發者對各個級別的理解有細微差異;另一方面,實現層面沒有達到理論上的要求。 用戶在使用或開發者在交付資料庫前,需要對隔離級別進行快速的正確性驗證,並且希望驗證是可靠的(沒有誤差)、快速的(多項 ...
  • 摘要:本文講解了GaussDB(DWS)上模糊查詢常用的性能優化方法,通過創建索引,能夠提升多種場景下模糊查詢語句的執行速度。 本文分享自華為雲社區《GaussDB(DWS) 模糊查詢性能優化》,作者: 黎明的風 。 在使用GaussDB(DWS)時,通過like進行模糊查詢,有時會遇到查詢性能慢的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...