在前面的 MES → SAP 入庫案例中,我們使用了 transactionId 來識別一次業務操作。

例如:

transactionId = 5f77e36d-1b64-4a5c-a16b-f57567086375

在 .NET 中通常可以直接產生:

Guid transactionId = Guid.NewGuid();

問題是:

為什麼要用 GUID?
用流水號 1、2、3、4... 不行嗎?

要理解這件事,就要先理解 分散式系統最大的問題之一:沒有一個所有節點都能隨時依賴的中央發號機制。


先從單機系統開始

假設今天所有 Request 都只會進入同一台 Server:

Mermaid · 圖表原始碼
flowchart LR
    Client --> Server
    Server --> DB

我們可能很自然地使用:

1
2
3
4
5

例如 SQL Server:

Id BIGINT IDENTITY(1,1)

因為所有資料最後都進到同一個 Database,由 Database 負責:

下一號是多少?

這種方式非常簡單。


但分散式系統不一樣

系統規模變大後,可能變成:

Mermaid · 圖表原始碼
flowchart LR
    Client --> LB[Load Balancer]
 
    LB --> A[MES Instance A]
    LB --> B[MES Instance B]
    LB --> C[MES Instance C]
 
    A --> SAP
    B --> SAP
    C --> SAP

現在有三台 MES Server。

假設它們都必須產生 Transaction ID。

如果我們使用:

1
2
3
4...

問題就出現了。


誰負責決定下一個數字?

假設:

MES A
目前產生到 100
 
MES B
目前產生到 100

下一秒兩台機器同時收到 Request:

MES A → 101
 
MES B → 101

Transaction ID 就撞號了。

你當然可以建立一個中央服務:

Mermaid · 圖表原始碼
flowchart LR
    A[MES A] --> ID[ID Generator]
    B[MES B] --> ID
    C[MES C] --> ID
 
    ID --> DB

每次都問:

下一個 ID 是多少?

這樣確實可以解決。

但現在你又多了一個問題:

ID Generator 掛掉怎麼辦?

所有系統可能都無法產生 ID。

而且每一次建立資料,都多了一次 Network IO:

Application
    ↓
詢問 ID Server
    ↓
取得 ID
    ↓
執行真正業務

系統之間也因此產生額外耦合。


GUID 解決的是什麼?

GUID / UUID 的核心思想就是:

每一個節點可以自己產生 ID,而不需要先問中央 Server。

UUID 標準定義的是 128-bit Identifier,設計目的就是讓不同時間、不同節點產生的識別碼具有極低的碰撞機率,而且不需要中央註冊機制。現行 UUID 標準是 RFC 9562,它取代了舊的 RFC 4122。

因此:

Mermaid · 圖表原始碼
flowchart TB
    A[MES Instance A<br/>Guid.NewGuid]
    B[MES Instance B<br/>Guid.NewGuid]
    C[MES Instance C<br/>Guid.NewGuid]
 
    A --> SAP
    B --> SAP
    C --> SAP

三台 Server 不需要互相詢問。

例如:

MES A
5f77e36d-1b64-4a5c-a16b-f57567086375
 
MES B
d70fc6ba-fbc6-4ceb-b386-8ac32b835697
 
MES C
799aabf7-1308-4d70-8755-c20435218891

這三台機器甚至不需要知道彼此存在。

這就是 GUID 在分散式系統中非常重要的一個特性:

Decentralized ID Generation

也就是:

去中心化產生識別碼。


GUID 和 UUID 有什麼不同?

概念上可以先把它們理解成同一類東西。

RFC 9562 也直接將 UUID 描述為也被稱為 GUID 的 128-bit Identifier。

在 Microsoft / .NET 生態裡你比較常看到:

Guid

例如:

var transactionId = Guid.NewGuid();

在 SQL Server 則是:

uniqueidentifier

SQL Server 的 uniqueidentifier 是 16 Bytes,也就是 128 bits。

所以通常會是:

C#
 
Guid
 
↓
 
SQL Server
 
uniqueidentifier

為什麼 MES → SAP 特別適合 GUID?

回到我們原本的問題。

MES 要送一筆入庫:

工單:WO001
料號:MAT001
數量:5

送出前:

var transactionId = Guid.NewGuid();

得到:

5f77e36d-1b64-4a5c-a16b-f57567086375

然後:

Mermaid · 圖表原始碼
sequenceDiagram
    participant MES
    participant SAP
 
    MES->>MES: Guid.NewGuid()
    Note over MES: 5f77e36d...
 
    MES->>SAP: 入庫 5 + TransactionId
    SAP->>SAP: 執行入庫
    SAP-->>MES: Success

這個 GUID 代表的是:

這一次業務操作

而不是:

這一次 HTTP Request

這個差異非常重要。


Retry 時不能重新產生 GUID

假設:

Mermaid · 圖表原始碼
sequenceDiagram
    participant MES
    participant SAP
 
    MES->>SAP: ABC123
    SAP->>SAP: 入庫成功
    SAP--xMES: Response Timeout

MES 不知道 SAP 到底成功還是失敗。

所以 MES Retry。

正確:

第一次
ABC123
 
Retry
ABC123
 
Retry
ABC123

錯誤:

第一次
ABC123
 
Retry
DEF456
 
Retry
GHI789

如果每 Retry 一次都重新:

Guid.NewGuid();

SAP 看到的會是:

ABC123 → 新操作
DEF456 → 新操作
GHI789 → 新操作

SAP 根本不知道它們其實是同一次業務操作。

因此:

GUID 必須在第一次業務操作開始時產生,之後所有 Retry 都沿用同一個 GUID。


正確的生命週期

應該是:

Mermaid · 圖表原始碼
sequenceDiagram
    participant MES
    participant SAP
 
    MES->>MES: 建立 Business Command
    MES->>MES: TransactionId = Guid.NewGuid()
 
    MES->>SAP: TransactionId = ABC123
    SAP--xMES: Timeout
 
    Note over MES: 不重新產生 GUID
 
    MES->>SAP: Retry ABC123
    SAP-->>MES: 回傳第一次執行結果

換句話說:

Business Command
      │
      └── TransactionId
             │
             ├── HTTP Request #1
             ├── HTTP Request #2 Retry
             └── HTTP Request #3 Retry

不是:

HTTP Request #1 → GUID A
HTTP Request #2 → GUID B
HTTP Request #3 → GUID C

GUID 和冪等性的關係

GUID 本身不會讓 API 自動變成冪等。

GUID 只是提供:

「這是不是同一個 Business Command?」

的識別依據。

真正的冪等機制還需要:

GUID
+
Database Unique Constraint
+
Transaction
+
Result / Status

例如:

CREATE TABLE SapInboundRequest
(
    Id BIGINT IDENTITY(1,1) NOT NULL
        CONSTRAINT PK_SapInboundRequest PRIMARY KEY,
 
    TransactionId UNIQUEIDENTIFIER NOT NULL,
 
    Status VARCHAR(20) NOT NULL,
 
    CreatedAt DATETIME2 NOT NULL,
 
    CONSTRAINT UQ_SapInboundRequest_TransactionId
        UNIQUE (TransactionId)
);

此時:

ABC123

最多只能存在一筆。


如果相同 Request 同時進來

例如因為 Retry、Queue Redelivery 或多 Instance:

Mermaid · 圖表原始碼
sequenceDiagram
    participant A as MES Instance A
    participant B as MES Instance B
    participant SAP
    participant DB
 
    A->>SAP: ABC123
    B->>SAP: ABC123
 
    SAP->>DB: INSERT ABC123
    SAP->>DB: INSERT ABC123
 
    DB-->>SAP: 第一筆成功
    DB-->>SAP: 第二筆 Unique Constraint Conflict

Database 最後負責保證:

ABC123
只能存在一次

所以 GUID 和 Unique Constraint 是兩個不同角色:

GUID
↓
告訴系統「我是誰」
 
Unique Constraint
↓
保證「這個 ID 只能存在一次」

GUID 的碰撞機率雖然極低,但工程上仍然不代表可以省略 Database Constraint。


為什麼不用時間當 Transaction ID?

有些系統會想:

20261001093000123

看起來也很唯一。

但如果:

Machine A
20261001093000123
 
Machine B
20261001093000123

兩台機器剛好同一時間產生,就可能撞號。

你可以繼續加入:

時間
+
Machine ID
+
Process ID
+
Counter

最後你其實就是在自己設計一套 Distributed ID Generator。

除非有特殊需求,否則通常沒必要自己重新發明。


為什麼不用 Database Identity?

例如:

BIGINT IDENTITY(1,1)

Identity 本身很好,而且在 Database 內通常比 GUID 更適合當 Clustered Primary Key。

但它有一個限制:

必須先進 Database,才能知道 ID。

而 MES → SAP 的 Transaction ID 必須在:

MES 呼叫 SAP 之前

就已經存在。

因為:

MES
↓
產生 Transaction ID
↓
送 SAP
↓
Timeout
↓
Retry 同一 Transaction ID

所以它需要的是:

Client-generated ID

而 GUID 非常適合這個用途。


GUID 最重要的分散式特性

假設現在系統有:

台北機房
MES A
 
新竹機房
MES B
 
雲端
MES C

它們可能:

不同 Server
不同 Process
不同 Container
不同 Region
不同 Database

但都可以:

Guid.NewGuid();

不需要:

Distributed Lock
Central Database
Central ID Server
互相溝通

就能先產生一個全域識別碼。

這就是 GUID 在 Distributed System 裡的價值。


一個很典型的分散式流程

Mermaid · 圖表原始碼
sequenceDiagram
    participant User
    participant MES1 as MES Instance A
    participant MES2 as MES Instance B
    participant Queue
    participant SAP
 
    User->>MES1: 建立入庫操作
 
    MES1->>MES1: Guid.NewGuid() = ABC123
 
    MES1->>Queue: Command ABC123
 
    Queue->>MES2: ABC123
 
    MES2->>SAP: ABC123
 
    SAP--xMES2: Response Lost
 
    Queue->>MES1: Redelivery ABC123
 
    MES1->>SAP: ABC123
 
    Note over SAP: 相同 TransactionId<br/>判定為同一次業務操作

你可以發現:

MES A
MES B
Queue
SAP

完全是不同 Component。

但是:

ABC123

可以一路穿過整個系統。

這就是分散式系統裡常見的:

Global Identifier

概念。


Transaction ID 不要和 Correlation ID 搞混

這也是實務上很常搞混的地方。

假設一次完整流程是:

建立工單
↓
扣料
↓
設備報工
↓
SAP 入庫
↓
發通知

整個流程可能有一個:

CorrelationId

用來 Trace:

這些 Request 都屬於同一條流程

但是其中的 SAP 入庫 Command 可以另外有:

TransactionId

例如:

CorrelationId
CORR-001
 
├── DeductMaterial
│   TransactionId = AAA
│
├── MachineReport
│   TransactionId = BBB
│
└── SapInbound
    TransactionId = CCC

可以把兩者理解成:

Correlation ID
= 這些操作是不是同一條流程?
 
Transaction ID / Idempotency Key
= 這是不是同一個業務命令?

不要把兩個用途混在一起。


GUID 也不是樂觀併發控制

這一點也要特別分清楚。

GUID / Transaction ID 處理的是:

是不是同一個 Command?

例如:

ABC123
ABC123

SAP 可以知道:

這是 Retry

但如果:

ABC123 → 庫存 +5
 
DEF456 → 庫存 +3

兩個 GUID 不同,而且兩筆都是合法操作。

如果它們同時修改同一筆 Inventory,仍然可能發生:

Lost Update

這就需要:

Version
RowVersion
ExpectedVersion

等 Optimistic Concurrency Control。

因此:

GUID / TransactionId
        ↓
Idempotency
 
Version / RowVersion
        ↓
Optimistic Concurrency

是兩個不同問題。


.NET 中怎麼產生 GUID?

最常見:

var transactionId = Guid.NewGuid();

Guid.NewGuid() 產生的是 UUID Version 4 類型的 GUID;Microsoft 文件指出它包含 122 bits 的強隨機熵。

例如:

public sealed record SapInboundCommand(
    Guid TransactionId,
    string WorkOrderNo,
    decimal Quantity);

建立新的業務操作:

var command = new SapInboundCommand(
    Guid.NewGuid(),
    workOrderNo,
    quantity);

第一次:

await SendToSapAsync(command);

Timeout 後:

await SendToSapAsync(command);

注意這裡 Retry 的仍然是:

command

而不是:

new SapInboundCommand(
    Guid.NewGuid(),
    workOrderNo,
    quantity);

這個差異非常重要。


SQL Server 怎麼存?

建議直接使用:

uniqueidentifier

而不是:

varchar(36)

例如:

TransactionId UNIQUEIDENTIFIER NOT NULL

SQL Server 原生 uniqueidentifier 就是為 GUID 類型準備的 16-byte Data Type。


GUID 不一定適合當 Clustered Primary Key

這是 SQL Server 很重要的一個實務問題。

如果使用隨機 UUID v4:

3f...
91...
12...
e7...
44...

新的值不是按照大小順序產生。

如果它直接是:

PRIMARY KEY CLUSTERED

大量 Insert 時可能帶來:

Page Split
Index Fragmentation
額外 IO

所以對你這個場景,我比較推薦:

Id BIGINT IDENTITY(1,1)
    PRIMARY KEY CLUSTERED,
 
TransactionId UNIQUEIDENTIFIER
    NOT NULL
    UNIQUE

也就是:

BIGINT Identity
↓
Database Physical Key
 
GUID
↓
Distributed Business Identifier

兩個各做自己擅長的事。

不需要強迫 GUID 同時扮演所有角色。


UUID v4 和 UUID v7

目前 UUID 標準 RFC 9562 除了傳統 UUID v4,也定義了 UUID v7。

UUID v7 將 Unix timestamp 放在高位,因此具有時間排序特性;RFC 9562 建議在適合的情況下優先考慮 v7,而非較舊的時間型 v1 / v6。

新版 .NET 已提供:

Guid.CreateVersion7();

Microsoft 文件指出 .NET 9+ 提供此 API。

所以未來如果你的需求是:

GUID
+
大致依產生時間排序
+
改善索引寫入特性

可以評估 UUID v7。

但如果你現在只是:

MES → SAP Idempotency Key

而且 TransactionId 不是 Clustered Primary Key,

那:

Guid.NewGuid()

已經非常足夠,而且簡單清楚。


GUID 不是密碼

另外一個很重要的安全觀念:

GUID ≠ Secret

即使 GUID 看起來像:

5f77e36d-1b64-4a5c-a16b-f57567086375

很難猜,

也不能因此設計成:

只要知道 GUID
就有權限存取資料

GUID 是:

Identifier

不是:

Authentication Token
Authorization Token
Password
API Key

權限驗證仍然必須另外處理。

Microsoft 也明確指出 Guid.NewGuid() 不應作為需要密碼學 PRF 的用途。


所以為什麼分散式系統喜歡 GUID?

最核心的原因其實不是:

因為 GUID 很長

而是:

每一個節點都可以自己產生

而且不需要:

問 Database
問中央 Server
取得 Distributed Lock
確認其他 Server 的 Counter

可以直接:

MES A → Guid.NewGuid()
 
MES B → Guid.NewGuid()
 
MES C → Guid.NewGuid()

然後安全地把這個 Identifier 帶到:

API
Message Queue
Database
SAP
Microservice
Log
Event

這使 GUID 特別適合:

Transaction ID
Idempotency Key
Event ID
Message ID
Aggregate ID
Correlation ID
Distributed Entity ID

回到 MES → SAP

所以你的設計可以變成:

Mermaid · 圖表原始碼
sequenceDiagram
    participant MES
    participant SAP
    participant DB
 
    MES->>MES: Guid.NewGuid()
    Note over MES: TransactionId = ABC123
 
    MES->>SAP: Inbound + ABC123
 
    SAP->>DB: INSERT ABC123
 
    alt 第一次收到
        DB-->>SAP: Success
        SAP->>DB: 執行業務資料更新
        SAP->>DB: 保存處理結果
        SAP-->>MES: Success
    else 已經存在
        DB-->>SAP: Unique Conflict
        SAP->>DB: 讀取 ABC123 處理結果
        SAP-->>MES: 回傳原結果
    end

如果 Response 遺失:

Retry ABC123

如果 User 真的再做一次入庫:

New Operation
↓
Guid.NewGuid()
↓
DEF456

所以最後規則非常簡單:

同一次 Business Command
= 同一個 GUID
 
新的 Business Command
= 新的 GUID

最後整理

GUID 在分散式系統真正解決的問題是:

如何讓不同 Machine
不同 Process
不同 Service
在不經過中央協調的情況下
產生幾乎不會衝突的 Global Identifier

對 MES → SAP 而言:

Guid
↓
識別一次業務操作
 
Same Guid
↓
Same Business Command / Retry
 
New Guid
↓
New Business Command

再搭配:

GUID
+
Unique Constraint
+
Database Transaction
+
Processing Status
+
Result

才能形成完整的 Idempotency 機制。

可以把整個概念記成:

GUID 解決「我是誰」
 
Idempotency 解決「我有沒有做過」
 
Unique Constraint 解決「同一個我不能出現兩次」
 
Transaction 解決「業務資料與冪等紀錄必須一起成功或一起失敗」
 
Version / RowVersion 解決「我修改資料時,資料是不是已經被別人改過」

這幾個概念各自負責不同問題,不應該混為一談。

Subscribe to new articles訂閱新文章