Appearance
PostgreSQL — Locking & Concurrency
1. Row-level Locks
Có 4 loại, từ nhẹ đến nặng:
| Lock | TypeORM mode | Ý nghĩa |
|---|---|---|
FOR KEY SHARE | pessimistic_key_share | Nhẹ nhất — chặn xóa/update primary key |
FOR SHARE | pessimistic_read | Chặn UPDATE/DELETE, cho phép đọc đồng thời |
FOR NO KEY UPDATE | pessimistic_partial_write | Chặn UPDATE/DELETE, vẫn cho FOR KEY SHARE |
FOR UPDATE | pessimistic_write | Nặng nhất — chặn tất cả trừ SELECT thường |
Compatibility matrix
| FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE | |
|---|---|---|---|---|
| FOR KEY SHARE | ✅ | ✅ | ✅ | ❌ |
| FOR SHARE | ✅ | ✅ | ❌ | ❌ |
| FOR NO KEY UPDATE | ✅ | ❌ | ❌ | ❌ |
| FOR UPDATE | ❌ | ❌ | ❌ | ❌ |
✅ = 2 transaction cùng tồn tại được, ❌ = bị treo.
SELECT thường không bị chặn bởi bất kỳ row lock nào.
FOR KEY SHARE
Chặn DELETE và UPDATE primary key, cho phép UPDATE column thường.
sql
SELECT * FROM users WHERE id = 1 FOR KEY SHARE;PostgreSQL tự động apply khi INSERT/UPDATE có foreign key:
sql
-- Bạn viết:
INSERT INTO orders (user_id) VALUES (1);
-- PostgreSQL tự làm ngầm:
SELECT id FROM users WHERE id = 1 FOR KEY SHARE;
INSERT INTO orders (user_id) VALUES (1);Dev ít khi tự dùng — chủ yếu PostgreSQL tự apply để bảo vệ foreign key integrity.
FOR SHARE
Cho phép nhiều transaction cùng đọc, chặn UPDATE/DELETE.
sql
SELECT * FROM users WHERE id = 1 FOR SHARE;Dùng khi cần đọc data để tính toán, đảm bảo không ai thay đổi trong lúc tính. Dev tự dùng, PostgreSQL không tự apply.
FOR NO KEY UPDATE
Giống FOR UPDATE nhưng nhẹ hơn 1 bậc — vẫn cho FOR KEY SHARE chạy được.
sql
SELECT * FROM users WHERE id = 1 FOR NO KEY UPDATE;Dùng khi update column thường nhưng vẫn muốn các INSERT vào bảng con (foreign key) chạy được song song.
FOR UPDATE
Nặng nhất — chặn tất cả trừ SELECT thường.
sql
SELECT * FROM users WHERE id = 1 FOR UPDATE;Dùng khi cần đọc rồi ghi, đảm bảo không ai chen vào giữa.
Dùng trong TypeORM
Bắt buộc phải nằm trong transaction, nếu không lock vô nghĩa vì release ngay sau SELECT.
typescript
// ✅ Đúng — lock nằm trong transaction
await dataSource.transaction(async (manager) => {
const user = await manager.findOne(User, {
where: { id: 1 },
lock: { mode: "pessimistic_write" }, // FOR UPDATE
});
user.tier = "gold";
await manager.save(user);
});
// ❌ Sai — transaction kết thúc ngay sau findOne, lock release luôn
const user = await userRepo.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write" },
});
user.tier = "gold";
await userRepo.save(user); // lock đã release từ lúc nãy2. Table-level Locks
PostgreSQL tự động apply, dev không cần tự lock thủ công.
3 mức quan trọng nhất
ACCESS SHARE — nhẹ nhất
sql
SELECT * FROM users;
-- tự động apply ACCESS SHARE
-- compatible với tất cả trừ ACCESS EXCLUSIVEROW EXCLUSIVE — hay gặp nhất
sql
INSERT / UPDATE / DELETE
-- tự động apply ROW EXCLUSIVEACCESS EXCLUSIVE — nặng nhất
sql
ALTER TABLE / DROP TABLE / TRUNCATE
-- conflict với TẤT CẢ lock khác kể cả ACCESS SHARE (SELECT thường)Lock queuing — nguy hiểm nhất
Session A: SELECT (ACCESS SHARE) → đang chạy
Session B: ALTER TABLE (ACCESS EXCLUSIVE) → chờ A
Session C: SELECT (ACCESS SHARE) → chờ B ← dù compatible với A!
Session D: SELECT → chờ B
... tất cả xếp hàng sau B1 ALTER TABLE trên production có thể:
chờ long-running transaction
→ block toàn bộ request mới
→ connection pool đầy
→ hệ thống đứngCách phòng tránh:
sql
SET lock_timeout = '2s';
ALTER TABLE users ADD COLUMN phone2 text;
-- tự hủy nếu chờ quá 2s, không block tiếp3. Deadlock
Xảy ra khi 2 transaction chờ nhau mãi mãi:
Transaction A giữ lock row 1, chờ row 2
Transaction B giữ lock row 2, chờ row 1
→ cả 2 chờ nhau → không ai tiến đượcPostgreSQL tự phát hiện và xử lý
Tự chọn 1 transaction làm victim → rollback → transaction kia tiếp tục.
ERROR 40P01: deadlock detected
Detail: Process 1234 waits for ShareLock on transaction 5678
Process 5678 waits for ShareLock on transaction 1234Cách phòng tránh
Luôn lock theo cùng thứ tự:
typescript
// ❌ Nguy hiểm — 2 transaction lock ngược thứ tự nhau
// Transaction A: lock id=1 trước, rồi id=2
// Transaction B: lock id=2 trước, rồi id=1
// ✅ An toàn — luôn lock theo thứ tự id tăng dần
const ids = [1, 2].sort((a, b) => a - b);
for (const id of ids) {
await manager.findOne(User, {
where: { id },
lock: { mode: "pessimistic_write" },
});
}Lock tất cả row cần dùng ngay từ đầu transaction:
sql
BEGIN;
SELECT * FROM users WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- ORDER BY id đảm bảo luôn lock theo cùng thứ tự
UPDATE users SET tier = 'gold' WHERE id = 1;
UPDATE users SET tier = 'silver' WHERE id = 2;
COMMIT;4. Monitoring
sql
-- Xem các lock đang active
SELECT pid, locktype, relation::regclass, mode, granted
FROM pg_locks
WHERE relation IS NOT NULL;
-- Xem ai đang block ai
SELECT blocked.pid, blocked.query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
-- Kill connection bị treo
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - query_start > interval '5 minutes';5. Tóm tắt — khi nào dùng gì
| Tình huống | Nên dùng |
|---|---|
| Đọc rồi ghi, không muốn ai chen vào | FOR UPDATE |
| Đọc để tính toán, không muốn data thay đổi | FOR SHARE |
| Update column thường, vẫn cho insert bảng con | FOR NO KEY UPDATE |
| Bảo vệ foreign key | PostgreSQL tự apply FOR KEY SHARE |
| ALTER TABLE trên production | SET lock_timeout trước |
| Deadlock | Lock theo cùng thứ tự, FOR UPDATE sớm ngay đầu transaction |