主頁 > 後端開發 > 面試官:如何在千萬級資料中查詢 10W 的資料,都有什么方案?

面試官:如何在千萬級資料中查詢 10W 的資料,都有什么方案?

2023-03-14 07:00:41 後端開發

作者:變速風聲
鏈接:https://juejin.cn/post/7104090532015505416

前言

在開發中遇到一個業務訴求,需要在千萬量級的底池資料中篩選出不超過 10W 的資料,并根據配置的權重規則進行排序、打散(如同一個類目下的商品資料不能連續出現 3 次),

下面對該業務訴求的實作,設計思路和方案優化進行介紹,對「千萬量級資料中查詢 10W 量級的資料」設計了如下方案

  1. 多執行緒 + CK 翻頁方案
  2. ES scroll scan 深翻頁方案
  3. ES + Hbase 組合方案
  4. RediSearch + RedisJSON 組合方案

初版設計方案

整體方案設計為:

  1. 先根據配置的「篩選規則」,從底池表中篩選出「目標資料」
  2. 在根據配置的「排序規則」,對「目標資料」進行排序,得到「結果資料」

技術方案如下:

  1. 每天運行導數任務,把現有的千萬量級的底池資料(Hive 表)匯入到 Clickhouse 中,后續使用 CK 表進行資料篩選,
  2. 將業務配置的篩選規則和排序規則,構建為一個「篩選 + 排序」物件 SelectionQueryCondition
  3. 從 CK 底池表取「目標資料」時,開啟多執行緒,進行分頁篩選,將獲取到的「目標資料」存放到 result 串列中,
//分頁大小  默認 5000
int pageSize = this.getPageSize();
//頁碼數
int pageCnt = totalNum / this.getPageSize() + 1;

List<Map<String, Object>> result = Lists.newArrayList();
List<Future<List<Map<String, Object>>>> futureList = new ArrayList<>(pageCnt);

//開啟多執行緒呼叫
for (int i = 1; i <= pageCnt; i++) {
    //將業務配置的篩選規則和排序規則 構建為 SelectionQueryCondition 物件
    SelectionQueryCondition selectionQueryCondition = buildSelectionQueryCondition(selectionQueryRuleData);
    selectionQueryCondition.setPageSize(pageSize);
    selectionQueryCondition.setPage(i);
    futureList.add(selectionQueryEventPool.submit(new QuerySelectionDataThread(selectionQueryCondition)));
}

for (Future<List<Map<String, Object>>> future : futureList) {
    //RPC 呼叫
    List<Map<String, Object>> queryRes = future.get(20, TimeUnit.SECONDS);
    if (CollectionUtils.isNotEmpty(queryRes)) {
        // 將目標資料存放在 result 中
        result.addAll(queryRes);
    }
}

對目標資料 result 進行排序,得到最終的「結果資料」,

推薦一個開源免費的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

CK分頁查詢

在「初版設計方案」章節的第 3 步提到了「從 CK 底池表取目標資料時,開啟多執行緒,進行分頁篩選」,此處對 CK 分頁查詢進行介紹,

封裝了 queryPoolSkuList 方法,負責從 CK 表中獲得目標資料,該方法內部呼叫了 sqlSession.selectList 方法,

public List<Map<String, Object>> queryPoolSkuList( Map<String, Object> params ) {
    List<Map<String, Object>> resultMaps = new ArrayList<>();

    QueryCondition queryCondition = parseQueryCondition(params);
    List<Map<String, Object>> mapList = lianNuDao.queryPoolSkuList(getCkDt(),queryCondition);
    if (CollectionUtils.isNotEmpty(mapList)) {
        for (Map<String,Object> data : mapList) {
            resultMaps.add(camelKey(data));
        }
    }
    return resultMaps;
}

// lianNuDao.queryPoolSkuList

@Autowired
@Qualifier("ckSqlNewSession")
private SqlSession sqlSession;

public List<Map<String, Object>> queryPoolSkuList( String dt, QueryCondition queryCondition ) {
    queryCondition.setDt(dt);
    queryCondition.checkMultiQueryItems();
    return sqlSession.selectList("LianNu.queryPoolSkuList",queryCondition);
}

sqlSession.selectList 方法中呼叫了和 CK 互動的 queryPoolSkuList 查詢方法,部分代碼如下,

<select id="queryPoolSkuList" parameterType="com.jd.bigai.domain.liannu.QueryCondition" resultType="java.util.Map">
    select sku_pool_id,i
    tem_sku_id,
    skuPoolName,
    price,
    ...
    ...
    businessType
    from liannu_sku_pool_indicator_all
    where
    dt=#{dt}
    and
    <foreach collection="queryItems" separator=" and " item="queryItem" open=" " close=" " >
        <choose>
            <when test="queryItem.type == 'equal'">
                ${queryItem.field} = #{queryItem.value}
            </when>
            ...
            ...
        </choose>
    </foreach>
    <if test="orderBy == null">
        group by sku_pool_id,item_sku_id
    </if>
    <if test="orderBy != null">
        group by sku_pool_id,item_sku_id,${orderBy} order by ${orderBy} ${orderAd}
    </if>
    <if test="limitEnd != 0">
        limit #{limitStart},#{limitEnd}
    </if>
</select>

可以看到,在 CK 分頁查詢時,是通過 limit #{limitStart},#{limitEnd} 實作的分頁,

limit 分頁方案,在「深翻頁」時會存在性能問題,初版方案上線后,在 1000W 量級的底池資料中篩選 10W 的資料,最壞耗時會達到 10s~18s 左右,

使用ES Scroll Scan 優化深翻頁

對于 CK 深翻頁時候的性能問題,進行了優化,使用 Elasticsearch 的 scroll scan 翻頁方案進行優化,

ES的翻頁方案

ES 翻頁,有下面幾種方案

  1. from + size 翻頁
  2. scroll 翻頁
  3. scroll scan 翻頁
  4. search after 翻頁
翻頁方式 性能 優點 缺點 場景
from + size 靈活性好,實作簡單 深度分頁問題 資料量比較小,能容忍深度分頁問題
scroll 解決了深度分頁問題 需要維護一個 scrollId(快照版本),無法反應資料的實時性;可排序,但無法跳頁查詢 查詢海量資料
scroll scan 基于 scroll 方案,進一步提升了海量資料查詢的性能 無法排序,其余缺點同 scroll 查詢海量資料
search after 性能最好,不存在深度分頁問題,能夠反映資料的實時變更 實作復雜,需要有一個全域唯一的欄位,連續分頁的實作會比較復雜,因為每一次查詢都需要上次查詢的結果 不適用于大幅度跳頁查詢,適用于海量資料的分頁

對上述幾種翻頁方案,查詢不同數目的資料,耗時資料如下表,

ES 翻頁方式 1-10 49000-49010 99000-99010
from + size 8ms 30ms 117ms
scroll 7ms 66ms 36ms
search_after 5ms 8ms 7ms

耗時資料

此處,分別使用 Elasticsearch 的 scroll scan 翻頁方案、初版中的 CK 翻頁方案進行資料查詢,對比其耗時資料,

如上測驗資料,可以發現,以十萬,百萬,千萬量級的底池為例

  1. 底池量級越大,查詢相同的資料量,耗時越大
  2. 查詢結果 3W 以下時,ES 性能優;查詢結果 5W 以上時,CK 多執行緒性能優

ES+Hbase組合查詢方案

在「使用 ES Scroll Scan 優化深翻頁」中,使用 Elasticsearch 的 scroll scan 翻頁方案對深翻頁問題進行了優化,但在實作時為單執行緒呼叫,所以最終測驗耗時資料并不是特別理想,和 CK 翻頁方案性能差不多,

在調研階段發現,從底池中取出 10W 的目標資料時,一個商品包含多個欄位的資訊(CK 表中一行記錄有 150 個欄位資訊),如價格、會員價、學生價、庫存、好評率等,對于一行記錄,當減少獲取欄位的個數時,查詢耗時會有明顯下降,如對 sku1的商品,從之前獲取價格、會員價、學生價、親友價、庫存等 100 個欄位資訊,縮減到只獲取價格、庫存這兩個欄位資訊,

如下圖所示,使用 ES 查詢方案,對查詢同樣條數的場景(從千萬級底池中篩選出 7W+ 條資料),獲取的每條記錄的欄位個數從 32 縮減到 17,再縮減到 1個(其實是兩個欄位,一個是商品唯一標識 sku_id,另一個是 ES 對每條檔案記錄的 doc_id)時,查詢的耗時會從 9.3s 下降到 4.2s,再下降到 2.4s,

從中可以得出如下結論

  1. 一次 ES 查詢中,若查詢欄位和資訊較多,fetch 階段的耗時,遠大于 query 階段的耗時,
  2. 一次 ES 查詢中,若查詢欄位和資訊較多,通過減少不必要的查詢欄位,可以顯著縮短查詢耗時,

下面對結論中涉及的 queryfetch 查詢階段進行補充說明,

ES查詢的兩個階段:query和fetch

在 ES 中,搜索一般包括兩個階段,queryfetch 階段

query 階段

  • 根據查詢條件,確定要取哪些檔案(doc),篩選出檔案 ID(doc_id

fetch 階段

  • 根據 query 階段回傳的檔案 ID(doc_id),取出具體的檔案(doc

ES的filesystem cache

  • ES 會將磁盤中的資料自動快取到 filesystem cache,在記憶體中查找,提升了速度
  • filesystem cache 無法容納索引資料檔案,則會基于磁盤查找,此時查詢速度會明顯變慢
  • 若數量兩過大,基于「ES 查詢的的 query 和 fetch 兩個階段」,可使用 ES + HBase 架構,保證 ES 的資料量小于 filesystem cache,保證查詢速度

組合使用Hbase

在上文調研的基礎上,發現「減少不必要的查詢展示欄位」可以明顯縮短查詢耗時,沿著這個優化思路,參照參考鏈接 ref-1,設計了一種新的查詢方案

  1. ES 僅用于條件篩選,ES 的查詢結果僅包含記錄的唯一標識 sku_id(其實還包含 ES 為每條檔案記錄的 doc_id
  2. Hbase 是列存盤資料庫,每列資料有一個 rowKey,利用 rowKey 篩選一條記錄時,復雜度為 O(1),(類似于從 HashMap 中根據 keyvalue
  3. 根據 ES 查詢回傳的唯一標識 sku_id,作為 Hbase 查詢中的 rowKey,在 O(1) 復雜度下獲取其他資訊欄位,如價格,庫存等,

使用 ES + Hbase 組合查詢方案,在線上進行了小規模的灰度測驗,在 1000W 量級的底池資料中篩選 10W 的資料,對比 CK 翻頁方案,最壞耗時從 10~18s 優化到了 3~6s 左右,

也應該看到,使用 ES + Hbase 組合查詢方案,會增加系統復雜度,同時資料也需要同時存盤到 ES 和 Hbase,

RediSearch+RedisJSON優化方案

RediSearch 是基于 Redis 構建的分布式全文搜索和聚合引擎,能以極快的速度在 Redis 資料集上執行復雜的搜索查詢,RedisJSON 是一個 Redis 模塊,在 Redis 中提供 JSON 支持,RedisJSON 可以和 RediSearch 無縫配合,實作索引和查詢 JSON 檔案,

根據一些參考資料,RediSearch + RedisJSON 可以實作極高的性能,可謂碾壓其他 NoSQL 方案,在后續版本迭代中,可考慮使用該方案來進一步優化,

下面給出 RediSearch + RedisJSON 的部分性能資料,

RediSearch 性能資料

在同等服務器配置下索引了 560 萬個檔案 (5.3GB),RediSearch 構建索引的時間為 221 秒,而 Elasticsearch 為 349 秒,RediSearch 比 ES 快了 58%,

資料建立索引后,使用 32 個客戶端對兩個單詞進行檢索,RediSearch 的吞吐量達到 12.5K ops/sec,ES 的吞吐量為 3.1K ops/sec,RediSearch 比ES 要快 4 倍,同時,RediSearch 的延遲為 8ms,而 ES 為 10ms,RediSearch 延遲稍微低些,

對比 Redisearch Elasticsearch
搜索引擎 專用引擎 基于 Lucene 引擎
編程語言 C 語言 Java
存盤方案 記憶體 磁盤
協議 Redis 序列化協議 HTTP
集群 企業版支持 支持
性能 簡單查詢高于 ES 復雜查詢時高于 RediSearch

RedisJSON 性能資料

根據官網的性能測驗報告,RedisJson + RedisSearch 可謂碾壓其他 NoSQL

  • 對于隔離寫入(isolated writes),RedisJSON 比 MongoDB 快 5.4 倍,比 ES 快 200 倍以上
  • 對于隔離讀取(isolated reads),RedisJSON 比 MongoDB 快 12.7 倍,比 ES 快 500 倍以上

在混合作業負載場景中,實時更新不會影響 RedisJSON 的搜索和讀取性能,而 ES 會受到影響,

  • RedisJSON 支持的運算元/秒比 MongoDB 高約 50 倍,比 ES 高 7 倍/秒,
  • RedisJSON 的延遲比 MongoDB 低約 90 倍,比 ES 低 23.7 倍,

此外,RedisJSON 的讀取、寫入和負載搜索延遲,在更高的百分位數中遠比 ES 和 MongoDB 穩定,當增加寫入比率時,RedisJSON 還能處理越來越高的整體吞吐量,而當寫入比率增加時,ES 會降低它可以處理的整體吞吐量,

總結

本文從一個業務訴求觸發,對「千萬量級資料中查詢 10W 量級的資料」介紹了不同的設計方案,對于「在 1000W 量級的底池資料中篩選 10W 的資料」的場景,不同方案的耗時如下

  1. 多執行緒 + CK 翻頁方案,最壞耗時為 10s~18s
  2. 單執行緒 + ES scroll scan 深翻頁方案,相比 CK 方案,并未見到明顯優化
  3. ES + Hbase 組合方案,最壞耗時優化到了 3s~6s
  4. RediSearch + RedisJSON 組合方案,后續會實測該方案的耗時

參考資料:

  • https://juejin.cn/post/7103848212154286087
  • https://www.infoq.cn/article/wymrl5h80sfawg8u7ede
  • https://juejin.cn/post/7042476201574662175

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了,,,

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/546664.html

標籤:Java

上一篇:Spring Boot 分片上傳、斷點續傳、大檔案上傳、秒傳,應有盡有,建議收藏!!

下一篇:過濾器和攔截器總結

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more