From Two-Phase Commit to Saga: Simplifying Distributed Transactions
What is a Distributed Transaction?
A distributed transaction involves multiple operations spread across different systems, which all need to either complete successfully or fail as a unit. Think of it as coordinating a team spread across different cities to work seamlessly on a single project.
Two-Phase Commit Protocol
As the name suggests, this protocol runs distributed transactions in two phases
2. Commit Phase: If all systems are ready, the coordinator asks all nodes to commit simultaneously. If any system cannot commit or returns negative, the transaction is rolled back across all systems.
Challenges with Two-Phase Commit
Performance Bottlenecks: The 2PC protocol locks resources during the commit process, waiting for all involved nodes to agree before proceeding. This locking can lead to significant delays.
Fault Tolerance Issues: If any participant in the transaction fails during the second phase (the commit phase), the entire transaction must be rolled back. This makes 2PC highly vulnerable to system failures, leading to potential data inconsistencies if not managed correctly.
Why Move to Saga?
Independent Operations: Unlike 2PC, Saga allows each transaction step to be handled independently, with each service managing its own local transactions. This significantly reduces the need for global locks and improves system performance.
Compensation Logic: Saga introduces a method to handle failures through compensating transactions. If a step fails, the Saga orchestrates a series of compensatory actions to undo the impact of prior transactions, maintaining data integrity without the need for locks.
Flexibility and Resilience: By decoupling transaction steps, Saga enhances system resilience and adaptability. Each service can recover from failures autonomously, making the system more robust against individual service downtimes.
Let's write some code now.
We'll explore an example of an order processing system designed with the Saga pattern. Here's a glimpse of the orchestration class setup:
public class OrderProcessingSagaOrchestrator
{
private readonly OrderProcessingSaga _saga;
private readonly InventoryService _inventoryService;
private readonly PaymentService _paymentService;
public OrderProcessingSagaOrchestrator()
{
_saga = new OrderProcessingSaga();
_inventoryService = new InventoryService();
_paymentService = new PaymentService();
}
public async Task<SagaStepResult> ProcessOrder(Order order)
{
try
{
// Step 1: Initiate Order
var initiateOrderResult = _saga.InitiateOrderStep(order);
if (!initiateOrderResult.IsSuccess)
{
return initiateOrderResult;
}
// Step 2: Reserve Inventory
var reserveInventoryResult = await _inventoryService.ReserveInventoryStepAsync(order);
if (!reserveInventoryResult.IsSuccess)
{
// Step 2 failed, revert actions for previous steps
await _saga.RevertInitiateOrderStepAsync(order);
return reserveInventoryResult;
}
// Step 3: Process Payment
var processPaymentResult = await _paymentService.ProcessPaymentStepAsync(order);
if (!processPaymentResult.IsSuccess)
{
// Step 3 failed, revert actions for previous steps
await _inventoryService.RevertReserveInventoryStepAsync(order);
await _saga.RevertInitiateOrderStepAsync(order);
}
return processPaymentResult;
}
catch (Exception ex)
{
return new SagaStepResult { IsSuccess = false, ErrorMessage = ex.Message };
}
}
}
Here's my order class and SagaStepResult
领英推荐
public class Order
{
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public string Status { get; set; }
}
public class SagaStepResult
{
public bool IsSuccess { get; set; }
public string ErrorMessage { get; set; }
}
Order processing Saga :
public class OrderProcessingSaga
{
public SagaStepResult InitiateOrderStep(Order order)
{
// First step of the saga
Console.WriteLine($"Order initiated for OrderId: {order.OrderId}");
// Update the order status to "Created"
order.Status = "Created";
return new SagaStepResult { IsSuccess = true };
}
// Compensating action for Initial step step
public async Task RevertInitiateOrderStepAsync(Order order)
{
// Release the Order
await Task.Delay(1000);
Console.WriteLine($"Order initiated for OrderId: {order.OrderId} needs to be released");
}
}
Inventory Service :
public class InventoryService
{
public async Task<SagaStepResult> ReserveInventoryStepAsync(Order order)
{
try
{
// Reservation is successful
await Task.Delay(1000);
Console.WriteLine($"Inventory reserved for OrderId: {order.OrderId}");
// Update the order status to "InventoryReserved"
order.Status = "InventoryReserved";
return new SagaStepResult { IsSuccess = true };
}
catch (Exception ex)
{
return new SagaStepResult { IsSuccess = false, ErrorMessage = ex.Message };
}
}
public async Task RevertReserveInventoryStepAsync(Order order)
{
// Release the reserved inventory
await Task.Delay(1000);
Console.WriteLine($"Inventory reservation for OrderId: {order.OrderId} needs to be released");
}
}
Payment Service:
public class PaymentService
{
public async Task<SagaStepResult> ProcessPaymentStepAsync(Order order)
{
try
{
// Simulate a payment failure
await Task.Delay(1000);
Console.WriteLine($"Processing payment for OrderId: {order.OrderId}");
// Update the order status to "PaymentProcessed"
order.Status = "PaymentProcessed";
return new SagaStepResult { IsSuccess = false, ErrorMessage = "Payment failed" };
}
catch (Exception ex)
{
return new SagaStepResult { IsSuccess = false, ErrorMessage = ex.Message };
}
}
public async Task RevertProcessPaymentStepAsync(Order order)
{
// Refund the payment
await Task.Delay(1000);
Console.WriteLine($"Payment for OrderId: {order.OrderId} needs to be refunded");
}
}
PostProcessing Step:
public class PostProcessingService
{
public async Task<SagaStepResult> ExecutePostProcessingAsync(Order order)
{
try
{
// Update relevant databases or data stores
// Send notifications via email or messaging services
// Log the transaction details
// Monitor for any anomalies or performance issues
Console.WriteLine($"Post-processing activities for OrderId: {order.OrderId} completed successfully.");
return new SagaStepResult { IsSuccess = true };
}
catch (Exception ex)
{
Console.WriteLine("Error during post-processing: " + ex.Message);
return new SagaStepResult { IsSuccess = false, ErrorMessage = ex.Message };
}
}
}
Final Thoughts on the Saga Design Pattern:
As our technology gets more complicated and spread out, managing everything efficiently becomes really important. The Saga design pattern is a great way to handle complex tasks that involve many steps and different systems. It lets each part of the task take care of itself, which means if something goes wrong, it’s easier to fix without affecting everything else.