分享程式代碼相關筆記
目前文章總數:172 篇
最後更新:2025年 03月 22日
分散式資料庫是一個邏輯上統一但實際上分散在不同地理位置的資料庫系統。
它將資料分散儲存在多個實體位置的資料庫中,這些資料庫透過網路連接並協同工作,
對使用者來說就像是在使用單一資料庫一樣。
有以下特點:
1. 資料分散性 | 資料實際上分散儲存在不同的實體位置,但在邏輯上是一個整體 |
2. 資料獨立性 | 每個節點可以獨立運作,擁有自己的資料管理系統 |
3. 持續可用性 | 某個節點故障,系統仍可繼續運作,提高了系統的可靠性 |
示意:
常見的使用情境如下:
1. 跨國企業的系統 | 可以讓不同地區的分公司存取所需資料 |
2. 大型電商平台 | 需要在全球不同地區提供快速的資料存取服務 |
3. 社群媒體平台 | 需要處理大量的使用者資料並確保服務的可用性 |
但是資料庫分散存放會有以下問題要克服:
1. 資料一致性 |
2. 同步機制 |
3. 網路延遲 |
本篇介紹 XA 協議,解決分散式資料庫 - 克服上述 3 個困難點
分散式資料庫通常從以下幾個主要維度進行觀察和分析,每個分散性資料庫架構都會依照這 4 個核心維度做評估考量
1. 一致性協議角度 | 資料同步 & 速度 取捨方式; 資料一致性的層級,不可能同時存在 |
2. CAP理論角度 | 管理資料方式 ; 一致性(Consistency)、可用性(Availability)、分區容錯性(Partition tolerance),3 取 2 做取捨 |
3. 數據分布角度 | 保存資料方式 ; 物理分布策略下,如何儲存、分割、複製和管理資料 |
4. 事務處理角度 | 實現方式 ; 如何確保分散式環境下的事務完整性和一致性 |
這個角度主要關注的是「如何」實現資料一致性,以及在速度和一致性之間的取捨機制
1. 強一致性 (Paxos/Raft):
Paxos | 由 Leslie Lamport 提出的分散式一致性算法 |
確保所有節點在某個值上達成一致 | |
分為 Basic Paxos 和 Multi Paxos | |
實現較為複雜,但提供強大的一致性保證 | |
Raft | 為了解決 Paxos 實現複雜的問題而設計 |
將一致性問題分解為:領導人選舉、日誌複製、安全性 | |
更容易理解和實現 | |
被廣泛應用於如 etcd 等系統 |
2. 最終一致性 (Gossip):
Gossip Protocol | 模仿病毒傳播的方式傳遞資訊 |
節點間隨機選擇對象交換資訊 | |
最終所有節點都會收到資訊 | |
適用於大規模分散式系統 | |
補充特點 | 容忍網路延遲和分區 |
最終達到一致,但不保證即時一致 | |
系統可用性高 |
3. 交易一致性 (XA/TCC):
XA | 是一個分散式事務處理標準 |
使用兩階段提交協議(2PC) | |
適用於強一致性需求場景 | |
TCC (Try-Confirm-Cancel) | 將事務分為 Try、Confirm、Cancel 三個階段 |
更靈活的事務處理方式 | |
適用於微服務架構 |
這個理論影響了許多設計決策,例如在特定場景下選擇犧牲強一致性來換取更高的可用性
1. CP 型(一致性 + 分區容錯):
在網路分區時保證資料一致性 |
可能暫時犧牲可用性 |
適用於銀行等金融系統 |
EX: HBase、MongoDB (在主節點故障時) |
2. AP 型(可用性 + 分區容錯) :
保證系統持續可用 |
容忍資料暫時不一致 |
適用於社交媒體等場景 |
EX: Cassandra、DynamoDB |
2. CA 型(較少使用):
在單機或區域網路中使用 |
確保所有節點在某個值上達成一致 |
不考慮網路分區問題 |
實際應用較少 |
EX: 傳統關聯式資料庫的主從架構 |
分散式資料庫的核心元素、如何儲存、分割、複製和管理資料,同時也關係到資料的完整性、安全性和效能優化
1. 分片 (Sharding):
水平分片: | 依據key值將資料分散到不同節點 |
提高系統容量和處理能力 | |
垂直分片: | 依據業務將不同表分散到不同節點 |
提高特定業務的處理能力 |
2. 複製 (Replication):
主從複製: | 一個主節點多個從節點 |
提高讀取性能和可用性 | |
多主複製: | 多個主節點可寫入 |
提高寫入性能但增加複雜度 |
3. 混合 (Hybrid):
結合分片和複製的優點 |
根據業務需求靈活配置 |
平衡性能和可用性 |
這主要對應到「事務處理機制」和「一致性模型研究」中的具體實現方式
1. ACID(完整事務特性):
Atomicity(原子性):事務要麼全部完成,要麼全部失敗 |
Consistency(一致性):事務執行前後數據庫狀態一致 |
Isolation(隔離性):事務執行互不干擾 |
Durability(持久性):事務完成後永久生效 |
2. BASE(最終一致性) :
Basically Available(基本可用): 保證系統大部分時間可用 |
Soft State(軟狀態): 允許系統存在中間狀態 |
Eventually Consistent(最終一致性):數據最終達到一致狀態 |
3. 混合:
依據業務重要性選擇不同事務模型 |
重要交易使用 ACID |
一般操作使用 BASE |
在性能和一致性間取得平衡 |
XA 主要聚焦於一致性協議和事務處理這兩個維度,它是一種特定的分散式事務協議,用於確保多個獨立資源之間的事務一致性。
數據分布角度中,只要求符合強一致性處理,並不限制使用哪種保存方式。
Transaction Manager (事務管理器,TM) | Resource Manager (資源管理器,RM) | Application Program (應用程序) |
負責協調整個分散式事務 | 直接管理資源(如資料庫、消息佇列等) | 啟動事務 |
決定事務的提交或回滾 | 執行事務的實際操作 | 定義事務邊界 |
與所有資源管理器進行通訊 | 響應事務管理器的指令 | 調用資源管理器進行操作 |
以下為 XA 工作流程中,每個協議架構具體工作的內容:
流程 | 事務所屬 | 大致內容 |
1. 事務開始階段 | AP + TM | 由 AP 發起,並由 TM 產生全局唯一事務ID (XA ID) |
2. 事務執行階段 | 全部 | AP 呼叫 RM 工作 ; TM 全局觀察,做 AP, RM 的仲裁者 ; RM 處理事務內容 |
3. 第一階段(準備階段) | TM + RM | TM 告知 RM 進行準備 ; RM 將狀況回報 TM |
4. 第二階段(決策階段) | TM + RM | RM 回報所有內容 ; TM 做最終仲裁,決定提交、回滾 |
5. 事務結束階段 | AP + TM | TM 回報 AP 結果 ; AP 進行結算工作(EX: 結束程式、AP Log) |
3. 4. 項內容就是二階段提交(2PC)的核心思想:
準備階段:詢問所有參與者是否可以執行某個動作 |
決策階段:根據所有參與者的回應,統一決定「全部執行」或「全部不執行」 |
MySQL 主要負責 Resource Manager (RM)
流程 | 事務所屬 | 大致內容 |
1. 事務開始階段 | AP + TM | 由 AP 發起,並由 TM 產生全局唯一事務ID (XA ID) |
2. 事務執行階段 | 全部 | AP 呼叫 RM 工作 ; TM 全局觀察,做 AP, RM 的仲裁者 ; Mysql 處理事務內容(產生由 TM 發起的唯一ID) |
3. 第一階段(準備階段) | TM + RM | TM 告知 Mysql 進行準備 ; Mysql 將資料庫連線資訊回報 TM |
4. 第二階段(決策階段) | TM + RM | MySQL 回報執行提交的結果,每個庫是否都正常 ; TM 做最終仲裁,決定提交、回滾 |
5. 事務結束階段 | AP + TM | TM 回報 AP 結果 ; AP 進行結算工作(EX: 結束程式、AP Log) |
MySQL 會響應 XA 命令(XA START, XA END, XA PREPARE, XA COMMIT, XA ROLLBACK)
二階段提交確保了所有資料庫要麼都提交變更,要麼都不提交,避免了系統處於不一致的狀態。
在資料庫中,這種機制可以防止一部分資料庫更新成功而另一部分失敗的情況,保持了整個系統的一致性。
延伸的優點如下:
優點結論:資料絕對完整,同步不遺失
1. 強一致性:確保多個資料庫中的交易要麼全部提交,要麼全部回滾,維持資料一致性 |
2. 標準化:XA 是一個被廣泛採用的工業標準,在不同資料庫系統間有良好的兼容性 |
3. 資料完整性:嚴格遵循 ACID 特性,特別適合對資料完整性要求高的場景 |
4. 不需重寫應用邏輯:使用現有的協議框架,不需要在應用層實現複雜的補償邏輯 |
5. 透明性:對應用程式透明,可以像處理本地事務一樣處理分散式事務 |
並遺毒的缺點如下:
缺點結論:效能差
1. 效能問題:涉及多次網路通訊和資源鎖定,導致系統吞吐量降低 |
2. 可用性風險:任何參與者故障都可能導致整個事務阻塞 |
3. 資源鎖定時間長:準備階段後資源仍保持鎖定,造成資源長時間不可用 |
4. 伸縮性限制:在大規模分散式系統中可能成為性能瓶頸 |
5. 難以處理長時間執行的事務:對於需要長時間執行的業務邏輯不適用 |
6. 對網路分區敏感:在網路不穩定的環境下容易失敗 |
XA 協議具有 資料絕對完整,同步不遺失、效能差 主要特性
因此適合的應用情境如下:
1. 金融交易:
銀行轉帳 | 需要同時更新兩個或多個帳戶 |
證券交易 | 涉及多個資產和賬戶的變更 |
支付系統 | 需要多步驟交易的一致性 |
2. 庫存與訂單系統:
電商平台 | 涉及庫存扣減、訂單創建、支付記錄等多個操作 |
倉儲管理 | 確保庫存和訂單資料的一致性 |
3. 企業資源規劃(ERP)系統:
內部系統 | 需要在多個子系統間保持資料一致性 |
業務流程涉及多個資料庫或資料來源 |
4. 小型至中型分散式系統:
中、小型專案 | 服務數量適中(通常少於10個) |
中、小型產品 | 事務執行時間短 |
網路環境穩定 |
XA 協議具有 資料絕對完整,同步不遺失、效能差 主要特性
因此不建議的應用情境如下:
1. 高並發系統:
社交媒體平台 |
實時數據處理系統 |
需要高吞吐量的應用 |
2. 大規模微服務架構:
服務數量龐大且高度分散 |
使用異構資料庫的環境 |
3. 長時間執行的業務流程:
工作流系統 |
需要人工介入的業務流程 |
4. 可用性要求極高的系統:
不能容忍短暫服務中斷的關鍵系統 |
高可用性優先於強一致性的場景 |
將範例代碼下載後,將代碼根目錄的 .sql 與 docker-compose.yml 放進 Ubuntu 的目錄下,如圖:
輸入以下指令,進行安裝
docker-compose up -d
此 dokcer-compose.yml 安裝腳本,安裝了以下內容:
1. 安裝 2 個 Mysql 資料庫,並且容器化啟動,密碼都為 password |
2. 容器 A 對宿主機 Port 為 3306 ,容器 B 對宿主機 Port 為 3307 |
3. 容器 A 安裝 Mysql 資料庫後,預設執行 init-bank-a.sql 語法 |
4. 容器 B 安裝 Mysql 資料庫後,預設執行 init-bank-b.sql 語法 |
version: '3.8'
services:
mysql-bank-a:
image: mysql:8.0
container_name: mysql-bank-a
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: Bank_A
ports:
- "3306:3306"
volumes:
- ./mysql-bank-a-data:/var/lib/mysql
- ./init-bank-a.sql:/docker-entrypoint-initdb.d/init.sql
command: --default-authentication-plugin=mysql_native_password
--innodb_lock_wait_timeout=120
--max_connections=1000
networks:
xa-network:
ipv4_address: 172.20.0.2
mysql-bank-b:
image: mysql:8.0
container_name: mysql-bank-b
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: Bank_B
ports:
- "3307:3306"
volumes:
- ./mysql-bank-b-data:/var/lib/mysql
- ./init-bank-b.sql:/docker-entrypoint-initdb.d/init.sql
command: --default-authentication-plugin=mysql_native_password
--innodb_lock_wait_timeout=120
--max_connections=1000
networks:
xa-network:
ipv4_address: 172.20.0.3
networks:
xa-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
目前我們環境架構如下,想像我們是在一家銀行內,由 A 分行內轉到 B 分行,通常情況下金錢的流動上,都是同一個用戶,因此必須強一致性 :
1. 兩個資料庫已經啟動 & 我們目標會啟動代碼驅使兩個資料庫完成整個 XA 協議:
2. 並且 2 個資料庫有以下的資料結構
範例代碼下載後,啟動專案
啟動後會出現以下兩個按鈕
當點擊 不使用 XA 按鈕時,會呼叫以下 API
/// <summary>
/// 1. [不使用] XA 的 API
/// </summary>
[HttpGet]
public async Task<ActionResult<TransferResult>> TransferWithoutXA()
{
// Step 1. 準備資料, A庫 寫進 B庫
var request = new TransferRequest()
{
Amount = 10000,
FromAccount = "A_Louis",
ToAccount = "B_Louis",
};
// Step 2. 進行單庫寫入 (不使用XA)
var result = await _transferService.TransferWithoutXAAsync(request);
if (!result.Success)
return BadRequest(result);
return Ok(result);
}
如果不使用 XA 的情況下,我們多個分行的資料庫會有可能在以下 危險點1 ~ 3 造成問題
/// <summary>
/// 一、不使用 XA 的轉賬方法 - 可能出現的問題:
/// 1. 當 Bank_A 扣款成功,但 Bank_B 加款失敗時,資金會丟失
/// 2. 系統崩潰或網絡中斷時,無法確保數據一致性
/// 3. 無法進行有效的回滾操作
/// </summary>
public async Task<TransferResult> TransferWithoutXAAsync(TransferRequest request)
{
try
{
// Step 1-1 : 從 Bank_A 扣款
using (var connA = new MySqlConnection(_connectionStringBankA))
{
await connA.OpenAsync();
using (var cmd = connA.CreateCommand())
{
var sql = $@"
UPDATE accounts
SET balance = balance - @amount
WHERE account_number = @account
";
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@amount", request.Amount);
cmd.Parameters.AddWithValue("@account", request.FromAccount);
await cmd.ExecuteNonQueryAsync();
}
}
// 危險點1:如果在這裡系統崩潰或者網絡中斷
// Bank_A 的錢已經扣除,但 Bank_B 還沒收到
// 這時資金就會丟失,而且無法自動恢復
// Step 1-2 : 在 Bank_B 增加餘額
using (var connB = new MySqlConnection(_connectionStringBankB))
{
await connB.OpenAsync();
using (var cmd = connB.CreateCommand())
{
var sql = $@"
UPDATE accounts
SET balance = balance + @amount
WHERE account_number = @account
";
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@amount", request.Amount);
cmd.Parameters.AddWithValue("@account", request.ToAccount);
await cmd.ExecuteNonQueryAsync();
}
}
// 危險點2:如果在這裡出錯
// 雖然兩個操作都完成了,但我們無法確認是否都成功
// 可能會出現數據不一致的情況
return new TransferResult
{
Success = true,
Message = "Transfer completed",
TransactionId = "NO_XA_" + Guid.NewGuid().ToString()
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Transfer failed without XA");
// Step 2: 回滾階段
// 危險點3:即使捕獲到異常,我們也無法確保能夠正確地回滾之前的操作
// 可能會導致數據不一致
return new TransferResult
{
Success = false,
Message = $"Transfer failed: {ex.Message}",
TransactionId = "NO_XA_" + Guid.NewGuid().ToString()
};
}
}
以危險點1 來看,當扣款成功後,但是這時程式 Crash 或者網路異常
就有可能造成以下資料流的現象,不一致的問題發生。
當點擊 使用 XA 按鈕時,會呼叫以下 API
/// <summary>
/// 2. [使用] XA 的 API
/// </summary>
[HttpGet]
public async Task<ActionResult<TransferResult>> TransferWithXA()
{
// Step1. 準備資料, A庫 寫進 B庫
var request = new TransferRequest()
{
Amount = 10000,
FromAccount = "A_Louis",
ToAccount = "B_Louis",
};
// Step 2. 進行雙庫寫入 (使用XA)
var result = await _transferService.TransferWithXAAsync(request);
if (!result.Success)
return BadRequest(result);
return Ok(result);
}
遵循著 二階段 (2PC) ,我們依序完成 AP, TM 的工作,並且由 MySQL 實現 RM 的腳色
/// <summary>
/// 二、使用 XA 的轉賬方法 - 解決的問題:
/// 1. 確保轉賬的原子性:要麼全部成功,要麼全部失敗
/// 2. 支持分布式事務回滾:出錯時可以安全回滾
/// 3. 保證跨庫數據一致性:兩個銀行的數據始終保持一致
/// </summary>
public async Task<TransferResult> TransferWithXAAsync(TransferRequest request)
{
// Step 1. 產生本次 XA 的唯一碼
string xaId = Guid.NewGuid().ToString();
MySqlConnection connA = null;
MySqlConnection connB = null;
try
{
_logger.LogInformation($"Starting XA transaction {xaId}");
// Step 2-1. 同時打開兩個連接
connA = new MySqlConnection(_connectionStringBankA);
connB = new MySqlConnection(_connectionStringBankB);
await connA.OpenAsync();
await connB.OpenAsync();
// Step 2-2:開始 XA 事務
// 說明:這確保了後續的所有操作都在一個分布式事務中
await ExecuteXaCommandAsync(connA, $"XA START '{xaId}'");
await ExecuteXaCommandAsync(connB, $"XA START '{xaId}'");
// Step 2-3:行鎖
// 說明:將此帳號鎖定,避免同時間被異動
await ForUpdateAccount(connA, request.FromAccount);
await ForUpdateAccount(connB, request.ToAccount);
// Step 2-4:執行轉賬操作(業務邏輯)
// 說明:這些操作要麼全部成功,要麼全部失敗
await ExecuteTransferAsync(connA, request.FromAccount, -request.Amount);//A 扣
await ExecuteTransferAsync(connB, request.ToAccount, request.Amount);//B 增加
// Step 2-5:準備階段
// 說明:確保所有參與者都準備好提交
await ExecuteXaCommandAsync(connA, $"XA END '{xaId}'");
await ExecuteXaCommandAsync(connB, $"XA END '{xaId}'");
await ExecuteXaCommandAsync(connA, $"XA PREPARE '{xaId}'");
await ExecuteXaCommandAsync(connB, $"XA PREPARE '{xaId}'");
// Step 2-6:提交階段
// 說明:只有當所有準備工作都成功後才提交
await ExecuteXaCommandAsync(connA, $"XA COMMIT '{xaId}'");
await ExecuteXaCommandAsync(connB, $"XA COMMIT '{xaId}'");
_logger.LogInformation($"XA transaction completed successfully: {xaId}");
return new TransferResult
{
Success = true,
Message = "Transfer completed successfully with XA",
TransactionId = xaId
};
}
catch (Exception ex)
{
_logger.LogError(ex, $"XA transaction failed: {xaId}");
try
{
// Step 3:回滾階段
// XA 確保了即使在錯誤情況下也能正確回滾
if (connA?.State == System.Data.ConnectionState.Open)
await ExecuteXaCommandAsync(connA, $"XA ROLLBACK '{xaId}'");
if (connB?.State == System.Data.ConnectionState.Open)
await ExecuteXaCommandAsync(connB, $"XA ROLLBACK '{xaId}'");
}
catch (Exception rollbackEx)
{
_logger.LogError(rollbackEx, $"XA rollback failed: {xaId}");
}
return new TransferResult
{
Success = false,
Message = $"Transfer failed with XA: {ex.Message}",
TransactionId = xaId
};
}
finally
{
await connA?.CloseAsync();
await connB?.CloseAsync();
}
#region [閉包] 私有方法
/// <summary>
/// 執行 XA 事務相關命令
/// </summary>
async Task ExecuteXaCommandAsync(MySqlConnection connection, string command)
{
try
{
using var cmd = connection.CreateCommand();
cmd.CommandText = command;
cmd.CommandTimeout = 30; // 設置超時時間
_logger.LogDebug($"Executing XA command: {command}");
await cmd.ExecuteNonQueryAsync();
}
catch (MySqlException ex)
{
_logger.LogError(ex, $"Error executing XA command: {command}");
throw new Exception($"XA command failed: {command}", ex);
}
}
/// <summary>
/// 鎖定行防止併發修改
/// </summary>
async Task ForUpdateAccount(MySqlConnection connection, string accountNumber)
{
try
{
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT balance
FROM accounts
WHERE account_number = @account
FOR UPDATE"; // 鎖定行防止並發修改
cmd.Parameters.AddWithValue("@account", accountNumber);
using var reader = await cmd.ExecuteReaderAsync();
if (!await reader.ReadAsync())
{
throw new Exception($"Account not found: {accountNumber}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error ForUpdateAccount for account: {accountNumber}");
throw new Exception($"ForUpdateAccount failed for account: {accountNumber}", ex);
}
}
/// <summary>
/// 執行轉賬操作
/// </summary>
async Task ExecuteTransferAsync(MySqlConnection connection, string accountNumber, decimal amount)
{
try
{
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
UPDATE accounts
SET balance = balance + @amount
WHERE account_number = @account";
cmd.Parameters.AddWithValue("@amount", amount);
cmd.Parameters.AddWithValue("@account", accountNumber);
int rowsAffected = await cmd.ExecuteNonQueryAsync();
if (rowsAffected == 0)
{
throw new Exception($"Account not found or update failed: {accountNumber}");
}
_logger.LogInformation($"Transfer completed for account {accountNumber}: {amount}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error executing transfer for account: {accountNumber}");
throw new Exception($"Transfer failed for account: {accountNumber}", ex);
}
}
#endregion
}
在此情境下,可以透過 XA 實現 ACID 達成強一致性的同步資料,讓分散式資料庫,都能達到一致性的資料同步。
分散式資料庫下,在評估實際情境下 XA 克服了以下 3 個問題:
1. 資料一致性 | 強一致性(ACID) |
2. 同步機制 | 使用 2PC (準備、決策) |
3. 網路延遲 | 必須在內部環境,不需高可用的情境下,如銀行分行內部轉帳 |