Khi nhiều request cùng đọc → quyết định → ghi trên cùng row, race condition rất dễ xảy ra — trừ stock, charge ví, đặt ghế là 3 ví dụ kinh điển. select_for_update() phát SELECT ... FOR UPDATE → khoá hàng đó bên trong transaction đến khi commit/rollback.
Request khác cố đụng cùng row phải đứng chờ.
from django.db import transaction
def reserve_seat(seat_id, user):
with transaction.atomic():
seat = Seat.objects.select_for_update().get(id=seat_id)
if seat.holder_id is not None:
raise ValueError('Seat already taken')
seat.holder = user
seat.save()Không có lock thì 2 request đọc thấy holder = None cùng lúc → cả 2 cùng set → 1 bị ghi đè. Lỗi này lặng lẽ, không exception, gần như không debug được nếu không có log chi tiết.
Thêm 3 option hay dùng: nowait=True không chờ mà raise DatabaseError ngay; skip_locked=True bỏ qua row đang khoá (hợp với queue kiểu "worker nào grab job trước thì xử lý"); of=('self',) chỉ khoá bảng chính khi có JOIN, đỡ khoá lan sang bảng liên quan.
Điểm dễ vấp: select_for_update() bắt buộc nằm trong transaction.atomic() (ngoài atomic là Django raise lỗi luôn), và SQLite không hỗ trợ. Lock chỉ release khi transaction kết thúc — nếu giữa atomic block bạn gọi API ngoài hay sleep thì lock kéo dài theo, mọi user khác phải xếp hàng. Cách phòng tốt nhất là giữ atomic block thật ngắn, chỉ làm đúng phần cần lock rồi commit.