zalo-icon
facebook-icon
phone-icon
Transaction trong PostgreSQL: ACID, Isolation và xử lý đơn hàng an toàn

Một đơn hàng không thể ở trạng thái đã thanh toán nếu kho chưa trừ hoặc giao dịch tiền thất bại. Transaction tồn tại để nhiều thay đổi liên quan cùng thành công hoặc cùng quay lại.

Bài viết xây quy trình đặt hàng an toàn khi hai người cùng mua sản phẩm cuối cùng. Thay vì dừng ở bốn chữ ACID, nội dung đi vào isolation, row lock, deadlock, retry và cách kiểm thử bằng hai phiên PostgreSQL.

Bốn nguyên tắc ACID trong PostgreSQL

ACID dưới góc nhìn một đơn hàng

Atomicity bảo đảm trừ kho, tạo đơn và ghi thanh toán là một đơn vị. Consistency giữ các constraint đúng trước và sau giao dịch. Isolation kiểm soát cách giao dịch đồng thời nhìn thấy nhau. Durability bảo đảm dữ liệu đã commit sống sót sau sự cố trong giới hạn cấu hình bền vững.

ACID không thay thế thiết kế nghiệp vụ. Database chỉ bảo vệ những quy tắc đã được biểu diễn bằng constraint, transaction và câu lệnh phù hợp.

COMMIT và ROLLBACK trong PostgreSQL

BEGIN, COMMIT và ROLLBACK

BEGIN;
INSERT INTO sales.orders(customer_id,status) VALUES (1,'pending') RETURNING order_id;
INSERT INTO sales.order_items(order_id,product_id,quantity,unit_price)
VALUES (1001,1,2,1500000);
UPDATE catalog.products SET stock_quantity=stock_quantity-2 WHERE product_id=1;
COMMIT;

Nếu bất kỳ lệnh nào lỗi, transaction chuyển sang trạng thái aborted và phải rollback. Không nên bắt lỗi rồi tiếp tục coi như dữ liệu đã an toàn.

Ngăn bán vượt tồn kho bằng cập nhật nguyên tử

Bài toán overselling và lost update

Hai phiên cùng đọc tồn kho bằng 1, cùng kết luận đủ hàng rồi cùng trừ có thể bán quá số lượng. Cách đọc rồi cập nhật tách rời trong ứng dụng tạo khoảng trống cạnh tranh.

UPDATE catalog.products
SET stock_quantity=stock_quantity-1
WHERE product_id=1 AND stock_quantity >= 1
RETURNING stock_quantity;

Một câu UPDATE có điều kiện là thao tác nguyên tử. Nếu không trả về dòng, tồn kho không đủ. Đây thường là giải pháp đơn giản hơn đọc rồi khóa thủ công.

Row lock và isolation trong PostgreSQL

SELECT FOR UPDATE khi cần đọc rồi quyết định

BEGIN;
SELECT stock_quantity
FROM catalog.products
WHERE product_id=1
FOR UPDATE;
-- kiểm tra quy tắc phức tạp, sau đó cập nhật
UPDATE catalog.products SET stock_quantity=stock_quantity-1 WHERE product_id=1;
COMMIT;

Row lock chặn phiên khác sửa cùng bản ghi cho đến khi transaction kết thúc. Transaction phải ngắn; không giữ lock trong lúc chờ người dùng hoặc gọi API bên ngoài.

Isolation level và hiện tượng đọc đồng thời

PostgreSQL mặc định READ COMMITTED: mỗi câu lệnh thấy snapshot mới tại thời điểm bắt đầu câu lệnh. REPEATABLE READ giữ snapshot xuyên suốt transaction. SERIALIZABLE cố tạo kết quả tương đương chạy tuần tự và có thể buộc một transaction retry.

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- nghiệp vụ cần tính nhất quán trên nhiều dòng
COMMIT;

Isolation cao hơn không có nghĩa luôn tốt hơn. Nó tăng bảo vệ nhưng cũng tăng khả năng chờ, xung đột và retry.

Deadlock retry và idempotency trong PostgreSQL

Deadlock xảy ra thế nào và cách phòng tránh

Nếu phiên A khóa sản phẩm 1 rồi chờ sản phẩm 2, trong khi phiên B khóa sản phẩm 2 rồi chờ sản phẩm 1, PostgreSQL phát hiện vòng chờ và hủy một transaction. Đây là cơ chế bảo vệ, không phải database bị treo.

Phòng tránh bằng cách khóa tài nguyên theo cùng thứ tự, giữ transaction ngắn và retry giao dịch bị chọn làm nạn nhân. Không thể cam kết “không bao giờ deadlock” trong hệ thống đồng thời phức tạp.

SAVEPOINT cho lỗi cục bộ có kiểm soát

BEGIN;
INSERT INTO sales.orders(customer_id,status) VALUES (1,'pending');
SAVEPOINT before_coupon;
-- thử áp mã giảm giá
ROLLBACK TO SAVEPOINT before_coupon;
-- đơn hàng vẫn tồn tại trong transaction
COMMIT;

Savepoint hữu ích khi một phần tùy chọn có thể thất bại mà giao dịch chính vẫn hợp lệ. Lạm dụng savepoint dễ che giấu lỗi nghiệp vụ và làm luồng khó hiểu.

Idempotency khi ứng dụng phải retry

Mạng có thể ngắt sau khi database commit nhưng trước khi ứng dụng nhận phản hồi. Nếu gửi lại yêu cầu mà không có idempotency key, hệ thống có thể tạo hai đơn.

ALTER TABLE sales.orders ADD COLUMN idempotency_key uuid;
CREATE UNIQUE INDEX uq_orders_idempotency
ON sales.orders(idempotency_key) WHERE idempotency_key IS NOT NULL;

Ứng dụng dùng cùng khóa cho cùng một yêu cầu logic. Unique constraint biến retry thành thao tác an toàn thay vì nhân đôi giao dịch.

Kiểm thử đồng thời bằng hai phiên psql

Mở hai cửa sổ psql, bắt đầu transaction và thử cập nhật cùng sản phẩm. Quan sát phiên thứ hai chờ, sau đó commit hoặc rollback phiên thứ nhất. Kiểm tra pg_stat_activity và pg_locks để thấy quan hệ chặn.

SELECT pid, state, wait_event_type, wait_event, query
FROM pg_stat_activity WHERE datname=current_database();

Expected output cần ghi rõ tồn kho cuối, số đơn tạo ra và transaction nào bị retry.

Checklist transaction cho production

Đặt timeout hợp lý, không gọi dịch vụ mạng khi đang giữ lock, xử lý serialization failure và deadlock bằng retry có backoff, dùng unique constraint cho idempotency, log transaction ID và thời gian chờ lock. Pool kết nối cũng cần giới hạn để tránh quá tải database.

Quan trọng nhất là xác định ranh giới transaction theo nghiệp vụ, không theo số dòng code. Một transaction nên bao trọn những thay đổi phải nhất quán và không nhiều hơn.

Kết luận

Transaction là cơ chế cốt lõi để dữ liệu vẫn đúng khi lỗi và đồng thời xảy ra. Một thiết kế tốt kết hợp câu lệnh nguyên tử, khóa có chủ đích, isolation phù hợp, idempotency và retry; không dựa vào hy vọng rằng các yêu cầu sẽ luôn chạy lần lượt.

TechData.AI - Leading the Future.
Tham khảo các khoá học theo link: https://techdata.ai/techdata-ai-course/
Hoàng Minh.

Scroll to Top