一文帶你深度解析MySQL 8.0事務提交原理

来源:https://www.cnblogs.com/huaweiyun/p/18357117
-Advertisement-
Play Games

摘要:當多個引擎/節點同時訪問和修改數據時,如何保證數據在各個引擎/節點之間的一致性成為了一項挑戰。本文將深入探討MySQL集群在保持數據一致性的解決方案。 本文分享自華為雲社區《【華為雲MySQL技術專欄】MySQL 8.0事務提交原理解析!》,作者:GaussDB資料庫。 1. 概述 MySQL ...


摘要:當多個引擎/節點同時訪問和修改數據時,如何保證數據在各個引擎/節點之間的一致性成為了一項挑戰。本文將深入探討MySQL集群在保持數據一致性的解決方案。

本文分享自華為雲社區《【華為雲MySQL技術專欄】MySQL 8.0事務提交原理解析!》,作者:GaussDB資料庫。

 

1. 概述

MySQL是一個插件式、支持多存儲引擎架構的資料庫。一方面,MySQL支持一個事務跨多個引擎進行讀寫,使得資料庫系統具備良好的可擴展性和靈活性;另一方面,MySQL也支持一個事務跨多節點進行讀寫,通過分散式節點架構使MySQL消除了單點故障,提高資料庫系統的可靠性和可用性。

然而,當多個引擎/節點同時訪問和修改數據時,如何保證數據在各個引擎/節點之間的一致性成為了一項挑戰。

本文將深入探討MYSQL集群在保持數據一致性的解決方案。MySQL集群通過XA事務(X/Open Distributed Transaction Processing Model,簡稱X/Open DTP Model)解決了此問題。XA事務分為內部XA和外部XA事務,本文將聚焦內部XA的源碼實現。

2. XA事務

XA事務定義了三個參與角色(APP、TM、RM),並通過兩個階段實現分散式事務。

 

 

 

2.1  XA事務模型

  • XA事務中的三個參與角色分別是:

APPApplication Program,簡稱APP):應用程式,定義事務的開始和結束。

TMTransaction Manager,簡稱TM事務管理器,充當事務的協調者,監控事務的執行進度,負責事務的提交、回滾等。

RMResource Manager,簡稱RM資源管理器,充當事務的參與者,如資料庫、文件系統,提供訪問資源的方式。

  • 實現分散式事務的兩個階段:

階段一TM向所有的RM發出PREPARE指令,RM進行完成提交前的準備工作,並刷新相關操作日誌,此時不會進行事務提交。如果在PREPARE指令下發過程中某一RM節點失敗,則回滾事務,TM向所有RM節點下發ROLLBACK指令,防止數據不一致的情況發生。

階段二如果TM收到所有RM的成功消息,則TM向RM發出COMMIT指令,RM向TM返回提交成功的消息後,TM確認整個事務完成。如果任意一個RM節點COMMIT失敗,則TM嘗試重新下發COMMIT指令,嘗試失敗到上限次數後將返回報錯,整個事務失敗。

在單實例節點中,當Server層作為TM,多個存儲引擎作為RM,就會產生內部XA事務,MySQL利用內部事務保證了多個存儲引擎的一致性。外部XA事務一般是針對跨多MySQL實例的分散式事務,因此,外部XA的協調者是用戶的應用,參與者是MySQL節點。

外部XA事務與內部XA事務核心邏輯類似,同時給用戶提供了一套XA事務的操作命令,包括XA start,XA end,XA prepare和XA commit等。

3. 內部XA事務

在單個MYSQL實例中,使用內部XA事務來解決Server層Binlog日誌和Storage層事務日誌的一致性等問題。其中,Server層作為事務協調器,而多個存儲引擎作為事務參與者。

3.1 協調者對象tc_log

MySQL啟動時,包含了事務協調者的選擇。如果開啟了Binlog,並且存在事務引擎,則XA協調器為mysql_bin_log對象,使用Binlog物理文件記錄事務狀態;如果關閉了Binlog,且存在不少於2個事務引擎,則XA協調器為tc_log_mmap對象,使用記憶體結構來記錄事務狀態;其他情況(沒有事務引擎),則不需要XA,tc_log設置為tc_log_dummy 對象。

無論tc_log_dummy還是mysql_bin_log或tc_log_mmap都基於TC_LOG這個基類來實現的。TC_LOG是一個全局指針,作為事務提交的協調器,實現了事務的prepare,commit,rollback等介面。

 

 

圖3.1 TC_LOG類關係圖

mysql_bin_log,tc_log_mmap和tc_log_dummy作為協調者的基本邏輯如下:

mysql_bin_log作為協調者:

prepare:ha_prepare_low

commit:write-binlog + ha_comit_low

tc_log_mmap作為協調者:

prepare:ha_prepare_low

commit:wrtie-xid + ha_commit_low

tc_log_dummy作為協調者:

prepare:ha_prepare_low

commit:ha_commit_low

 

其中tc_log_dummy不會記錄事務日誌,只是做簡單的轉發,將Server層的調用路由到Storage層調用。tc_log_mmap是一個標準的事務協調者實現,它會創建一個名為tc.log的日誌並使用操作系統的記憶體映射(memory-map,mmap)機制將內容映射到記憶體中,tc.log文件中分為一個一個PAGE,每個PAGE上有多個XID(X/Open transaction IDentifier,全局事務唯一ID)。Binlog同樣基於TC_LOG來實現事務協調者功能,會遞增生成mysql-binlog.xxxx的文件,每個文件中包含多個事務產生的Binlog event,併在Binlog event中包含XID。tc_log_mmap和Binlog都基於XID來確定事務是否已提交。

本文主要關註於如何通過內部XA 保證Binlog和Redo log的一致性,即以Binlog作為協調器的場景,這裡的Binlog既是協調者也是參與者。

3.2 事務提交過程

如圖3.2為一個事務的執行過程,當客戶端發出COMMIT指令時,MYSQL內部將通過Prepare和Commit兩個階段完成事務的提交。

 

 

 


圖3.2 事務提交過程

Prepare階段,事務的Undo log設置為prepare狀態,寫Prepare Log(Prepare階段產生的Redo Log),將事務狀態設為TRX_PREPARED,寫XID(事務ID號)到Redo Log,同時把Redo Log刷新到磁碟中。

Commit階段,Binlog寫入文件並刷盤,同時也會把XID寫入到Binlog。調用引擎的Commit完成事務的提交,同時會對事務的Undo log從prepare狀態設置為提交狀態(可清理狀態),寫Commit Log(Commit階段產生的Redo log),釋放鎖、read view等,最後將事務狀態設置為TRX_NOT_STARTED狀態。

兩階段提交保證了事務在多個引擎之間的原子性,以Binlog寫入成功作為事務提交的標誌。

在崩潰恢復中,是以Binlog中的XIDRedo log中的XID進行比較,XIDBinlog 里存在則提交,不存在則回滾。我們來看崩潰恢復時具體的情況:

情況一:寫入Redo log後,處於Prepare狀態的時候崩潰了,此時:

由於Binlog還沒寫,Redo log處於Prepare狀態還沒提交,所以崩潰恢復的時候,這個事務會回滾,此時Binlog還沒寫,所以也不會傳到備庫。

情況二:假設寫完Binlog之後崩潰了,此時:

Redo log中的日誌是不完整的,處於Prepare狀態,還沒有提交,那麼恢復的時候,首先檢查Binlog中的事務是否完整(事務XIDBinlog里中存在,標誌該事務已經完成),如果事務完整,則直接提交事務,否則回滾事務。

情況三:假設Redo log處於Commit狀態的時候崩潰了,如果Binlog中的事務完整,那麼會重新寫入Commit標誌,並完成提交,否則回滾事務。由此可見,兩階段提交能夠確保數據的一致性。

一般常用的SQL語句都是通過公共介面mysql_execute_command來執行,我們來分析該介面執行的流程:

 

mysql_execute_command
{
   switch (command)
   {
        case SQLCOM_COMMIT
                trans_commit();
                break;
   }
   if thd->is_error()  //語句執行報錯
     trans_rollback_stmt(thd);
  else
trans_commit_stmt(thd); 
}

 

MySQL的Server層有兩個提交函數trans_commit_stmt()和trans_commit()。前者在每個語句執行完成時調用,一般標記語句的結束。而後者是在整個事務真正提交的時候調用,一般對應顯示執行COMMIT語句,或開啟一個新事務BEGIN/START TRANSCATION,或執行一條非臨時表的DDL語句等場景。

3.3 多語句事務提交

多語句事務提交一般指BEGIN/COMMIT顯示事務,主要邏輯在trans_commit()中,以下是具體實現:

// mysql層進行的事務提交
int ha_commit_trans(THD *thd, bool all, bool ignore_global_read_lock) {
  Transaction_ctx *trn_ctx = thd->get_transaction();
  // all為true,意味著當前是事務級提交範圍,否則是語句級提交範圍
  Transaction_ctx::enum_trx_scope trx_scope = all ? Transaction_ctx::SESSION : Transaction_ctx::STMT ;
  // 獲得註冊在當前事務的引擎列表,在trans_register_ha()中初始化
  Ha_trx_info *ha_info = trn_ctx->ha_trx_info(trx_scope);
  // 當前註冊的可讀可寫存儲引擎的數量,只有事務引擎支持讀寫
    uint rw_ha_count = 0;
    // 檢查是否可以跳過兩階段提交機制
    rw_ha_count = ha_check_and_coalesce_trx_read_only(thd, ha_info, all);
    trn_ctx->set_rw_ha_count(trx_scope, rw_ha_count);
  // Prepare 階段
  if (!trn_ctx->no_2pc(trx_scope) && (trn_ctx->rw_ha_count(trx_scope) > 1))
    error = tc_log->prepare(thd, all);
  }
  // Commit 階段
 if (error || (error = tc_log->commit(thd, all))) {
    ha_rollback_trans(thd, all);
    goto end;
  }
}

 

協調者如何確認是否走2PC(兩階段提交)邏輯?

這裡主要根據事務修改是否涉及多個引擎來決定,即函數ha_check_and_coalesce_trx_read_only()。特殊的是,如果打開Binlog,Binlog也會作為參與者而被考慮在內,最終協調者會統計事務中涉及修改的參與者數量。如果數量超過1個,則進行2PC提交流程。

當滿足以上條件,進入Prepare階段,調用Binlog協調器的prepare介面。Prepare階段,Binlog Prepare介面沒什麼可做,而InnoDB Prepare介面主要做的事情就是修改事務和Undo段的狀態,以及記錄XID。

InnoDB Prepare介面會把記憶體中事務對象的狀態修改為TRX_STATE_PREPARED,並將事務對應Undo段在記憶體中的對象狀態修改為TRX_UNDO_PREPARED。然後,把XID信息寫入當前事務對應日誌組的Undo Log Header中的XID區域。修改TRX_UNDO_STATE欄位值和寫入XID,這兩個操作都要修改Undo頁。修改Undo頁之前,會先記錄相應的Redo日誌。最後,刷事務更新產生的Redo日誌。

// innodb prepare,innodb層事務準備階段
static void trx_prepare(trx_t *trx) /*!< in/out: transaction */
{
  lsn_t lsn = 0;
  // 對於系統和undo表空間回滾段,如果有更新需要持久化到redo中
  if (trx->rsegs.m_redo.rseg != nullptr && trx_is_redo_rseg_updated(trx)) {
    // lsn = mtr.commit_lsn(); 開啟第一個mtr,並返回寫入redo log buffer後的最新位點,提交時刻對應的lsn
    lsn = trx_prepare_low(trx, &trx->rsegs.m_redo, false);
  }
  // 對於臨時表空間回滾段,如果有更新不需要持久化到redo中
  if (trx->rsegs.m_noredo.rseg != nullptr && trx_is_temp_rseg_updated(trx)) {
    trx_prepare_low(trx, &trx->rsegs.m_noredo, true);
  }
  // 更新事務和事務系統狀態信息
  trx->state = TRX_STATE_PREPARED;
  trx_sys->n_prepared_trx++;
  // 釋放RC及以下隔離級別的GAP lock
  if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) {
    trx->skip_lock_inheritance = true;
    lock_trx_release_read_locks(trx, true);
  }
  switch (thd_requested_durability(trx->mysql_thd)) {
    // thd初始化時預設設置為HA_REGULAR_DURABILITY
case HA_REGULAR_DURABILITY:
   trx->ddl_must_flush = false;
      // redolog刷新
      trx_flush_log_if_needed(lsn, trx);
  }
}

 

緊接著進入2PC的Commit階段,trans_commit()調用binlog協調器的MYSQL_BIN_LOG::Commit()介面,功能集中在MYSQL_BIN_LOG::ordered_commit()函數中。到了Commit階段,一個事務就已經接近尾聲了。寫操作(包括增、刪、改)已經完成,記憶體中的事務狀態已經修改,Undo狀態也已經修改,XID信息也已經寫入Undo Log Header,Prepare階段產生的Redo日誌已經寫入到Redo日誌文件。剩餘的收尾工作,包括Redo日誌刷盤、事務的Binlog日誌從臨時存放點拷貝到Binlog日誌文件、Binlog日誌文件刷盤以及InnoDB事務提交。

// tc_log->commit ==> MYSQL_BIN_LOG::commit()
MYSQL_BIN_LOG::commit()
//  這個函數很重要,它包含了binlog組提交三步曲,
int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit) {
    //1:Flush Stag:按照事務提交的順序,先刷Redo log到磁碟,然後把每個事務產生的 binlog 日誌從臨時存放點拷貝到 binlog 日誌文件緩存中
    flush_error = process_flush_stage_queue(&total_bytes, &do_rotate, &wait_queue); 
    //2: Sync Stage: binlog 日誌刷盤之前會進入等待過程,目的是為了攢到更多的binlog日誌後,合併IO單次刷盤
sync_binlog_file(false);//binlog fsync to disk
    //3: Commit Stage: 各線程按序提交事務    process_commit_stage_queue(thd, commit_queue);
 }

 

Redo Binlog日誌刷盤都涉及到磁碟IO。如果每提交一個事務,都把該事務中的 Redo日誌、Binlog日誌刷盤,那麼就會涉及到很多小數據量的IO操作,但是頻繁的小數量IO操作非常消耗磁碟的讀寫性能。

為了提高磁碟IO效率併進一步提升事務的提交效率,MySQL從5.6開始引入了Binlog日誌組提交功能。該功能將事務的Commit階段細分為3個子階段。對於每個子階段,都可以有多個事務同時處於該子階段,寫日誌和刷盤操作可以合併。

  • Flush子階段,先將Redo日誌刷盤,接著將所有的binlog caches寫入到binlog文件緩存中。
  • Sync子階段,對binlog文件緩存做fsync操作,多個線程的 binlog 合併為一次刷盤。
  • Commit子階段,依次將redolog中已經prepare的事務在引擎層提交,commit階段不用刷盤,因為flush階段中的redolog刷盤已經足夠保證資料庫崩潰時的數據安全了。當前Commit子階段主要包含了InnoDB層的事務提交,真正執行事務提交入口函數為trx_commit_low()。trx_commit_low()主要分成兩個部分trx_write_serialisation_history()和trx_commit_in_memory()。trx_write_serialisation_history()處理整個事務執行過程中所使用insert/update的回滾段的收尾工作。trx_commit_in_memory()在記憶體中設置事務提交的標誌trx->state = TRX_STATE_COMMITTED_IN_MEMORY,本事務的數據可以即刻被其他事務可見;在設置事務提交已經完成的標誌後,才會釋放當前事務的Read View和事務過程中所持有的table lock和record lock,清除trx_sys系統中的當前事務等。

3.4 單語句事務提交

從SQL的執行過程分析可以看到,無論執行何種語句,最後都會執行trans_commit_stmt(),即單語句提交函數。如果當前是單語句事務,一般指AUTOCOMMIT為ON的場景,那麼會走事務提交邏輯,即ha_commit_trans()函數。額外考慮到COMMIT和DDL語句等已經在調用trans_commit_stmt()之前將事務提交,所以在這裡只需要標記語句結束即可。

// 執行單語句事務提
bool trans_commit_stmt(THD *thd, bool ignore_global_read_lock) {
  int res = false;
  // 單語句事務,需要走2PC提交邏輯
  if (thd->get_transaction()->is_active(Transaction_ctx::STMT)) {
    res = ha_commit_trans(thd, false, ignore_global_read_lock);
  } else if (tc_log)
    // COMMIT/DDL等,只需要走引擎層提交邏輯,置為false,只標識語句結束,跳過真正提交階段
    res = tc_log->commit(thd, false);
  thd->get_transaction()->reset(Transaction_ctx::STMT);
  return res;
}

 

ha_commit_trans()最後會走到innobase_commit()中,innobase_commit()中的參數commit_trx控制是否真的進行存儲引擎層的提交處理,trans_commit_stmt()里會設置 commit_trx為0,允許跳過事務提交。

這裡的判斷邏輯是,只有當commit_trx= 1或者設置autocommit=1的情況下,才會真正進入事務提交邏輯。而多語句事務對應的trans_commit()函數里會設置commit_trx=1,進入innobase_commit_low()執行真正的事務提交邏輯。

/** 在innodb層提交一個事務
thd:需要提交事務的會話
commit_trx:true,需要提交事務。false,跳過事務提交。 
 */
static int innobase_commit(handlerton *hton, THD *thd, bool commit_trx) 
{
  trx_t *trx = check_trx_exists(thd);
  // innobase_commi僅在“真正的”commit時被調用,而且在每個語句之後(走trans_commit_stmt()函數)也被調用,因此這裡需要will_commit判斷是否要真正去提交事務。
  bool will_commit =
      commit_trx ||
      (!thd_test_options(thd, OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN)); // autocommit=1且不在顯示事務塊中
  if (will_commit) {
    /* 在顯示提交commit,或者autocommit=1、且不在顯示事務塊內*/
    innobase_commit_low(trx);
  } else {
    /* 其他情況,我們只是標記SQL語句結束,不做事務提交 */
    trx_mark_sql_stat_end(trx);
  }
  return 0;
}

 

4. 總結

本文從多語句/單語句事務提交原理角度出發,介紹了MySQL的兩階段提交協議。在prepare階段,InnoDB把數據更新到記憶體後記錄Redo log,此時Redo log的狀態為prepare狀態;在Commit階段,Server生成Binlog後落盤,InnoDB把剛寫入的Redo log狀態更新為commit狀態。兩階段提交保證了事務在多個引擎和Binlog之間的原子性,同樣保證了通過備份和Binlog恢復出的資料庫和原資料庫的數據一致性。

點擊關註,第一時間瞭解華為雲新鮮技術~


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

-Advertisement-
Play Games
更多相關文章
  • 單條記錄大小增長倍數和ibd文件大小的增長倍數不成正比 環境信息 資料庫版本: GreatSQL 8.0.25 字元集:utf8mb4 innodb_default_row_format: dynamic innodb_page_size: 16384 問題描述 表數據為新insert數據,無del ...
  • Web層緩存顯著提高了應用性能,通過減少重覆的數據處理和資料庫查詢來加快響應時間。Redis作為高效的記憶體數據結構存儲系統,在實現緩存層中發揮了重要作用,它支持各種數據結構,能夠迅速存取數據,從而減少資料庫負擔,提升用戶體驗。然而,緩存機制也面臨挑戰,如緩存穿透、緩存擊穿和緩存雪崩等問題。緩存穿透通... ...
  • 《數據資產管理核心技術與應用》是清華大學出版社出版的一本圖書,全書共分10章,第1章主要讓讀者認識數據資產,瞭解數據資產相關的基礎概念,以及數據資產的發展情況。第2~8章主要介紹大數據時代數據資產管理所涉及的核心技術,內容包括元數據的採集與存儲、數據血緣、數據質量、數據監控與告警、數據服務、數據許可權 ...
  • AntV團隊迅速將G6圖可視化引擎融入Awesome-Graphs項目,發佈1.2.0版本,提升交互體驗,包括路徑高亮、模糊搜索等功能,現邀請體驗並徵集改進意見。 ...
  • 在當今數字化的時代,數據已然成為企業決策與運營的關鍵要素。而保障數據的完整性、準確性以及及時性,對於企業的發展有著舉足輕重的意義。在數據運維管理範疇內,補數據屬於大數據開發和運維人員常用的運維操作手段。 周期補數據和定時補數據作為兩個相對特殊的補數據方式,在各類不同的場景中均發揮著至關重要的作用。 ...
  • GreatSQL 並行Load Data加快數據導入 資料庫信息 資料庫版本:GreatSQL 8.0.32-25 Clickhouse表需要導入到 GreatSQL 中,表數據量龐大所以選用導出CSV的方式。 測試數據復現操作 load data MySQL load data 語句能快速將一個文 ...
  • 1.資料庫結構優化 一個好的資料庫設計方案對於資料庫的性能往往會起到事半功倍的效果。優化設計需要考慮數據冗餘、查詢和更新的速度、欄位的數據類型是否合理等多方面的因素。 將欄位很多的表分解成多個表 概述:對於欄位較多的表,如果有些欄位的使用頻率很低,可以將這些欄位分離出來形成新表。這樣可以減少表的數據 ...
  • 指標是反映企業的各項核心業務活動、管理成效的數據體系,指標體系作為聯結業務邏輯與數據實體的關鍵橋梁,是構建高質量數據統計的基礎單元,併在量化業務績效和效果評估中扮演著核心角色。 為了更好地服務於客戶並提供切實可行的實踐指導,自4月24日起,袋鼠雲將推出全新《指標體系建設實戰》系列直播。該系列內容覆蓋 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...