在前面的 MES → SAP 入庫案例中,我們使用了 transactionId 來識別一次業務操作。
例如:
transactionId = 5f77e36d-1b64-4a5c-a16b-f57567086375在 .NET 中通常可以直接產生:
Guid transactionId = Guid.NewGuid();問題是:
為什麼要用 GUID?
用流水號1、2、3、4...不行嗎?
要理解這件事,就要先理解 分散式系統最大的問題之一:沒有一個所有節點都能隨時依賴的中央發號機制。
先從單機系統開始
假設今天所有 Request 都只會進入同一台 Server:
Scroll sideways · 可左右捲動查看完整圖表
Mermaid · 圖表原始碼
flowchart LR
Client --> Server
Server --> DB我們可能很自然地使用:
1
2
3
4
5例如 SQL Server:
Id BIGINT IDENTITY(1,1)因為所有資料最後都進到同一個 Database,由 Database 負責:
下一號是多少?這種方式非常簡單。
但分散式系統不一樣
系統規模變大後,可能變成:
Scroll sideways · 可左右捲動查看完整圖表
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 → 101Transaction ID 就撞號了。
你當然可以建立一個中央服務:
Scroll sideways · 可左右捲動查看完整圖表
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。
因此:
Scroll sideways · 可左右捲動查看完整圖表
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 則是:
uniqueidentifierSQL 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然後:
Scroll sideways · 可左右捲動查看完整圖表
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
假設:
Scroll sideways · 可左右捲動查看完整圖表
Mermaid · 圖表原始碼
sequenceDiagram
participant MES
participant SAP
MES->>SAP: ABC123
SAP->>SAP: 入庫成功
SAP--xMES: Response TimeoutMES 不知道 SAP 到底成功還是失敗。
所以 MES Retry。
正確:
第一次
ABC123
Retry
ABC123
Retry
ABC123錯誤:
第一次
ABC123
Retry
DEF456
Retry
GHI789如果每 Retry 一次都重新:
Guid.NewGuid();SAP 看到的會是:
ABC123 → 新操作
DEF456 → 新操作
GHI789 → 新操作SAP 根本不知道它們其實是同一次業務操作。
因此:
GUID 必須在第一次業務操作開始時產生,之後所有 Retry 都沿用同一個 GUID。
正確的生命週期
應該是:
Scroll sideways · 可左右捲動查看完整圖表
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 CGUID 和冪等性的關係
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:
Scroll sideways · 可左右捲動查看完整圖表
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 ConflictDatabase 最後負責保證:
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 裡的價值。
一個很典型的分散式流程
Scroll sideways · 可左右捲動查看完整圖表
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
ABC123SAP 可以知道:
這是 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 NULLSQL 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
所以你的設計可以變成:
Scroll sideways · 可左右捲動查看完整圖表
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 解決「我修改資料時,資料是不是已經被別人改過」這幾個概念各自負責不同問題,不應該混為一談。