PostgreSQL cài REPEATABLE READ bằng snapshot isolation: transaction lấy một snapshot tại câu lệnh đầu tiên và nhìn thấy nguyên trạng đó tới cuối.
Khi bạn UPDATE một hàng mà transaction khác đã commit thay đổi sau snapshot ấy, database không có cách nào ghi đúng theo cả hai góc nhìn nên nó huỷ transaction của bạn:
ERROR: could not serialize access due to concurrent update
SQLSTATE 40001Đây không phải lỗi hệ thống mà là tín hiệu hãy chạy lại. Khác biệt lớn so với READ COMMITTED: ở mức mặc định, UPDATE gặp hàng đang bị sửa sẽ chờ rồi áp dụng lên bản mới nhất, không báo lỗi (đổi lại có thể mất update nếu logic dựa trên giá trị đã đọc).
Ứng dụng bắt buộc có vòng retry:
async function withRetry<T>(run: () => Promise<T>, max = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await run()
} catch (err) {
// 40001 serialization failure, 40P01 deadlock detected
const code = (err as { code?: string }).code
if ((code !== '40001' && code !== '40P01') || attempt >= max) throw err
await sleep(50 * 2 ** attempt)
}
}
}Ba điều kiện để retry đúng: chạy lại toàn bộ transaction từ đầu (không thể tiếp tục transaction đã abort), khối retry không được chứa tác dụng phụ ngoài database, và phải có giới hạn số lần cùng backoff để tranh chấp không lan rộng.
Cùng mã lỗi nhóm 40xxx còn có deadlock (40P01) — xử lý chung một chỗ.