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
- 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.
- 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.
- Isolation (Tính độc lập): Các transaction đồng thời không ảnh hưởng lẫn nhau.
- 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
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ế
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
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
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
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ệ
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
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:
- 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
- Read Committed: Phù hợp cho hầu hết các ứng dụng OLTP
- Repeatable Read: Khi cần đảm bảo tính nhất quán cao hơn
- 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:
- Timeout: Transaction sẽ bị hủy sau một khoảng thời gian chờ đợi
- Dependency graph: Hệ thống xây dựng đồ thị phụ thuộc và phát hiện chu trình
- 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
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;
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;
Sử dụng lock timeout:
-- PostgreSQL SET lock_timeout = '5s'; -- SQL Server SET LOCK_TIMEOUT 5000; -- 5 seconds
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;
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:
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;
Exclusive Lock (X): Chỉ một transaction có thể đọc và ghi
-- MySQL/PostgreSQL SELECT * FROM products WHERE id = 1 FOR UPDATE;
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:
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
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';
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
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
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:
- 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ũ
- 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
- 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:
Separate databases:
- Write database: Normalized, optimized for consistency
- Read database: Denormalized, optimized for query performance
Event sourcing:
- Lưu trữ chuỗi các events thay vì state
- Rebuild state từ events khi cần
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:
Database-level replication:
-- MySQL: Kiểm tra replication status SHOW SLAVE STATUS\G -- PostgreSQL: Kiểm tra replication status SELECT * FROM pg_stat_replication;
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']);
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:
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
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
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:
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
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
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
- Phase 1 (Prepare): Coordinator yêu cầu mỗi participant chuẩn bị commit
- 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
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; }); }
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 }
Atomikos, Bitronix: JTA implementations
Narayana: Transaction manager từ JBoss
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 đề
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
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
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