Skip to content

PostgreSQL — Locking & Concurrency

1. Row-level Locks

Có 4 loại, từ nhẹ đến nặng:

LockTypeORM modeÝ nghĩa
FOR KEY SHAREpessimistic_key_shareNhẹ nhất — chặn xóa/update primary key
FOR SHAREpessimistic_readChặn UPDATE/DELETE, cho phép đọc đồng thời
FOR NO KEY UPDATEpessimistic_partial_writeChặn UPDATE/DELETE, vẫn cho FOR KEY SHARE
FOR UPDATEpessimistic_writeNặng nhất — chặn tất cả trừ SELECT thường

Compatibility matrix

FOR KEY SHAREFOR SHAREFOR NO KEY UPDATEFOR 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ãy

2. 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 EXCLUSIVE

ROW EXCLUSIVE — hay gặp nhất

sql
INSERT / UPDATE / DELETE
-- tự động apply ROW EXCLUSIVE

ACCESS 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 B

1 ALTER TABLE trên production có thể:

chờ long-running transaction
→ block toàn bộ request mới
→ connection pool đầy
→ hệ thống đứng

Cá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ếp

3. 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 được

PostgreSQL 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 1234

Cá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ốngNên dùng
Đọc rồi ghi, không muốn ai chen vàoFOR UPDATE
Đọc để tính toán, không muốn data thay đổiFOR SHARE
Update column thường, vẫn cho insert bảng conFOR NO KEY UPDATE
Bảo vệ foreign keyPostgreSQL tự apply FOR KEY SHARE
ALTER TABLE trên productionSET lock_timeout trước
DeadlockLock theo cùng thứ tự, FOR UPDATE sớm ngay đầu transaction

Personal notes by thanhlt