在微服務架構中,每個微服務都是一個獨立的單元,擁有自己的資料庫和交易管理。由於微服務之間可能需要進行交互操作,因此確保每個微服務的交易安全和資源操作的安全性是非常重要的。就算是服務於單一目標的小型系統在交易上也不能馬虎,確保自身的資料一致性和完整性才能更加地支持分散式系統的資料一致性。
每個微服務都應關注自身的交易安全,確保在存取資料庫時交易能夠正確且一致地完成。例如,一個處理訂單的微服務應確保在創建新訂單時,所有相關的資料庫操作(如更新庫存、計算價格等)都在同一個交易中完成。
舉個在 Production Service 的例子,你可以關注在專案中的這支程式:
{production_service}/app/app/Models/v1/ProductionModel.php
/**
* 新增商品與庫存的 transcation
*
* @param string $name
* @param string $description
* @param integer $price
* @param integer $amount
* @return int|null 新增的商品 id, 失敗回傳 null
*/
public function createProductionTranscation(string $name, string $description, int $price, int $amount):?int
{
$productionData = [
"name" => $name,
"description" => $description,
"price" => $price,
"created_at" => date("Y-m-d H:i:s") ,
"updated_at" => date("Y-m-d H:i:s")
];
try{
$this->db->transStart();
$this->db->table("production")
->insert($productionData);
$productionInsertId = $this->db->insertID();
$inventory = [
"p_key" => $productionInsertId,
"amount" => $amount,
"created_at" => date("Y-m-d H:i:s"),
"updated_at" => date("Y-m-d H:i:s")
];
$this->db->table("inventory")
->insert($inventory);
$result = $this->db->transComplete();
if ($result === false) {
return null;
}
return $productionInsertId;
}catch(\Exception $e){
log_message('error', '[ERROR] {exception}', ['exception' => $e]);
return false;
}
}
上述方法 createProductionTranscation
是一個涉及兩張資料表建立資源的過程,它能夠將新的商品相關資訊(名稱、描述、價格,和庫存量)儲存到資料庫中。這個方法使用了 CodeIgniter 的交易以保障資料的完整性和一致性。
透過這個流程,保證了 insert 商品和庫存資料時的交易完整性,即如果任何一個 insert 失敗,整個交易將被還原,以防止資料不一致。
樂觀鎖定(Optimistic Locking)是一種在資料庫或系統設計中常用的技巧,旨在高並行處理的環境中最小化資料鎖定的使用,以提高系統的效能。相對於悲觀鎖定(Pessimistic Locking),樂觀鎖定假設在大多數情況下,資源不會出現同時競爭的情況,因此不會在操作開始時就加入鎖定,而是在實際進行寫入操作時,檢查資料是否被修改過或是不符合業務邏輯。
假設在一個購物系統中,有多個使用者同時嘗試購買同一件商品,每個使用者的購買操作都需要檢查庫存並更新庫存量。使用樂觀鎖定的方式,購買操作都會先讀取商品的庫存量和版本號碼,然後在購買時檢查版本號碼是否仍然是最初讀取的版本,如果是,則更新庫存量並增加版本號,如果不是,則通知庫存已被更新,需要重新嘗試購買或是系統流程自動重試。
這種方式最大的好處是能夠在高並行的環境中保持系統的效能,但缺點是可能會增加因衝突而導致的交易還原和重試,這需要根據具體的系統和場景來進行評估和選擇。
另一種方式是在執行更新動作時檢查某些欄位的數值是否符合業務邏輯,你可以關注 Production Service 中的 app/Models/v1/InventoryModel.php
的 reduceInventoryTranscation()
。你可以關注方法中關於交易的第二個步驟:對 inventory
的寫入,在這裡採用了條件更新的策略:
$this->db->table("inventory")
->where("p_key", $p_key)
->where("amount >=", $reduceAmount)
->update($inventory);
在這個更新操作中,除了檢查商品的 p_key
外,還檢查目前的庫存量 amount
是否大於或等於要減少的量 $reduceAmount
。這是一個典型的樂觀鎖定策略,只有當庫存量滿足條件時,才會執行實際的更新操作。
在分散式系統和微服務架構中,冪等(Idempotence)是一個重要的概念。若是一個操作或一個介面被稱之為冪等的,意思就是無論執行一次或是多次結果都是相同的。在微服務架構中,確保服務呼叫的冪等性是非常重要的,它可以幫助防止在補償或是服務重試時發生的重複處理錯誤。
INSERT ON DUPLICATE KEY UPDATE
語法,或在設計存儲過程檢查欲建立的項目是否已存在。在 Anser-Orchestration 中,我們會建立起 UUID 並在每一個敏感操作中傳入。而這個 UUID 便是唯一的交易識別碼也是 Order 的主鍵,以下是 Order Service app/Controllers/v1/OrderController.php
的程式碼片段:
$orderEntity = OrderBusinessLogic::getOrder($o_key);
if($orderEntity){
return $this->respond([
"msg" => "OK",
"total" => (int)$orderEntity->ext_price
]);
}
這個API透過訂單的key ($o_key) 來獲取訂單資訊,如果訂單存在,它將返回訂單的總價。每次呼叫此API,只要訂單的資料沒有變化,都會得到相同的結果。這裡的冪等性主要體現在讀取操作上,它不會改變系統的狀態,每次呼叫都會返回相同的結果。這種冪等性對於構建可靠和可預測的系統特別重要,它可以幫助 Client 在網路不穩定或其他失敗情況下重試請求,而不會對系統造成負面影響。
你可以再看看以下範例 app/Models/v1/OrderModel.php
:
/**
* 刪除訂單與訂單商品 transcation
*
* @param integer $orderKey
* @return bool
*/
public function deleteOrderTranscation(string $orderKey):bool
{
try {
$this->db->transStart();
$time = [
"deleted_at" => date("Y-m-d H:i:s")
];
$this->db->table("order")
->where("o_key",$orderKey)
->update($time);
$this->db->table("order_product")
->where("o_key", $orderKey)
->update($time);
$result = $this->db->transComplete();
} catch (\Exception $e) {
log_message('error', '[ERROR] {exception}', ['exception' => $e]);
return false;
}
return $result;
}
上述函式透過更新資料庫中的 deleted_at
欄位來表示一個訂單和訂單相關的商品已被刪除。這是一個「軟性刪除」(soft delete)策略,我們將保留資料庫中的資料,而不是永久刪除它們。
冪等性在這個情境下是指:無論呼叫 deleteOrderTransaction()
函式多少次,結果都是相同的,即訂單和訂單相關的商品被標記為已刪除。若是第一次呼叫,deleted_at
欄位會被更新;若是後續呼叫,deleted_at
欄位也只會被更新成最新的時間,在邏輯上「訂單已被刪除」的這個事實不會有任何改變。
雖然我們已經透過許多方式保障了微服務在承受並行請求時的資料安全性,但在複雜的分散式交易中還是可能有所疏漏。因此,在十分重要的資源改變中留下修改記錄是一件非常重要的事情。
藉由記錄每個操作和交易,我能將能追溯系統中的所有敏感寫入與更新。當系統出現問題或資料狀態不符合預期時,這類記錄能提供關鍵的資訊來幫助理解問題的根本原因。透過檢查操作記錄,開發人員和系統管理員能夠找出問題發生的時間、影響的範圍,以及可能的解決方案。
你可以參考 User Service 的 app/Models/v1/WalletModel.php
了解大概的撰寫方式:
/**
* 使用者錢包扣款
*
* @param integer $u_key
* @param string $o_key
* @param string $type
* @param integer $nowAmount
* @param integer $total
* @return boolean
*/
public function chargeTransaction(int $u_key, string $o_key, string $type, int $nowAmount, int $total): bool
{
$history = [
"u_key" => $u_key,
"type" => $type,
"amount" => $total,
"o_key" => $o_key,
"created_at" => date("Y-m-d H:i:s"),
"updated_at" => date("Y-m-d H:i:s")
];
try {
$this->db->transBegin();
$this->db->table("history")
->insert($history);
$wallet = [
"balance" => $nowAmount - $total,
"updated_at" => date("Y-m-d H:i:s")
];
$this->db->table("wallet")
->where("u_key", $u_key)
->where("balance >=", $total)
->update($wallet);
if ($this->db->transStatus() === false || $this->db->affectedRows() == 0) {
$this->db->transRollback();
return false;
} else {
$this->db->transCommit();
return true;
}
} catch (\Exception $e) {
log_message('error', '[ERROR] {exception}', ['exception' => $e]);
return false;
}
return true;
}
在 chargeTransaction
這個函式中,除了在使用者的錢包中扣款外,還在 history 表中留下交易記錄:
insert()
函式將 $history
陣列插入到 history
表中,以留下這次交易的記錄。update()
函式更新了 wallet
表中的資料。使用額外的檢查,保證使用者的餘額足夠進行這次扣款。在資料庫中,這些 history
的資料會是這個樣子:
透過以上的策略和技巧,我們可以確保在單個微服務中以相對安全的方式處理自身的交易以及安全地操作資源。微服務架構以高度解耦與擴充彈性的優勢,成為了現代軟體開發的主流架構之一。但與此同時,由於每個微服務都是獨立執行與獨立存儲的,交易一致性和資源安全的問題也隨之凸顯。所以,適當的技巧和資料處理策略是保障微服務交易和資源操作安全的關鍵。