Cấu trúc dữ liệu và Giải thuật Bài 95: Tối ưu hóa Cơ sở dữ liệu chuyên sâu - Kỹ thuật Sharding, Partitioning và Phân tán Transaction (Saga Pattern).
Trong Bài 94, chúng ta đã dùng Redis và Kafka để dựng lên một bức tường thành bảo vệ cơ sở dữ liệu khỏi hàng triệu luồng truy cập đồng thời. Tuy nhiên, khi hệ thống E-commerce của Hasaki hoạt động qua nhiều năm, dữ liệu tĩnh (Cold Data) sẽ tích tụ lại. Khi một bảng orders trong MySQL vượt qua mốc hàng chục triệu dòng, chiều cao của B+ Tree sẽ tăng lên, các truy vấn bắt đầu chậm chạp và ổ cứng I/O chạm ngưỡng cổ chai.
Lúc này, dù bạn có bao nhiêu Goroutines hay Kafka Brokers ở tầng trên, hệ thống vẫn sẽ chậm. Để giải quyết, chúng ta phải phẫu thuật trực tiếp vào tầng Lưu trữ (Storage) bằng kỹ thuật chia để trị.
1. Ranh giới giữa Partitioning và Sharding
Nhiều kỹ sư thường nhầm lẫn giữa hai khái niệm này, nhưng ranh giới vật lý của chúng hoàn toàn khác biệt.
A. Partitioning (Phân mảnh cục bộ)
Đây là kỹ thuật chia một bảng khổng lồ thành nhiều bảng nhỏ hơn (Partitions) nằm trên cùng một máy chủ Database.
-
Ví dụ: Bảng
ordersđược chia theo tháng (Range Partitioning) thànhorders_2026_09,orders_2026_10. -
Ưu điểm: Cấu trúc B+ Tree của mỗi partition nhỏ hơn, query nhanh hơn. Hệ quản trị CSDL (như PostgreSQL, MySQL) tự động xử lý logic định tuyến, ứng dụng (Application) không cần sửa code.
-
Nhược điểm: Vẫn bị giới hạn bởi RAM, CPU và năng lực Ổ cứng của một máy chủ vật lý duy nhất.
B. Sharding (Phân mảnh ngang phân tán)
Khi một máy chủ không thể gánh nổi, Sharding cắt dữ liệu ra và ném sang các máy chủ vật lý (Database Nodes) hoàn toàn độc lập.

Để định tuyến đúng dữ liệu OrderID = 105 vào đúng máy chủ, chúng ta sử dụng một Shard Key kết hợp với Consistent Hashing (Băm nhất quán - Bài 92):
-
Thay vì băm IP, ta băm
Hash(OrderID). -
Khi có thay đổi về số lượng Database (Thêm/Bớt Node), Consistent Hashing đảm bảo lượng dữ liệu phải di chuyển (Migrate) giữa các Database là thấp nhất.
2. Lời nguyền của Sharding và Kiến trúc Microservices
Ngay khi bạn cắt dữ liệu ra nhiều máy chủ, hoặc tách hệ thống thành các Microservices (Order Service, Payment Service, Inventory Service), bạn đã vô tình phá vỡ một thứ thiêng liêng nhất của Cơ sở dữ liệu quan hệ: Tính toàn vẹn ACID (Đặc biệt là tính Nguyên tử - Atomicity).
Giả sử luồng mua hàng gồm 3 bước:
-
Tạo đơn hàng (Order DB)
-
Trừ tiền ví điện tử (Payment DB)
-
Trừ tồn kho (Inventory DB)
Trong một hệ thống Monolith cũ, bạn chỉ cần gõ BEGIN; ... COMMIT;. Nếu bước 3 lỗi, ROLLBACK sẽ hủy toàn bộ. Nhưng trong Microservices, 3 bước này nằm ở 3 Database khác nhau. Bạn không thể dùng một lệnh ROLLBACK chung. Cố gắng sử dụng cơ chế Two-Phase Commit (2PC) sẽ khóa chặt (Lock) toàn bộ các Database trên mạng lưới, khiến hệ thống chậm như rùa bò và mất đi khả năng High-Concurrency.
3. Saga Pattern: Vị cứu tinh của Giao dịch phân tán (Distributed Transaction)
Để xử lý bài toán trên mà không làm khóa Database, Hector Garcia-Molina đã giới thiệu Saga Pattern vào năm 1987. Saga không cố gắng tạo ra một Transaction khổng lồ. Nó định nghĩa luồng công việc là một chuỗi các Local Transactions (Giao dịch cục bộ).
Mỗi khi một dịch vụ hoàn thành giao dịch cục bộ của nó (commit thành công vào DB riêng), nó sẽ bắn ra một Sự kiện (Event/Message) vào Kafka hoặc RabbitMQ để kích hoạt dịch vụ tiếp theo.

So Sánh Các Kiểu Điều Phối Trong Distributed Transactions: Choreography vs. Orchestration
| Kiểu điều phối | Đặc điểm | Phù hợp cho |
|---|---|---|
| Choreography (Múa tập thể) | Không có trung tâm điều khiển. Order bắn event -> Payment nghe và làm -> Payment bắn event -> Inventory nghe. | Hệ thống nhỏ (2-4 bước), cần tốc độ phản hồi cực nhanh, ít phụ thuộc. |
| Orchestration (Nhạc trưởng) | Có một Service trung tâm (Orchestrator) ra lệnh: "Payment trừ tiền đi", "Inventory xuất kho đi". | Hệ thống phức tạp, nhiều bước rẽ nhánh, cần kiểm soát trạng thái giao dịch tập trung. |
4. Hành động bù trừ (Compensating Transactions)
Vấn đề lớn nhất của Saga: Điều gì xảy ra nếu Bước 1 (Tạo đơn) và Bước 2 (Trừ tiền) đã Commit thành công vào DB, nhưng Bước 3 (Trừ tồn kho) thất bại do kho báo hết hàng? Bạn không thể ROLLBACK Bước 2 bằng lệnh SQL của cơ sở dữ liệu được nữa, vì Transaction của Bước 2 đã đóng từ lâu.
Giải pháp duy nhất của Saga là chạy Compensating Transactions (Giao dịch bù trừ) theo chiều ngược lại.
-
Bù trừ của "Trừ tiền" là "Hoàn tiền".
-
Bù trừ của "Tạo đơn hàng" là "Cập nhật trạng thái đơn thành Đã Hủy".
Saga yêu cầu hệ thống khi đi tiến được thì phải có khả năng lùi lại (Undo) bằng các logic nghiệp vụ (Business Logic) một cách tuyệt đối an toàn và độc lập (Idempotent).
5. Hiện thực hóa Saga Orchestrator bằng Golang
Với Golang, bạn có thể dễ dàng xây dựng một Saga Orchestrator sử dụng Go Channels để kiểm soát trạng thái của các microservices và thực hiện bẻ lái (rollback) khi có lỗi.
Go
package main
import (
"fmt"
"time"
)
// Các trạng thái của Giao dịch
type SagaState string
const (
StateOrderCreated SagaState = "ORDER_CREATED"
StatePaid SagaState = "PAID"
StateFailed SagaState = "FAILED"
)
// Giả lập các Microservices (Trả về lỗi nếu thất bại)
func executePayment(orderID string) error {
fmt.Printf("[Payment Service] Đang trừ tiền cho đơn %s...\n", orderID)
// Trả về nil nếu thành công
return nil
}
func compensatePayment(orderID string) {
fmt.Printf("[Payment Service] Thực hiện BÙ TRỪ: Hoàn tiền cho đơn %s\n", orderID)
}
func executeInventory(orderID string) error {
fmt.Printf("[Inventory Service] Đang kiểm tra và trừ tồn kho cho đơn %s...\n", orderID)
// Giả lập kho báo hết hàng -> Trả về lỗi
return fmt.Errorf("HẾT HÀNG")
}
func compensateOrder(orderID string) {
fmt.Printf("[Order Service] Thực hiện BÙ TRỪ: Hủy đơn hàng %s\n", orderID)
}
// Saga Orchestrator quản lý tiến trình
func RunCheckoutSaga(orderID string) {
fmt.Printf("\n--- BẮT ĐẦU SAGA CHO ĐƠN HÀNG: %s ---\n", orderID)
// Bước 1: Khởi tạo đơn (Mặc định thành công)
fmt.Printf("[Order Service] Đã tạo đơn %s (Pending)\n", orderID)
currentState := StateOrderCreated
// Bước 2: Thanh toán
if err := executePayment(orderID); err != nil {
fmt.Println("Lỗi thanh toán! Hủy Saga.")
compensateOrder(orderID)
return
}
currentState = StatePaid
// Bước 3: Kho hàng
if err := executeInventory(orderID); err != nil {
fmt.Printf("Lỗi kho hàng (%v)! Bắt đầu kích hoạt chuỗi BÙ TRỪ...\n", err)
// Kích hoạt Compensating Transactions ngược lại lịch sử
switch currentState {
case StatePaid:
compensatePayment(orderID)
fallthrough // Go tiếp xuống dưới để bù trừ tiếp các bước trước
case StateOrderCreated:
compensateOrder(orderID)
}
return
}
fmt.Println("Saga hoàn tất! Đơn hàng xử lý thành công.")
}
func main() {
RunCheckoutSaga("ORD-1004")
time.Sleep(1 * time.Second)
}
Kiến trúc Database phân tán đòi hỏi kỹ sư phải thay đổi hoàn toàn tư duy từ Tính nhất quán tuyệt đối (Strong Consistency) sang Tính nhất quán cuối cùng (Eventual Consistency). Không có gì đảm bảo tại giây thứ 1 toàn bộ hệ thống đều đồng bộ, nhưng Saga đảm bảo rằng ở giây thứ 5, dù thành công hay thất bại, hệ thống sẽ tự làm sạch và trở về trạng thái toàn vẹn.
All Rights Reserved