多資料庫有序GUID

来源:http://www.cnblogs.com/tdfblog/archive/2017/11/20/SequentialGuid.html
-Advertisement-
Play Games

本文主要討論在資料庫中使用GUID類型作為主鍵時常見的弊端,以及解決辦法,希望您能有所收穫。 ...


背景

常見的一種資料庫設計是使用連續的整數為做主鍵,當新的數據插入到資料庫時,由資料庫自動生成。但這種設計不一定適合所有場景。

隨著越來越多的使用Nhibernate、EntityFramework等ORM(對象關係映射)框架,應用程式被設計成為工作單元(Unit Of Work)模式,需要在數據持久化之前生成主鍵,為了保證在多線程併發以及站點集群環境中主鍵的唯一性,最簡單最常見的方式是將主鍵設計成為GUID類型。

工作單元是資料庫應用程式經常使用的一種設計模式,簡單一點來說,就是對多個資料庫操作進行打包,記錄對象上的所有變化,併在最後提交時一次性將所有變化通過系統事務寫入資料庫。目的是為了減少資料庫調用次數以及避免資料庫長事務。關於工作單元的知識可以在園子裡面搜索到很多,在這裡就不做詳細的介紹了。

GUID(全球唯一標識符)也稱為UUID,是一種由演算法生成的二進位長度為128位的數字標識符。在理想情況下,任何電腦和電腦集群都不會生成兩個相同的GUID。GUID 的總數達到了2^128(3.4×10^38)個,所以隨機生成兩個相同GUID的可能性非常小,但並不為0。GUID一詞有時也專指微軟對UUID標準的實現。

RFC 41222描述了創建標準GUID,如今大多數GUID生成演算法通常是一個很長的隨機數,再結合一些像網路MAC地址這種隨機的本地組件信息。

GUID的優點允許開發人員隨時創建新值,而無需從資料庫伺服器檢查值的唯一性,這似乎是一個完美的解決方案。

很多資料庫在創建主鍵時,為了充分發揮資料庫的性能,會自動在該列上創建聚集索引。我們先來說一說什麼是聚集索引,。集索引確定表中數據的物理順序。聚集索引類似於電話簿,按姓氏排列數據。由於聚集索引規定數據在表中的物理存儲順序,因此一個表也只能包含一個聚集索引。它能夠快速查找到數據,但是如果插入資料庫的主鍵不在列表的末尾,向表中添加新行時就非常緩慢。例如,看下麵這個例子,在表中已經存在三行數據(例子來自Jeremy Todd的博客GUIDs as fast primary keys under multiple databases

ID Name
1 Holmes, S.
4 Watson, J.
7 Moriarty, J.

此時非常簡單:數據行按對應ID列的順序儲存。如果我們新添加一行ID為8的數據,不會產生任何問題,新行會追加的末尾。

ID Name
1 Holmes, S.
4 Watson, J.
7 Moriarty, J.
8 Lestrade, I.

但如果我們想插入一行的ID為5的數據:

ID Name
1 Holmes, S.
4 Watson, J.
5 Hudson, Mrs.
7 Moriarty, J.
8 Lestrade, I.

ID為7,8的數據行必須向下移動。雖然在這算什麼事兒,但當您的數據量達到數百萬行的級別之後,這就是個問題了。如果你還想要每秒處理上百次這種請求,那可真是難上加難了。

這就是GUID主鍵引發的問題:它是隨機產生的,所以在數據插入時,隨時都會涉及到數據的移動,導致插入會很緩慢,還會涉及大量不必要的磁碟活動。總結果有兩點:

GUID最關鍵的問題就是它是隨機的。我們需要設計一種有規則的GUID生成方式,在之後生成的GUID類型總是比之前的要大,保證插入資料庫的主鍵是在列表末尾追加的,這種我們稱之為有序GUID

GUID排序規則

在講解有序GUID之前,我們必須先瞭解一下GUID在.Net中以及各個資料庫中的排序規則,排序規則不一樣,生成有序GUID的規則也會隨之變化。

128位的GUID主要有4部分組成:Data1, Data2, Data3, and Data4,你可以看成下麵這樣:

11111111-2222-3333-4444-444444444444

Data1 占4個位元組, Data2 2個位元組, Data3 2個位元組加 Data4 8個位元組。我們分別的對個位元組編上序號:

序號 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Value 11 11 11 11 - 22 22 - 33 33 - 44 44 - 44 44 44 44 44 44

GUID在.Net中的排序規則

在.Net中,GUID預設的排序過段規則是按左到右的,看下麵這個示例。

    var list = new List<Guid> {
        new Guid("00000000-0000-0000-0000-010000000000"),
        new Guid("00000000-0000-0000-0000-000100000000"),
        new Guid("00000000-0000-0000-0000-000001000000"),
        new Guid("00000000-0000-0000-0000-000000010000"),
        new Guid("00000000-0000-0000-0000-000000000100"),
        new Guid("00000000-0000-0000-0000-000000000001"),
        new Guid("00000000-0000-0000-0100-000000000000"),
        new Guid("00000000-0000-0000-0010-000000000000"),
        new Guid("00000000-0000-0001-0000-000000000000"),
        new Guid("00000000-0000-0100-0000-000000000000"),
        new Guid("00000000-0001-0000-0000-000000000000"),
        new Guid("00000000-0100-0000-0000-000000000000"),
        new Guid("00000001-0000-0000-0000-000000000000"),
        new Guid("00000100-0000-0000-0000-000000000000"),
        new Guid("00010000-0000-0000-0000-000000000000"),
        new Guid("01000000-0000-0000-0000-000000000000")
    };
    list.Sort();

    foreach (Guid guid in list) {
        Console.WriteLine(guid.ToString());
    }

輸出結果:

00000000-0000-0000-0000-000000000001
00000000-0000-0000-0000-000000000100
00000000-0000-0000-0000-000000010000
00000000-0000-0000-0000-000001000000
00000000-0000-0000-0000-000100000000
00000000-0000-0000-0000-010000000000
00000000-0000-0000-0010-000000000000
00000000-0000-0000-0100-000000000000
00000000-0000-0001-0000-000000000000
00000000-0000-0100-0000-000000000000
00000000-0001-0000-0000-000000000000
00000000-0100-0000-0000-000000000000
00000001-0000-0000-0000-000000000000
00000100-0000-0000-0000-000000000000
00010000-0000-0000-0000-000000000000
01000000-0000-0000-0000-000000000000

通過上面的輸出結果,我們可以得到排序的權重如下:

序號 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
權重 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Value 11 11 11 11 - 22 22 - 33 33 - 44 44 - 44 44 44 44 44 44

這與數字排序規則一致,從右到左進行依次進行排序(數字越小,權重越高,排序的優先順序越高)。

GUID在各個資料庫中的排序規則

在SQL Server資料庫中,我們有一種非常簡單的方式來比較兩個GUID類型的大小值(其實在SQL Server資料庫中稱為UniqueIdentifier類型):

With UIDs As (--                         0 1 2 3  4 5  6 7  8 9  A B C D E F
            Select ID =  1, UID = cast ('00000000-0000-0000-0000-010000000000' as uniqueidentifier)
    Union   Select ID =  2, UID = cast ('00000000-0000-0000-0000-000100000000' as uniqueidentifier)
    Union   Select ID =  3, UID = cast ('00000000-0000-0000-0000-000001000000' as uniqueidentifier)
    Union   Select ID =  4, UID = cast ('00000000-0000-0000-0000-000000010000' as uniqueidentifier)
    Union   Select ID =  5, UID = cast ('00000000-0000-0000-0000-000000000100' as uniqueidentifier)
    Union   Select ID =  6, UID = cast ('00000000-0000-0000-0000-000000000001' as uniqueidentifier)
    Union   Select ID =  7, UID = cast ('00000000-0000-0000-0100-000000000000' as uniqueidentifier)
    Union   Select ID =  8, UID = cast ('00000000-0000-0000-0010-000000000000' as uniqueidentifier)
    Union   Select ID =  9, UID = cast ('00000000-0000-0001-0000-000000000000' as uniqueidentifier)
    Union   Select ID = 10, UID = cast ('00000000-0000-0100-0000-000000000000' as uniqueidentifier)
    Union   Select ID = 11, UID = cast ('00000000-0001-0000-0000-000000000000' as uniqueidentifier)
    Union   Select ID = 12, UID = cast ('00000000-0100-0000-0000-000000000000' as uniqueidentifier)
    Union   Select ID = 13, UID = cast ('00000001-0000-0000-0000-000000000000' as uniqueidentifier)
    Union   Select ID = 14, UID = cast ('00000100-0000-0000-0000-000000000000' as uniqueidentifier)
    Union   Select ID = 15, UID = cast ('00010000-0000-0000-0000-000000000000' as uniqueidentifier)
    Union   Select ID = 16, UID = cast ('01000000-0000-0000-0000-000000000000' as uniqueidentifier)
)
Select * From UIDs Order By UID, ID

例子來自Ferrari的博客How are GUIDs sorted by SQL Server?

查詢結果:

ID UID
16 01000000-0000-0000-0000-000000000000
15 00010000-0000-0000-0000-000000000000
14 00000100-0000-0000-0000-000000000000
13 00000001-0000-0000-0000-000000000000
12 00000000-0100-0000-0000-000000000000
11 00000000-0001-0000-0000-000000000000
10 00000000-0000-0100-0000-000000000000
9 00000000-0000-0001-0000-000000000000
8 00000000-0000-0000-0010-000000000000
7 00000000-0000-0000-0100-000000000000
6 00000000-0000-0000-0000-000000000001
5 00000000-0000-0000-0000-000000000100
4 00000000-0000-0000-0000-000000010000
3 00000000-0000-0000-0000-000001000000
2 00000000-0000-0000-0000-000100000000
1 00000000-0000-0000-0000-010000000000

通過上面可以得於是如下結果:

  1. 先按每1-8從左到右進行排序;
  2. 接著按第9-10位從右到左進行排序;
  3. 最後按後11-16位從右到左進行排序;

通過分析,我們可得到如下權重列表:

序號 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
權重 16 15 14 13 12 11 10 9 7 8 1 2 3 4 5 6
Value 11 11 11 11 - 22 22 - 33 33 - 44 44 - 44 44 44 44 44 44

在Microsoft官方文檔中,有一篇文檔關於GUID與uniqueidentifier的值比較:
Comparing GUID and uniqueidentifier Values

不同的資料庫處理GUID的方式也是不同的。在SQL Server存在內置GUID類型,沒有原生GUID支持的資料庫通過模擬來方式來實現的。在Oracle保存為raw bytes類型,具體類型為raw(16);在MySql中通常將GUID儲存為char(36)的字元串形式。

關於Oracle、MySql資料庫的排序規則與.Net中排序規則,不過篇章的限制,這裡不再做具體的演示,不過我在github上提供了示例SQL語句:https://gist.github.com/tangdf/f0aed064ba10bfa0050e4344b9236889,您可以自己進行測試。我們在這裡只給出最終的結論:

小結:

  1. .Net中GUID的排序規則是從左到右依次進行排序,與數字排序規則一致;
  2. Sql Server資料庫提供對GUID類型的支持,在資料庫中稱為UniqueIdentifier類型,但是排序規則比較複雜:
    • 先按每1-8從左到右進行排序;
    • 接著按第9-10位從右到左進行排序;
    • 最後按後11-16位從右到左進行排序;
  3. Oracle資料庫未提供對GUID類型的支持,使用的是raw bytes類型保存數據raw(16),具體類型為,排序規則與GUID在.Net中規則一致;
  4. MySql數據未提供對GUID類型的支持,使用的是字元串的類型保存數據,使用是的char(36)類型,由於使用的是字元串類型,排序規則與GUID在.Net中的規則一致。

有序GUID

有序GUID是有規則的生成GUID,保存在之後生成的GUID類型總是比之前的要大。不過在上一節中,已經提到過各個資料庫對GUID支持不一樣,而且排序的規則也不一樣,所以我們需要為每一個資料庫提供不一致的有序GUID生成規則。

UuidCreateSequential函數

我們都知道SQL Server資料庫有一個NewSequentialId()函數,用於創建有序GUID。在創建表時,可以將它設置成為GUID類型欄位的預設值,在插入新增數據時自動創建主鍵的值(該函數只能做為欄位的預設值,不能直接在SQL中調用)。

示例:

Create Table TestTable
       (
         ID UniqueIdentifier Not Null Default ( NewSequentialId() ) ,
         Number Int
       );

NewSequentialId()函數只能在資料庫使用,不過在 Microsoft 的 MSDN 文檔中有說明,NEWSEQUENTIALID 是對 Windows UuidCreateSequential 函數的包裝https://msdn.microsoft.com/zh-cn/library/ms189786(v=sql.120).aspx。這樣我們可以在C#通過非托管方法調用:

   [System.Runtime.InteropServices.DllImport("rpcrt4.dll", SetLastError = true)]
   private static extern int UuidCreateSequential(out Guid guid);

   public static Guid NewSequentialGuid()
   {
       const int RPC_S_OK = 0;

       int result = UuidCreateSequential(out var guid);
       if (result != RPC_S_OK) {
           throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error());
       }

       return guid;
   }

不這個方法也存在三個問題:

  1. 這個方法涉及到安全問題,UuidCreateSequential函數依賴的計算硬體,該方法的後12位其實是網卡的MAC地址。這是我電腦生成的一組有序GUID。

    {A2A93393-C8DC-11E7-B133-2C56DC497A97}
    {A2A93394-C8DC-11E7-B133-2C56DC497A97}
    {A2A93395-C8DC-11E7-B133-2C56DC497A97}
    {A2A93396-C8DC-11E7-B133-2C56DC497A97}
    {A2A93397-C8DC-11E7-B133-2C56DC497A97}
    {A2A93398-C8DC-11E7-B133-2C56DC497A97}
    {A2A93399-C8DC-11E7-B133-2C56DC497A97}
    {A2A9339A-C8DC-11E7-B133-2C56DC497A97}
    {A2A9339B-C8DC-11E7-B133-2C56DC497A97}
    {A2A9339C-C8DC-11E7-B133-2C56DC497A97}

    這是我電腦的網卡的MAC地址:

  2. 由於UuidCreateSequential函數生成的有序GUID中包括MAC地址,所以如果在伺服器集群環境中,肯定存在一臺伺服器A上生成的有序GUID總比另一臺伺服器B生成要更小,伺服器A產生的數據插入到資料庫時,由於聚集索引的問題,總是會移動伺服器B已經持久化到資料庫中的數據。集群的伺服器越多,產生的IO問題更嚴重。在伺服器群集環境中,需要自行實現有序GUID。

  3. UuidCreateSequential函數生成的GUID規則與SQL Server中排序的規則存在不一致,這樣仍然會導致嚴重的IO問題,所以需要將GUID重新排序後再持久化到資料庫。例如上面列出生成的GUID列表,依次生成的數據可以看出,是第4位位元組在自增長,在這與任何一個資料庫的排序規則都不一致;關於該函數生成的規則,可以見此鏈接:https://stackoverflow.com/questions/5585307/sequential-guids

下麵的方法是將生成的GUID調整成為適合Sql Server使用的有序GUID(針對其它資料庫支持,您可以按排序規則自行修改):


[System.Runtime.InteropServices.DllImport("rpcrt4.dll", SetLastError = true)]
static extern int UuidCreateSequential(byte[] buffer);

static Guid NewSequentialGuid() {

    byte[] raw = new byte[16];
    if (UuidCreateSequential(raw) != 0)
        throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error());

    byte[] fix = new byte[16];

    // reverse 0..3
    fix[0x0] = raw[0x3];
    fix[0x1] = raw[0x2];
    fix[0x2] = raw[0x1];
    fix[0x3] = raw[0x0];

    // reverse 4 & 5
    fix[0x4] = raw[0x5];
    fix[0x5] = raw[0x4];

    // reverse 6 & 7
    fix[0x6] = raw[0x7];
    fix[0x7] = raw[0x6];

    // all other are unchanged
    fix[0x8] = raw[0x8];
    fix[0x9] = raw[0x9];
    fix[0xA] = raw[0xA];
    fix[0xB] = raw[0xB];
    fix[0xC] = raw[0xC];
    fix[0xD] = raw[0xD];
    fix[0xE] = raw[0xE];
    fix[0xF] = raw[0xF];

    return new Guid(fix);
}

小結:
UuidCreateSequential函數存在隱私的問題,不適合集群環境,並且需要重新排序後再提交到資料庫;

COMB解決方案

COMB 類型的GUID 是由Jimmy Nilsson在他的“The Cost of GUIDs as Primary Keys”一文中設計出來的。
基本設計思路是這樣的:既然GUID數據生成是隨機的造成索引效率低下,影響了系統的性能,那麼能不能通過組合的方式,保留GUID的前10個位元組,用後6個位元組表示GUID生成的時間(DateTime),這樣我們將時間信息與GUID組合起來,在保留GUID的唯一性的同時增加了有序性,以此來提高索引效率(這是針對Sql Server資料庫來設計的)。

在NHibernate框架中已經實現該功能,可以在github上看到實現方式:https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs#L25-L72

在EF以及EF Core也同樣實現了類似的解決方案,EF Core的實現方式:https://github.com/aspnet/EntityFrameworkCore/blob/f7f6d6e23c8e47e44a61983827d9e41f2afe5cc7/src/EFCore/ValueGeneration/SequentialGuidValueGenerator.cs#L25-L44

在這裡介紹一下使用的方式,由EF Core框架自動生成有序GUID的方式:

    public class SampleDbContext : DbContext
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<GuidEntity>(b =>
            {
                b.Property(e => e.Id).HasValueGenerator<SequentialGuidValueGenerator>();
            });
        }
    }

但是請註意,這兩個ORM的解決方案只針對Sql Server資料庫,因為只保證了最後幾位位元組是按順序來生成的。

SequentialGuid框架

SequentialGuid框架也是我要推薦給您,因為它提供了常見資料庫生成有序Guid的解決方案。

關於該框架的設計思路以及針對各個資料庫的性能測試,見鏈接:https://www.codeproject.com/Articles/388157/GUIDs-as-fast-primary-keys-under-multiple-database

使用方式,建議您參考ABP框架,在ABP中使用SequentialGuid框架來生成有序GUID,關鍵代碼鏈接:https://github.com/aspnetboilerplate/aspnetboilerplate/blob/b36855f0c238c3592203f058c641862844a0614e/src/Abp/SequentialGuidGenerator.cs#L36-L51

總結

我們來解決一下:

  1. 在資料庫中最好不要使用隨機的GUID,它會影響性能;
  2. 在SQL Server中提供了NewSequentialId函數來生成有序GUID;
  3. 各個資料庫對GUID支持的不一樣,而且排序的規則也不一樣;
  4. UuidCreateSequential函數存在隱私的問題,不適合集群環境,並且需要重新排序後再提交到資料庫;
  5. 各ORM框架提供了有序GUID的支持,但是其實只是針對Sql Server資料庫設計的;
  6. 推薦您使用SequentialGuid框架,它解決了多資料庫以及集群環境的問題。

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

-Advertisement-
Play Games
更多相關文章
  • Windows常用的分區格式有三種,分別是FAT16、FAT32、NTFS格式。 在Linux操作系統里有Ext2、Ext3、Linux swap和VFAT四種格式。 FAT16: 作為一種文件名稱,FAT(File Allocation Table,文件分配表)自1981年問世以來,已經成為一個計 ...
  • In Doing We Learning 在操作中學習。如果只是光看教程,沒有實際的操作,對編程語言的理解很空泛,所以決定從單片機中學習C語言。 #include<reg52.h> //包含的頭文件。 sbit LED = P0^0; //這裡就是對P0寄存器進行位操作。相當於對P0^0位賦予了一個 ...
  • 會話保持的三種方式 Session sticky會話綁定:通過在前端調度器的配置中實現統一session發送至同一後髮端伺服器 Session cluster會話集群:通過配置Tomcat保持所有Tomcat的session的信息一致。 Session server 會話服務:將所有的session ...
  • 1》相關知識簡介: 1>常用的磁碟IO調度器: CFQ:完全公平隊列演算法; deadline:最後期限演算法; anticipatory:順序讀寫隊列演算法/預期演算法; NOOP:no operation,最簡單的調度演算法; 2>如何替換IO調度演算法:預設演算法為CFQ; /sys/block/<devi ...
  • kickstart 部署 - 無人值守安裝系統(linux) ...
  • 1.安裝nodejs,自帶npm環境。 地址:https://nodejs.org/en/download/,Node.js 歷史版本下載地址:https://nodejs.org/dist/ 可自行選擇適合自己操作系統的版本。 安裝過程參見:http://www.runoob.com/nodejs ...
  • 本示例主要學習如果對線程池中的操作實現超時,併在線程池中正確等待。 線程池還有一個ThreadPool.RegisterWaitForSingleObject,這個方法允許我們將回調函數放入線程池中的隊列中。當提供的等待事件處理器接收到信號或發生超時時,這個回調函數將被調用,這樣就實現了... ...
  • 返回總目錄 本小節目錄 Split Temporary Variable(分解臨時變數) Remove Assignments to Parameters(移除對參數的賦值) Remove Assignments to Parameters(移除對參數的賦值) 6.6Split Temporary ...
一周排行
    -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# ...