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 khai báo lock) không bị chặn bởi bất kỳ row lock nào, và cũng không tự sinh ra FOR KEY SHARE nào cả. FOR KEY SHARE chỉ phát sinh ngầm khi PostgreSQL cần check foreign key constraint (xem phần dưới).
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.
Ví dụ thực tế — kiểm tra giá trước khi tạo order:
sql
-- Service A: đọc giá, KHÔNG có ý định sửa, chỉ cần đảm bảo giá không đổi
-- trong lúc đang tính tổng tiền order
BEGIN;
SELECT price, stock FROM products WHERE id = 10 FOR SHARE;
-- ... tính tổng tiền, validate, ghi order ...
COMMIT;sql
-- Service A2 đọc cùng row, FOR SHARE → không bị chặn (đọc đồng thời OK)
BEGIN;
SELECT price FROM products WHERE id = 10 FOR SHARE;
COMMIT; -- ✓ chạy ngay
-- Service B (admin) muốn đổi giá → phải chờ A, A2 commit
BEGIN;
UPDATE products SET price = 200000 WHERE id = 10;
-- ⏳ chờ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 NO KEY UPDATE vs FOR UPDATE — khác nhau ở đâu?
Cả hai đều do dev chủ động gọi khi sắp update row, và cả hai đều mang nghĩa "tôi sắp sửa row này". Khác biệt chỉ lộ ra khi có table khác foreign-key reference tới table đang lock.
Trường hợp KHÔNG có FK liên quan: FOR NO KEY UPDATE và FOR UPDATE hoạt động giống hệt nhau, không ai phân biệt được, vì không có ai cần FOR KEY SHARE để compatible.
Trường hợp CÓ FK liên quan — đây là lúc khác biệt xuất hiện:
sql
-- accounts(id) được transactions(account_id) reference (FK)
-- TH1: Session A dùng FOR UPDATE
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- chưa commit
-- Session B insert transaction mới
-- PostgreSQL tự FOR KEY SHARE accounts.id=1 để check FK còn tồn tại
INSERT INTO transactions (account_id, amount) VALUES (1, -50);
-- ⏳ BỊ CHẶN — FOR UPDATE conflict với FOR KEY SHAREsql
-- TH2: Session A dùng FOR NO KEY UPDATE thay vì FOR UPDATE
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR NO KEY UPDATE;
-- chưa commit
-- Session B insert transaction mới
INSERT INTO transactions (account_id, amount) VALUES (1, -50);
-- ✓ KHÔNG BỊ CHẶN — FOR NO KEY UPDATE compatible với FOR KEY SHAREKết luận thực dụng:
| Câu hỏi | Trả lời |
|---|---|
| Table đang lock có bị table khác FK reference tới không? | Không → dùng FOR UPDATE cho đơn giản, không có downside |
Có → cân nhắc FOR NO KEY UPDATE nếu muốn các INSERT/FK-check khác chạy song song, không bị chặn bởi lock của bạn |
Nói cách khác: FOR NO KEY UPDATE chỉ có ý nghĩa thực tiễn khi table đang lock là "cha" trong quan hệ FK và bạn muốn giảm tối đa việc block các transaction khác đang chỉ cần insert/check FK.
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.
Ví dụ thực tế — pattern rút tiền điển hình:
sql
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- giả sử check balance >= 100 trong app code
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;Trong lúc đó, mọi session khác — kể cả chỉ muốn FOR SHARE để đọc — đều phải chờ:
sql
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR SHARE;
-- ⏳ phải chờ transaction trên COMMIT/ROLLBACKDù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ãy4 mode tương ứng:
typescript
// FOR KEY SHARE — PostgreSQL thường tự apply, ít khi dev gọi tay
await manager.findOne(Account, {
where: { id },
lock: { mode: "pessimistic_key_share" },
});
// FOR SHARE — đọc để tính toán, không sửa, cấm người khác sửa
await manager.findOne(Account, {
where: { id },
lock: { mode: "pessimistic_read" },
});
// FOR NO KEY UPDATE — sắp update, không đổi key, muốn giảm conflict với FK
await manager.findOne(Account, {
where: { id },
lock: { mode: "pessimistic_partial_write" },
});
// FOR UPDATE — sắp update/delete, lock chặt nhất
await manager.findOne(Account, {
where: { id },
lock: { mode: "pessimistic_write" },
});2. Lock tự sinh bởi INSERT / UPDATE / DELETE
Không cần khai báo gì thêm — PostgreSQL tự sinh row lock khi thực thi các lệnh này.
INSERT
sql
INSERT INTO orders (user_id, total) VALUES (1, 500000);Không lock row vừa insert (chưa ai biết row đó tồn tại). Nhưng nếu có FK, PostgreSQL tự sinh FOR KEY SHARE lên row cha để check FK còn tồn tại:
sql
-- orders.user_id FK tới users.id
-- PostgreSQL tự làm ngầm:
SELECT id FROM users WHERE id = 1 FOR KEY SHARE;
INSERT INTO orders (user_id, total) VALUES (1, 500000);UPDATE
sql
UPDATE accounts SET balance = balance - 100 WHERE id = 1;PostgreSQL tự sinh FOR NO KEY UPDATE lên row bị update — đủ để chặn UPDATE/DELETE khác, nhưng vẫn cho FOR KEY SHARE (insert bảng con có FK) chạy được.
Nếu UPDATE đổi luôn primary key/unique key → tự nâng lên FOR UPDATE (vì key thay đổi, phải chặn cả FK-check).
DELETE
sql
DELETE FROM accounts WHERE id = 1;PostgreSQL tự sinh FOR UPDATE — nặng nhất, vì xóa row thì key biến mất, phải chặn tất cả kể cả FK-check.
Tóm tắt lock tự sinh
| Lệnh | Lock tự sinh | Lý do |
|---|---|---|
INSERT | Không lock row mới. FOR KEY SHARE lên row cha (nếu có FK) | Row mới chưa ai thấy, chỉ cần verify FK |
UPDATE (không đổi key) | FOR NO KEY UPDATE | Chặn update/delete khác, vẫn cho FK-check |
UPDATE (đổi primary/unique key) | FOR UPDATE | Key thay đổi, phải chặn cả FK-check |
DELETE | FOR UPDATE | Key biến mất, phải chặn tất cả |
Hành vi khi gặp conflict
Mặc định khi UPDATE/DELETE gặp row đang bị lock bởi transaction khác → chờ vô thời hạn cho tới khi transaction kia COMMIT hoặc ROLLBACK.
Có 2 cách thay đổi hành vi này:
NOWAIT — báo lỗi ngay lập tức thay vì chờ:
sql
-- SQL thuần
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- → ERROR 55P03: could not obtain lock on row in relation "accounts"
-- TypeORM
await manager.findOne(Account, {
where: { id },
lock: { mode: "pessimistic_write", onLocked: "nowait" },
});Dùng khi: không muốn request bị treo, thà báo lỗi để retry ngay còn hơn chờ không biết bao lâu.
SKIP LOCKED — bỏ qua các row đang bị lock, chỉ lấy row available:
sql
-- SQL thuần
SELECT * FROM jobs WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
-- TypeORM
await manager.findOne(Job, {
where: { status: "pending" },
order: { createdAt: "ASC" },
lock: { mode: "pessimistic_write", onLocked: "skip_locked" },
});Dùng khi: queue processing với nhiều worker — mỗi worker lấy 1 job available, không tranh nhau cùng 1 row.
Worker 1: lấy job_id=1 (FOR UPDATE) → đang xử lý
Worker 2: SKIP LOCKED → bỏ qua job_id=1, lấy job_id=2 ngay
Worker 3: SKIP LOCKED → bỏ qua 1,2, lấy job_id=3 ngay
→ không worker nào phải chờ worker khácSo sánh 3 hành vi:
| Mode | Hành vi khi gặp locked row | Dùng khi |
|---|---|---|
| Mặc định | Chờ đến khi unlock | Cần đúng row đó, chấp nhận chờ |
NOWAIT | Báo lỗi ngay | Không muốn chờ, tự retry trong app |
SKIP LOCKED | Bỏ qua, lấy row khác | Queue processing, nhiều worker song song |
3. 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ếp4. 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;5. 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';6. 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 |
| Không muốn chờ lock, thà retry ngay | FOR UPDATE NOWAIT |
| Queue processing, nhiều worker song song | FOR UPDATE SKIP LOCKED |
| ALTER TABLE trên production | SET lock_timeout trước |
| Deadlock | Lock theo cùng thứ tự, FOR UPDATE sớm ngay đầu transaction |