Trong bốn bài đầu tiên của series, chúng ta đã tìm hiểu về nền tảng tối ưu hóa cơ sở dữ liệu, phân tích và tối ưu câu truy vấn SQL, chiến lược indexing chuyên sâu, và thiết kế schema tối ưu. Bài viết này sẽ đi sâu vào một khía cạnh quan trọng khác: quản lý transaction và concurrency hiệu quả.

Trong môi trường thực tế, hệ thống cơ sở dữ liệu thường phải xử lý nhiều người dùng và ứng dụng truy cập đồng thời. Việc quản lý transaction và concurrency không chỉ đảm bảo tính nhất quán của dữ liệu mà còn ảnh hưởng trực tiếp đến hiệu năng và khả năng mở rộng của hệ thống. Trong bài viết này, chúng ta sẽ khám phá các nguyên tắc, kỹ thuật và chiến lược để quản lý transaction và concurrency hiệu quả.

Transaction isolation levels và tác động đến hiệu năng

Transaction là một chuỗi các thao tác được thực hiện như một đơn vị logic duy nhất, tuân theo các thuộc tính ACID (Atomicity, Consistency, Isolation, Durability).

Các thuộc tính ACID


  flowchart LR
  ACID["ACID Properties"]
  Atomicity["Atomicity<br/>Tất cả hoặc không có gì"]
  Consistency["Consistency<br/>Dữ liệu luôn nhất quán"]
  Isolation["Isolation<br/>Các transaction độc lập"]
  Durability["Durability<br/>Dữ liệu được lưu trữ bền vững"]

  ACID --> Atomicity
  ACID --> Consistency
  ACID --> Isolation
  ACID --> Durability
  1. Atomicity (Tính nguyên tử): Tất cả các thao tác trong transaction đều thành công hoặc không có thao tác nào được áp dụng.
  2. Consistency (Tính nhất quán): Transaction đưa cơ sở dữ liệu từ trạng thái nhất quán này sang trạng thái nhất quán khác.
  3. Isolation (Tính độc lập): Các transaction đồng thời không ảnh hưởng lẫn nhau.
  4. Durability (Tính bền vững): Khi transaction đã commit, dữ liệu được lưu trữ bền vững.

Transaction isolation levels

Các hệ quản trị cơ sở dữ liệu cung cấp nhiều mức isolation khác nhau, mỗi mức có sự cân bằng khác nhau giữa tính nhất quán và hiệu năng:


  flowchart TD
  IsolationLevels["**Isolation Levels**"]

  ReadUncommitted["Read Uncommitted<br/>⬆ Hiệu năng cao nhất<br/>⬇ Nhất quán thấp nhất"]
  ReadCommitted["Read Committed"]
  RepeatableRead["Repeatable Read"]
  Serializable["Serializable<br/>⬇ Hiệu năng thấp nhất<br/>⬆ Nhất quán cao nhất"]

  IsolationLevels -->|"⬆ Mức độ cô lập"| ReadUncommitted
  ReadUncommitted -->|"Dirty Read ❌"| ReadCommitted
  ReadCommitted -->|"Non-repeatable Read ❌"| RepeatableRead
  RepeatableRead -->|"Phantom Read ❌"| Serializable

  Serializable -->|"Mức cô lập cao nhất"| IsolationLevels
  1. Read Uncommitted: Mức isolation thấp nhất

    • Cho phép dirty reads (đọc dữ liệu chưa commit)
    • Hiệu năng cao nhất, nhưng ít đảm bảo nhất
    • Ít khi được sử dụng trong thực tế
  2. Read Committed: Mức mặc định trong nhiều hệ thống

    • Ngăn chặn dirty reads
    • Vẫn cho phép non-repeatable reads và phantom reads
    • Cân bằng tốt giữa hiệu năng và tính nhất quán
  3. Repeatable Read: Mức trung gian

    • Ngăn chặn dirty reads và non-repeatable reads
    • Vẫn cho phép phantom reads trong một số hệ thống
    • Mức mặc định trong MySQL/InnoDB
  4. Serializable: Mức isolation cao nhất

    • Ngăn chặn tất cả các vấn đề concurrency
    • Hiệu năng thấp nhất do phải sử dụng locking nghiêm ngặt
    • Chỉ nên sử dụng khi tính nhất quán là yêu cầu tuyệt đối

Các vấn đề concurrency

  1. Dirty Read: Transaction đọc dữ liệu chưa được commit bởi transaction khác

    Transaction A: UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    Transaction B: SELECT balance FROM accounts WHERE id = 1; -- Đọc giá trị đã bị trừ
    Transaction A: ROLLBACK; -- Dữ liệu Transaction B đọc không còn hợp lệ
    
  2. Non-repeatable Read: Transaction đọc cùng một dữ liệu hai lần nhưng nhận được kết quả khác nhau

    Transaction A: SELECT balance FROM accounts WHERE id = 1; -- Đọc balance = 1000
    Transaction B: UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT;
    Transaction A: SELECT balance FROM accounts WHERE id = 1; -- Đọc balance = 900
    
  3. Phantom Read: Transaction thực hiện query hai lần nhưng số lượng rows thay đổi

    Transaction A: SELECT COUNT(*) FROM accounts WHERE balance > 1000; -- Đếm được 5 tài khoản
    Transaction B: INSERT INTO accounts (id, balance) VALUES (6, 1500); COMMIT;
    Transaction A: SELECT COUNT(*) FROM accounts WHERE balance > 1000; -- Đếm được 6 tài khoản
    

Tác động của isolation levels đến hiệu năng

Mức isolation càng cao, hiệu năng càng giảm do cần nhiều locking hơn:

-- MySQL: Thiết lập isolation level cho session
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- PostgreSQL
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- SQL Server
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

Lựa chọn isolation level phù hợp:

  1. Read Uncommitted: Chỉ sử dụng cho các truy vấn phân tích không yêu cầu tính chính xác tuyệt đối
  2. Read Committed: Phù hợp cho hầu hết các ứng dụng OLTP
  3. Repeatable Read: Khi cần đảm bảo tính nhất quán cao hơn
  4. Serializable: Chỉ sử dụng cho các giao dịch tài chính quan trọng hoặc yêu cầu tính nhất quán tuyệt đối

Ví dụ thực tế về isolation level

Giả sử chúng ta có hệ thống đặt vé:

-- Bảng seats lưu trạng thái ghế (available/booked)
CREATE TABLE seats (
    id INT PRIMARY KEY,
    event_id INT,
    seat_number VARCHAR(10),
    status VARCHAR(20),
    user_id INT NULL
);

-- Với Read Committed (mặc định trong PostgreSQL)
-- Transaction A: Kiểm tra và đặt ghế
BEGIN;
-- Kiểm tra ghế còn trống
SELECT * FROM seats WHERE event_id = 101 AND seat_number = 'A1' AND status = 'available';
-- Giả sử ghế còn trống, tiến hành đặt
-- Nhưng trước khi UPDATE, Transaction B cũng kiểm tra và thấy ghế còn trống

-- Transaction B (đồng thời)
BEGIN;
SELECT * FROM seats WHERE event_id = 101 AND seat_number = 'A1' AND status = 'available';
-- Cũng thấy ghế còn trống
UPDATE seats SET status = 'booked', user_id = 202 WHERE id = 1;
COMMIT;

-- Quay lại Transaction A
UPDATE seats SET status = 'booked', user_id = 101 WHERE id = 1;
-- Không có lỗi, nhưng đã ghi đè lên booking của Transaction B!
COMMIT;

Giải pháp với isolation level cao hơn:

-- Sử dụng Serializable
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT * FROM seats WHERE event_id = 101 AND seat_number = 'A1' AND status = 'available';
-- Nếu Transaction B đã update và commit, Transaction A sẽ bị abort khi commit
UPDATE seats SET status = 'booked', user_id = 101 WHERE id = 1;
COMMIT; -- Có thể gặp lỗi serialization failure

Hoặc sử dụng locking rõ ràng:

-- Sử dụng explicit locking
BEGIN;
-- FOR UPDATE sẽ lock row cho đến khi transaction kết thúc
SELECT * FROM seats WHERE event_id = 101 AND seat_number = 'A1' AND status = 'available' FOR UPDATE;
-- Transaction B sẽ phải đợi cho đến khi Transaction A kết thúc
UPDATE seats SET status = 'booked', user_id = 101 WHERE id = 1;
COMMIT;

Deadlocks và cách phòng tránh

Deadlock xảy ra khi hai hoặc nhiều transaction chờ đợi lẫn nhau để giải phóng lock, dẫn đến tình trạng bế tắc.

Cơ chế deadlock


  flowchart TD
  A[Transaction A] -->|Holds lock on| B[Resource 1]
  C[Transaction B] -->|Holds lock on| D[Resource 2]
  A -->|Wants lock on| D
  C -->|Wants lock on| B

Ví dụ về deadlock:

-- Transaction A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- Locks account 1
-- Đợi một chút...
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- Cần lock account 2

-- Transaction B (đồng thời)
BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE id = 2; -- Locks account 2
-- Đợi một chút...
UPDATE accounts SET balance = balance + 200 WHERE id = 1; -- Cần lock account 1, nhưng đã bị Transaction A lock
-- Deadlock: A đang chờ B giải phóng account 2, B đang chờ A giải phóng account 1

Phát hiện và xử lý deadlock

Hầu hết các hệ quản trị cơ sở dữ liệu hiện đại có cơ chế phát hiện deadlock tự động:

  1. Timeout: Transaction sẽ bị hủy sau một khoảng thời gian chờ đợi
  2. Dependency graph: Hệ thống xây dựng đồ thị phụ thuộc và phát hiện chu trình
  3. Victim selection: Khi phát hiện deadlock, một transaction sẽ bị chọn làm “nạn nhân” và bị rollback
-- MySQL: Kiểm tra thông tin deadlock gần đây
SHOW ENGINE INNODB STATUS;

-- PostgreSQL: Kiểm tra các lock đang chờ
SELECT blocked_locks.pid AS blocked_pid,
       blocking_locks.pid AS blocking_pid,
       blocked_activity.usename AS blocked_user,
       blocking_activity.usename AS blocking_user,
       blocked_activity.query AS blocked_statement,
       blocking_activity.query AS blocking_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
    AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
    AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
    AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
    AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
    AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
    AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
    AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
    AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
    AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
    AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;

Chiến lược phòng tránh

  1. Truy cập tài nguyên theo thứ tự nhất quán:

    -- Luôn cập nhật account với ID nhỏ hơn trước
    BEGIN;
    -- Sắp xếp các ID theo thứ tự tăng dần
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    UPDATE accounts SET balance = balance + 100 WHERE id = 2;
    COMMIT;
    
  2. Giảm thời gian giữ lock:

    -- Thay vì
    BEGIN;
    SELECT * FROM accounts WHERE id = 1; -- Thao tác khác
    -- ... Nhiều thao tác khác ...
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    COMMIT;
    
    -- Nên
    -- Chuẩn bị dữ liệu trước
    SELECT * FROM accounts WHERE id = 1;
    -- ... Xử lý dữ liệu ...
    
    -- Chỉ bắt đầu transaction khi cần thiết
    BEGIN;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    COMMIT;
    
  3. Sử dụng lock timeout:

    -- PostgreSQL
    SET lock_timeout = '5s';
    
    -- SQL Server
    SET LOCK_TIMEOUT 5000; -- 5 seconds
    
  4. Sử dụng SELECT FOR UPDATE SKIP LOCKED (PostgreSQL, Oracle):

    -- Bỏ qua các row đã bị lock thay vì đợi
    BEGIN;
    SELECT * FROM jobs WHERE status = 'pending'
    ORDER BY priority DESC LIMIT 1 FOR UPDATE SKIP LOCKED;
    -- Xử lý job
    UPDATE jobs SET status = 'processing' WHERE id = ?;
    COMMIT;
    
  5. Retry logic trong ứng dụng:

/**
* Chuyển tiền giữa các tài khoản với retry logic để tránh deadlock
*
* @param int $fromId ID của tài khoản nguồn
* @param int $toId ID của tài khoản đích
* @param float $amount Số tiền cần chuyển
* @return bool True nếu thành công, False nếu thất bại sau nhiều lần thử
*/
public function transferMoney(int $fromId, int $toId, float $amount): bool
{
   $maxRetries = 3;
   $retryCount = 0;

   while ($retryCount < $maxRetries) {
       try {
           return DB::transaction(function () use ($fromId, $toId, $amount) {
               // Luôn truy cập theo thứ tự ID để tránh deadlock
               $accountIds = collect([$fromId, $toId])->sort()->values();

               // Lock và cập nhật tài khoản đầu tiên với FOR UPDATE
               $firstAccount = DB::table('accounts')
                   ->where('id', $accountIds[0])
                   ->lockForUpdate()
                   ->first();

               if (!$firstAccount) {
                   throw new ModelNotFoundException("Tài khoản {$accountIds[0]} không tồn tại");
               }

               // Tính toán số tiền cần trừ/cộng cho tài khoản đầu tiên
               $firstAmount = $accountIds[0] == $fromId ? -$amount : $amount;

               // Cập nhật số dư tài khoản đầu tiên
               DB::table('accounts')
                   ->where('id', $accountIds[0])
                   ->update(['balance' => DB::raw("balance + {$firstAmount}")]);

               // Lock và cập nhật tài khoản thứ hai với FOR UPDATE
               $secondAccount = DB::table('accounts')
                   ->where('id', $accountIds[1])
                   ->lockForUpdate()
                   ->first();

               if (!$secondAccount) {
                   throw new ModelNotFoundException("Tài khoản {$accountIds[1]} không tồn tại");
               }

               // Tính toán số tiền cần trừ/cộng cho tài khoản thứ hai
               $secondAmount = $accountIds[1] == $fromId ? -$amount : $amount;

               // Cập nhật số dư tài khoản thứ hai
               DB::table('accounts')
                   ->where('id', $accountIds[1])
                   ->update(['balance' => DB::raw("balance + {$secondAmount}")]);

               // Ghi log giao dịch
               DB::table('transactions')->insert([
                   'from_account_id' => $fromId,
                   'to_account_id' => $toId,
                   'amount' => $amount,
                   'created_at' => now(),
               ]);

               return true;
           });
       } catch (\Illuminate\Database\QueryException $e) {
           // Xử lý deadlock và các lỗi liên quan đến transaction
           if ($e->errorInfo[1] == 1213) { // Mã lỗi MySQL cho deadlock
               $retryCount++;
               // Exponential backoff
               usleep($retryCount * 100000); // 0.1s, 0.2s, 0.3s

               // Log lỗi deadlock
               Log::warning("Deadlock phát hiện khi chuyển tiền, thử lại lần {$retryCount}", [
                   'from_id' => $fromId,
                   'to_id' => $toId,
                   'amount' => $amount
               ]);
           } else {
               // Các lỗi khác không phải deadlock, throw lại
               Log::error("Lỗi khi chuyển tiền: " . $e->getMessage(), [
                   'from_id' => $fromId,
                   'to_id' => $toId,
                   'amount' => $amount,
                   'error' => $e->errorInfo
               ]);
               return false;
           }
       } catch (\Exception $e) {
           Log::error("Lỗi khi chuyển tiền: " . $e->getMessage(), [
               'from_id' => $fromId,
               'to_id' => $toId,
               'amount' => $amount
           ]);
           return false;
       }
   }

   // Thất bại sau nhiều lần thử
   Log::error("Chuyển tiền thất bại sau {$maxRetries} lần thử", [
       'from_id' => $fromId,
       'to_id' => $toId,
       'amount' => $amount
   ]);

   return false;
}

Optimistic vs Pessimistic locking

Có hai cách tiếp cận chính để xử lý concurrency: Optimistic locking và Pessimistic locking.

Pessimistic locking

Pessimistic locking giả định rằng xung đột sẽ xảy ra và ngăn chặn chúng bằng cách lock dữ liệu trước khi thực hiện thay đổi.


  sequenceDiagram
    participant TransactionA as Transaction A
    participant Database as Database
    participant TransactionB as Transaction B

    TransactionA->>Database: BEGIN
    TransactionA->>Database: SELECT * FROM products WHERE id = 1 FOR UPDATE
    Note over Database: Row is locked

    TransactionB->>Database: BEGIN
    TransactionB->>Database: SELECT * FROM products WHERE id = 1 FOR UPDATE
    Note over TransactionB: Waits for lock

    TransactionA->>Database: UPDATE products SET stock = stock - 1 WHERE id = 1
    TransactionA->>Database: COMMIT
    Note over Database: Lock released
    Note over TransactionB: Continues execution

    TransactionB->>Database: UPDATE products SET stock = stock - 1 WHERE id = 1
    TransactionB->>Database: COMMIT

Các loại pessimistic locking:

  1. Shared Lock (S): Nhiều transaction có thể đọc cùng lúc, nhưng không thể ghi

    -- MySQL/PostgreSQL
    SELECT * FROM products WHERE id = 1 FOR SHARE;
    
  2. Exclusive Lock (X): Chỉ một transaction có thể đọc và ghi

    -- MySQL/PostgreSQL
    SELECT * FROM products WHERE id = 1 FOR UPDATE;
    
  3. Row-level vs Table-level locks:

    -- MySQL: Table-level lock
    LOCK TABLES products WRITE;
    -- Thao tác với bảng products
    UNLOCK TABLES;
    
    -- PostgreSQL: Table-level lock
    LOCK TABLE products IN EXCLUSIVE MODE;
    

Ưu điểm của pessimistic locking:

  • Đảm bảo tính nhất quán cao
  • Phù hợp khi xung đột thường xuyên xảy ra
  • Không cần xử lý retry logic phức tạp

Nhược điểm:

  • Giảm concurrency và throughput
  • Có thể dẫn đến deadlock
  • Giữ lock lâu có thể ảnh hưởng đến hiệu năng

Optimistic locking

Optimistic locking giả định rằng xung đột ít khi xảy ra và chỉ kiểm tra xung đột khi commit.


  sequenceDiagram
    participant Client
    participant OS as Order Service
    participant IS as Inventory Service
    participant PS as Payment Service

    %% Successful flow
    Client->>OS: Place Order
    Note over OS: T1: Create Order
    OS->>IS: Request Inventory
    Note over IS: T2: Reserve Inventory
    IS->>PS: Request Payment
    Note over PS: T3: Process Payment
    PS->>OS: Payment Confirmation
    Note over OS: T4: Complete Order
    OS->>Client: Order Completed

    %% Failure handling with compensating transactions
    alt Payment Fails
        PS-->>OS: Payment Failed
        OS-->>IS: Cancel Reservation
        IS-->>OS: Inventory Released
        OS-->>Client: Order Canceled
    end

Cách triển khai optimistic locking:

  1. Version column:

    CREATE TABLE products (
        id INT PRIMARY KEY,
        name VARCHAR(100),
        price DECIMAL(10,2),
        stock INT,
        version INT DEFAULT 1
    );
    
    -- Khi update
    UPDATE products
    SET price = 150, version = version + 1
    WHERE id = 1 AND version = 1;
    
    -- Kiểm tra số rows affected
    -- Nếu = 0, có xung đột và cần retry
    
  2. Timestamp column:

    CREATE TABLE products (
        id INT PRIMARY KEY,
        name VARCHAR(100),
        price DECIMAL(10,2),
        stock INT,
        last_updated TIMESTAMP
    );
    
    -- Khi update
    UPDATE products
    SET price = 150, last_updated = CURRENT_TIMESTAMP
    WHERE id = 1 AND last_updated = '2023-01-15 14:30:00';
    
  3. Hash/Checksum:

    -- Tính toán checksum từ các cột quan trọng
    SELECT id, name, price, stock, MD5(CONCAT(name, price, stock)) as checksum
    FROM products WHERE id = 1;
    
    -- Khi update
    UPDATE products
    SET price = 150
    WHERE id = 1 AND MD5(CONCAT(name, price, stock)) = 'abc123def456';
    

Ưu điểm của optimistic locking:

  • Concurrency cao hơn, không blocking
  • Không có deadlock
  • Phù hợp khi xung đột ít xảy ra

Nhược điểm:

  • Cần xử lý retry logic trong ứng dụng
  • Hiệu năng kém hơn khi xung đột thường xuyên xảy ra
  • Có thể dẫn đến starvation (một transaction liên tục bị conflict)

Khi nào sử dụng mỗi loại locking

  1. Sử dụng Pessimistic locking khi:

    • Xung đột thường xuyên xảy ra
    • Dữ liệu quan trọng, không thể chấp nhận conflict
    • Thời gian xử lý transaction ngắn
    • Số lượng người dùng đồng thời không quá lớn
  2. Sử dụng Optimistic locking khi:

    • Xung đột ít khi xảy ra
    • Có thể chấp nhận retry khi có conflict
    • Cần concurrency cao
    • Read-heavy workloads

Ví dụ thực tế: Quản lý inventory

-- Pessimistic approach
BEGIN;
-- Lock row
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- Kiểm tra stock
IF stock >= 5 THEN
    -- Đủ hàng, giảm stock
    UPDATE products SET stock = stock - 5 WHERE id = 1;
    -- Tạo order
    INSERT INTO orders (...) VALUES (...);
    COMMIT;
ELSE
    -- Không đủ hàng
    ROLLBACK;
END IF;

-- Optimistic approach
BEGIN;
-- Đọc dữ liệu không lock
SELECT stock, version FROM products WHERE id = 1;
-- Kiểm tra stock
IF stock >= 5 THEN
    -- Đủ hàng, thử giảm stock
    UPDATE products
    SET stock = stock - 5, version = version + 1
    WHERE id = 1 AND version = current_version;

    -- Kiểm tra xem update có thành công không
    IF rows_affected = 1 THEN
        -- Tạo order
        INSERT INTO orders (...) VALUES (...);
        COMMIT;
    ELSE
        -- Có xung đột, rollback và retry
        ROLLBACK;
        -- Retry logic
    END IF;
ELSE
    -- Không đủ hàng
    ROLLBACK;
END IF;

Mô hình concurrency nâng cao

Ngoài các kỹ thuật cơ bản, có nhiều mô hình concurrency nâng cao giúp cải thiện hiệu năng và khả năng mở rộng.

Multi-Version Concurrency Control (MVCC)

MVCC là cơ chế được sử dụng bởi nhiều hệ quản trị cơ sở dữ liệu hiện đại (PostgreSQL, Oracle, MySQL InnoDB) để cải thiện concurrency.


  flowchart TD
    RowX["Row X"]
    Version1[
        Version 1
        txid=100
        xmin=100
        xmax=200
    ]
    Version2[
        Version 2
        txid=200
        xmin=200
        xmax=null
    ]
    Transaction150["Transaction 150"]
    Transaction250["Transaction 250"]

    RowX -->|Contains| Version1
    RowX -->|Updated To| Version2
    Transaction150 -- Sees --> Version1
    Transaction250 -- Sees --> Version2

Nguyên lý hoạt động:

  1. Mỗi khi row được update, một phiên bản mới được tạo ra thay vì ghi đè phiên bản cũ
  2. Mỗi transaction chỉ nhìn thấy phiên bản phù hợp với thời điểm bắt đầu transaction
  3. Các phiên bản cũ sẽ được dọn dẹp bởi quá trình VACUUM (PostgreSQL) hoặc purge (MySQL)

Ưu điểm của MVCC:

  • Readers không block writers và ngược lại
  • Cải thiện đáng kể concurrency cho read-heavy workloads
  • Hỗ trợ snapshot isolation

Nhược điểm:

  • Overhead về không gian lưu trữ
  • Cần quá trình dọn dẹp (VACUUM) định kỳ
  • Có thể gặp vấn đề với long-running transactions

Command Query Responsibility Segregation (CQRS)

CQRS tách biệt các thao tác đọc (queries) và ghi (commands) thành các mô hình riêng biệt.


  flowchart TD
    Client["Client"]
    CommandAPI["Command API"]
    QueryAPI["Query API"]
    WriteModel["Write Model<br/>Optimized for writes"]
    ReadModel["Read Model<br/>Optimized for reads"]

    Client --> CommandAPI
    Client --> QueryAPI
    CommandAPI --> WriteModel
    QueryAPI --> ReadModel
    WriteModel -->|Sync| ReadModel

Cách triển khai CQRS:

  1. Separate databases:

    • Write database: Normalized, optimized for consistency
    • Read database: Denormalized, optimized for query performance
  2. Event sourcing:

    • Lưu trữ chuỗi các events thay vì state
    • Rebuild state từ events khi cần
  3. Eventual consistency:

    • Read model được cập nhật bất đồng bộ
    • Chấp nhận độ trễ giữa write và read

Ưu điểm của CQRS:

  • Tối ưu hóa riêng cho read và write
  • Khả năng mở rộng độc lập
  • Phù hợp cho hệ thống phức tạp với nhiều views khác nhau

Nhược điểm:

  • Phức tạp hơn trong triển khai và bảo trì
  • Eventual consistency có thể gây khó khăn cho UI
  • Overhead về đồng bộ hóa dữ liệu

Read/Write Splitting

Read/Write Splitting là kỹ thuật phân tách traffic đọc và ghi đến các server khác nhau.


  flowchart TD
  A[Application] --> B[Load Balancer/Router]
  B -->|Writes| C[Primary DB]
  B -->|Reads| D[Read Replica 1]
  B -->|Reads| E[Read Replica 2]
  B -->|Reads| F[Read Replica N]

Cách triển khai:

  1. Database-level replication:

    -- MySQL: Kiểm tra replication status
    SHOW SLAVE STATUS\G
    
    -- PostgreSQL: Kiểm tra replication status
    SELECT * FROM pg_stat_replication;
    
  2. Application-level routing:

    // Laravel: Cấu hình trong config/database.php
    'mysql' => [
        'read' => [
            'host' => [
                'replica1.example.com',
                'replica2.example.com',
            ],
        ],
        'write' => [
            'host' => 'primary.example.com',
        ],
        'sticky' => true,
        'driver' => 'mysql',
        // Các cấu hình khác...
    ],
    
    // Sử dụng trong code
    // Truy vấn đọc sẽ tự động đi đến read replicas
    $users = DB::table('users')->get();
    
    // Truy vấn ghi sẽ tự động đi đến primary
    DB::table('users')->insert(['name' => 'John']);
    
  3. Middleware solutions:

    • MySQL Proxy, ProxySQL
    • PgPool-II, PgBouncer cho PostgreSQL
    • AWS RDS Read Replicas

Ưu điểm:

  • Cải thiện đáng kể read throughput
  • Giảm tải cho primary server
  • Cải thiện availability

Nhược điểm:

  • Replication lag và eventual consistency
  • Phức tạp hơn trong quản lý
  • Cần xử lý các trường hợp đặc biệt (read-after-write consistency)

Sharding và Partitioning

Sharding và Partitioning chia dữ liệu thành các phần nhỏ hơn để cải thiện concurrency.


  flowchart TD
  A[Application] --> B[Shard Router]
  B --> C[Shard 1<br/>user_id: 1-1M]
  B --> D[Shard 2<br/>user_id: 1M-2M]
  B --> E[Shard 3<br/>user_id: 2M-3M]
  B --> F[Shard N<br/>...]

Ưu điểm:

  • Cải thiện concurrency bằng cách phân tán workload
  • Giảm kích thước của mỗi database instance
  • Giảm contention trên các hot tables

Nhược điểm:

  • Phức tạp trong quản lý và bảo trì
  • Cross-shard transactions phức tạp
  • Cần chiến lược sharding phù hợp

Quản lý connection pool và distributed transactions

Connection Pool Management

Connection pool là kỹ thuật quan trọng để quản lý và tái sử dụng các kết nối database, giúp cải thiện hiệu năng và khả năng mở rộng.


  flowchart TD
  A[Application] --> B[Connection Pool]
  B --> C[Connection 1]
  B --> D[Connection 2]
  B --> E[Connection 3]
  B --> F[Connection N]
  C --> G[Database]
  D --> G
  E --> G
  F --> G

Các tham số quan trọng trong connection pool:

  1. Pool size:

    • Min connections: Số lượng kết nối tối thiểu duy trì trong pool
    • Max connections: Số lượng kết nối tối đa cho phép
    • Ideal size: Thường là Ncores * (1 + Ndisk_spindles) * 2
  2. Connection lifecycle:

    • Max age: Thời gian tối đa một kết nối tồn tại
    • Idle timeout: Thời gian không hoạt động trước khi đóng kết nối
    • Validation interval: Tần suất kiểm tra kết nối còn hoạt động
  3. Behavior settings:

    • Acquisition timeout: Thời gian chờ để lấy kết nối từ pool
    • Fairness: Đảm bảo các request được xử lý theo thứ tự
    • Statement caching: Cache prepared statements

Ví dụ cấu hình connection pool với Laravel (PHP):

// Cấu hình trong config/database.php
'connections' => [
    'pgsql' => [
        'driver' => 'pgsql',
        'url' => env('DATABASE_URL'),
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '5432'),
        'database' => env('DB_DATABASE', 'mydb'),
        'username' => env('DB_USERNAME', 'user'),
        'password' => env('DB_PASSWORD', 'password'),
        'charset' => 'utf8',
        'prefix' => '',
        'prefix_indexes' => true,
        'search_path' => 'public',
        'sslmode' => 'prefer',
        // Cấu hình connection pool
        'pool' => [
            'min_connections' => 5,    // Số kết nối tối thiểu
            'max_connections' => 10,   // Số kết nối tối đa
            'idle_timeout' => 30,      // Thời gian timeout (giây)
        ],
    ],
],

Ví dụ với Node.js và pg-pool:

const { Pool } = require("pg");

const pool = new Pool({
  user: "user",
  host: "localhost",
  database: "mydb",
  password: "password",
  port: 5432,
  max: 20, // maximum connections
  idleTimeoutMillis: 30000, // close idle connections after 30 seconds
  connectionTimeoutMillis: 2000, // return an error after 2 seconds if connection could not be established
});

Monitoring Connection Pool

Giám sát connection pool là rất quan trọng để phát hiện vấn đề:

-- PostgreSQL: Kiểm tra số lượng kết nối hiện tại
SELECT count(*), state FROM pg_stat_activity GROUP BY state;

-- MySQL: Kiểm tra số lượng kết nối hiện tại
SHOW STATUS WHERE Variable_name = 'Threads_connected';
SHOW STATUS WHERE Variable_name = 'Max_used_connections';

Các vấn đề phổ biến:

  1. Connection leaks: Kết nối không được trả về pool

    • Giải pháp: Sử dụng try-finally hoặc try-with-resources để đảm bảo đóng kết nối
  2. Pool exhaustion: Tất cả kết nối đều đang được sử dụng

    • Giải pháp: Tăng kích thước pool hoặc giảm thời gian giữ kết nối
  3. Connection thrashing: Tạo và đóng kết nối liên tục

    • Giải pháp: Tăng min connections và idle timeout

Distributed Transactions

Distributed transaction là transaction trải rộng trên nhiều database hoặc service.


  sequenceDiagram
  participant TransactionCoordinator
  participant Database1
  participant Database2
  participant Database3
  TransactionCoordinator->>Database1: Query
  TransactionCoordinator->>Database2: Query
  TransactionCoordinator->>Database3: Query

Two-Phase Commit (2PC)

Two-Phase Commit là protocol phổ biến để đảm bảo tính nhất quán trong distributed transactions:


  sequenceDiagram
  participant C as Coordinator
  participant DB1 as Database 1
  participant DB2 as Database 2

  C->>DB1: Prepare
  C->>DB2: Prepare
  DB1->>C: Ready
  DB2->>C: Ready
  C->>DB1: Commit
  C->>DB2: Commit
  DB1->>C: Done
  DB2->>C: Done
  1. Phase 1 (Prepare): Coordinator yêu cầu mỗi participant chuẩn bị commit
  2. Phase 2 (Commit/Rollback): Nếu tất cả đồng ý, coordinator yêu cầu commit; nếu không, yêu cầu rollback

Ví dụ với PostgreSQL:

-- Node 1
BEGIN;
-- Thực hiện các thao tác
PREPARE TRANSACTION 'tx1';

-- Node 2
BEGIN;
-- Thực hiện các thao tác
PREPARE TRANSACTION 'tx1';

-- Coordinator
-- Nếu tất cả nodes đã sẵn sàng
COMMIT PREPARED 'tx1'; -- trên tất cả nodes
-- Nếu có lỗi
ROLLBACK PREPARED 'tx1'; -- trên tất cả nodes

Nhược điểm của 2PC:

  • Blocking protocol: Participants phải chờ quyết định từ coordinator
  • Single point of failure: Nếu coordinator gặp sự cố
  • Hiệu năng kém do overhead của coordination

Saga Pattern

Saga là một chuỗi các transaction cục bộ, mỗi transaction cập nhật dữ liệu trong một service và phát ra event để kích hoạt transaction tiếp theo.


  sequenceDiagram
  participant OS as Order Service
  participant IS as Inventory Service
  participant PS as Payment Service

  OS->>OS: Create Order (T1)
  OS->>IS: Reserve Inventory Event
  IS->>IS: Reserve Items (T2)
  IS->>PS: Process Payment Event
  PS->>PS: Process Payment (T3)
  PS->>OS: Payment Successful Event
  OS->>OS: Complete Order (T4)

  PS-->>OS: Payment Failed Event
  OS-->>IS: Cancel Reservation Event
  IS-->>IS: Compensating Transaction (C2)
  OS-->>OS: Cancel Order (C1)

Mỗi transaction cục bộ có một compensating transaction tương ứng để rollback thay đổi nếu cần:

// Laravel Saga Pattern Implementation
class OrderSaga
{
 protected $orderService;
 protected $inventoryService;
 protected $paymentService;

 public function __construct(
     OrderService $orderService,
     InventoryService $inventoryService,
     PaymentService $paymentService
 ) {
     $this->orderService = $orderService;
     $this->inventoryService = $inventoryService;
     $this->paymentService = $paymentService;
 }

 public function execute(int $customerId, array $items, float $amount)
 {
     try {
         // T1: Create Order
         Log::info('Starting transaction: Create Order');
         $orderId = $this->orderService->createOrder($customerId, $items);

         try {
             // T2: Reserve Inventory
             Log::info("Starting transaction: Reserve Inventory for Order {$orderId}");
             $this->inventoryService->reserveItems($orderId, $items);

             try {
                 // T3: Process Payment
                 Log::info("Starting transaction: Process Payment for Order {$orderId}");
                 $this->paymentService->processPayment($orderId, $amount);

                 // T4: Complete Order
                 Log::info("Starting transaction: Complete Order {$orderId}");
                 $this->orderService->completeOrder($orderId);

                 Log::info("Saga completed successfully for Order {$orderId}");
                 return $orderId;
             } catch (\Exception $e) {
                 // C3: Compensating transaction - Refund Payment
                 Log::warning("Payment failed for Order {$orderId}. Executing compensation: Refund");
                 $this->paymentService->refundPayment($orderId);
                 throw $e;
             }
         } catch (\Exception $e) {
             // C2: Compensating transaction - Release Inventory
             Log::warning("Inventory reservation failed for Order {$orderId}. Executing compensation: Release Items");
             $this->inventoryService->releaseItems($orderId);
             throw $e;
         }
     } catch (\Exception $e) {
         // C1: Compensating transaction - Cancel Order
         Log::warning("Order process failed. Executing compensation: Cancel Order");
         if (isset($orderId)) {
             $this->orderService->cancelOrder($orderId);
         }

         Log::error('Saga failed: ' . $e->getMessage());
         throw new OrderProcessingException('Failed to process order: ' . $e->getMessage(), 0, $e);
     }
 }
}

// Usage in a controller
class OrderController extends Controller
{
 protected $orderSaga;

 public function __construct(OrderSaga $orderSaga)
 {
     $this->orderSaga = $orderSaga;
 }

 public function placeOrder(Request $request)
 {
     try {
         $orderId = $this->orderSaga->execute(
             $request->user()->id,
             $request->input('items'),
             $request->input('amount')
         );

         return response()->json([
             'success' => true,
             'order_id' => $orderId
         ]);
     } catch (OrderProcessingException $e) {
         return response()->json([
             'success' => false,
             'message' => $e->getMessage()
         ], 422);
     }
 }
}

Ưu điểm của Saga:

  • Không blocking, phù hợp cho microservices
  • Mỗi service duy trì tính nhất quán cục bộ
  • Khả năng mở rộng tốt hơn

Nhược điểm:

  • Phức tạp trong triển khai và debug
  • Eventual consistency thay vì strong consistency
  • Cần xử lý compensating transactions

Các công cụ và frameworks

  1. Laravel DB Transactions: Quản lý transaction trong Laravel

    // Chuyển tiền giữa các tài khoản
    public function transferMoney($fromAccountId, $toAccountId, $amount)
    {
        return DB::transaction(function () use ($fromAccountId, $toAccountId, $amount) {
            $fromAccount = Account::lockForUpdate()->findOrFail($fromAccountId);
            $toAccount = Account::lockForUpdate()->findOrFail($toAccountId);
    
            $fromAccount->balance -= $amount;
            $toAccount->balance += $amount;
    
            $fromAccount->save();
            $toAccount->save();
    
            return true;
        });
    }
    
  2. Laravel Transaction với Isolation Levels:

    // Đặt hàng với transaction
    public function placeOrder(array $orderData)
    {
        // Thiết lập isolation level (PostgreSQL)
        DB::statement('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
    
        return DB::transaction(function () use ($orderData) {
            // Tạo đơn hàng
            $order = Order::create([
                'user_id' => $orderData['user_id'],
                'total' => $orderData['total'],
                'status' => 'pending'
            ]);
    
            // Giảm số lượng trong kho
            foreach ($orderData['items'] as $item) {
                $inventory = Inventory::lockForUpdate()->findOrFail($item['inventory_id']);
                if ($inventory->quantity < $item['quantity']) {
                    throw new Exception('Không đủ hàng trong kho');
                }
    
                $inventory->quantity -= $item['quantity'];
                $inventory->save();
    
                // Tạo chi tiết đơn hàng
                OrderItem::create([
                    'order_id' => $order->id,
                    'product_id' => $item['product_id'],
                    'quantity' => $item['quantity'],
                    'price' => $item['price']
                ]);
            }
    
            // Xử lý thanh toán
            $payment = Payment::create([
                'order_id' => $order->id,
                'amount' => $orderData['total'],
                'status' => 'completed'
            ]);
    
            // Cập nhật trạng thái đơn hàng
            $order->status = 'confirmed';
            $order->save();
    
            return $order;
        }, 3); // Retry 3 times if deadlock occurs
    }
    
  3. Atomikos, Bitronix: JTA implementations

  4. Narayana: Transaction manager từ JBoss

  5. Eventuate Tram: Framework cho Saga pattern

Case Study: Tối ưu concurrency trong hệ thống đặt vé

Hãy xem xét một case study về tối ưu concurrency trong hệ thống đặt vé sự kiện:

Vấn đề ban đầu

Một hệ thống đặt vé online đang gặp các vấn đề:

  • Double booking: Nhiều người dùng đặt cùng một ghế
  • Deadlocks khi nhiều người dùng đặt vé đồng thời
  • Hiệu năng kém khi có flash sale hoặc sự kiện hot
  • Timeout khi xử lý thanh toán

Schema hiện tại:

CREATE TABLE events (
 id INT PRIMARY KEY,
 name VARCHAR(255),
 venue_id INT,
 event_date TIMESTAMP,
 total_seats INT,
 available_seats INT
);

CREATE TABLE seats (
 id INT PRIMARY KEY,
 event_id INT,
 seat_number VARCHAR(10),
 section VARCHAR(50),
 price DECIMAL(10,2),
 status VARCHAR(20) -- 'available', 'reserved', 'booked'
);

CREATE TABLE reservations (
 id INT PRIMARY KEY,
 user_id INT,
 event_id INT,
 seat_id INT,
 status VARCHAR(20), -- 'pending', 'confirmed', 'cancelled'
 created_at TIMESTAMP,
 expires_at TIMESTAMP
);

Phân tích vấn đề

  1. Race conditions khi đặt vé:

    • Nhiều người dùng có thể đồng thời thấy ghế còn trống và cố gắng đặt
    • Thiếu locking strategy phù hợp
  2. Deadlocks khi đặt nhiều ghế:

    • Người dùng A đặt ghế 1, 2, 3
    • Người dùng B đặt ghế 3, 4, 5
    • Có thể dẫn đến deadlock nếu A đã lock ghế 1, 2 và B đã lock ghế 4, 5
  3. Connection pool exhaustion:

    • Trong thời điểm cao điểm, số lượng kết nối vượt quá giới hạn
    • Transactions kéo dài do xử lý thanh toán

Giải pháp tối ưu

1. Pessimistic Locking cho đặt vé

-- Trong một transaction
BEGIN;

-- Lock ghế cần đặt
SELECT * FROM seats
WHERE event_id = 123 AND seat_number IN ('A1', 'A2') AND status = 'available'
FOR UPDATE SKIP LOCKED;

-- Nếu số lượng rows trả về khớp với số ghế cần đặt, tiến hành đặt
UPDATE seats
SET status = 'reserved'
WHERE event_id = 123 AND seat_number IN ('A1', 'A2') AND status = 'available';

-- Tạo reservation
INSERT INTO reservations (user_id, event_id, seat_id, status, created_at, expires_at)
VALUES
(456, 123, (SELECT id FROM seats WHERE event_id = 123 AND seat_number = 'A1'), 'pending', NOW(), NOW() + INTERVAL '15 minutes'),
(456, 123, (SELECT id FROM seats WHERE event_id = 123 AND seat_number = 'A2'), 'pending', NOW(), NOW() + INTERVAL '15 minutes');

-- Cập nhật số lượng ghế còn trống
UPDATE events
SET available_seats = available_seats - 2
WHERE id = 123;

COMMIT;

2. Tránh deadlocks bằng cách sắp xếp thứ tự lock

-- Luôn lock ghế theo thứ tự seat_number để tránh deadlock
SELECT * FROM seats
WHERE event_id = 123 AND seat_number IN ('A1', 'A2', 'A3')
AND status = 'available'
ORDER BY seat_number  -- Quan trọng: luôn lock theo cùng một thứ tự
FOR UPDATE SKIP LOCKED;

3. Tách biệt reservation và payment


  sequenceDiagram
  participant U as User
  participant S as Seat Selection Service
  participant D as Database
  participant P as Payment Service

  U->>S: 1. Select Seats
  S->>D: 2. Reserve Seats<br/>Short Transaction
  U->>P: 3. Process Payment
  P->>D: 4. Confirm Reservation<br/>Separate Transaction