Giả sử bạn đang code một app cho shop online, và khi thanh toán đơn hàng bạn cần phải:
- Giữ tiền từ thẻ của khách.
- Giảm số lượng hàng trong kho.
- Tạo bản ghi về transaction thành công.
Nếu giữa chừng có gì đó trục trặc thì sao? Ví dụ, hàng trong kho hết sau khi đã giữ tiền, nhưng trước khi tạo bản ghi đơn hàng? Mọi thứ sẽ rối tung: tiền thì "treo", đơn hàng chưa xong, còn server của bạn thì nhận cả đống email chửi (và có khi còn bị kiện nữa).
Transaction chính là cứu cánh cho mấy tình huống này. Nó cho phép gom nhiều thao tác thành một "khối nguyên tử" khi làm việc với database. Kiểu như nút "Undo" trong trình soạn thảo: nếu có gì sai, chỉ cần quay lại từ đầu là xong.
Transaction đảm bảo tính toàn vẹn dữ liệu như thế nào?
Transaction dựa trên nguyên lý ACID:
- Tính nguyên tử (Atomicity) — Tất cả thao tác trong transaction hoặc là chạy hết, hoặc là không có gì xảy ra. "Tất cả hoặc không gì cả".
- Tính nhất quán (Consistency) — Dữ liệu luôn ở trạng thái nhất quán trước và sau khi transaction chạy.
- Tính cô lập (Isolation) — Một transaction không làm phiền transaction khác.
- Tính bền vững (Durability) — Khi transaction hoàn thành, kết quả sẽ được lưu lại kể cả khi hệ thống bị crash.
Tại sao mình lại nhắc lại cái này? Vì đây là lý tưởng mà ai cũng muốn đạt tới. Và... thực tế thì hiếm khi đạt được trọn vẹn. Khi học lại về transaction ở phần sau của khoá này, bạn sẽ thấy đôi khi phải hy sinh một vài nguyên lý ACID.
Vậy nên hãy tận hưởng lúc transaction còn đơn giản và đẹp đẽ. Giờ thì vào ví dụ luôn nhé!
Ví dụ sử dụng transaction
Cùng xem kịch bản thêm sinh viên và đăng ký cho họ vào khoá học.
Giả sử bạn đang làm việc với database của trường đại học. Có những bạn ngoài trường muốn học thử khoá của bạn. Nếu khoá còn chỗ, thì bạn sẽ đăng ký cho họ làm sinh viên (tạm thời) và thêm vào khoá học. Đây là cách thực hiện.
Khi thêm sinh viên mới vào database và đăng ký cho họ vào khoá học, bạn cần:
- Thêm bản ghi vào bảng
students. - Tạo bản ghi trong bảng
enrollments, liên kết sinh viên với khoá học.
Nếu có gì đó sai (ví dụ khoá học đã đầy), bạn phải rollback để dữ liệu không bị lệch giữa các bảng. Đây là cách làm:
-- Bắt đầu transaction
BEGIN;
-- Bước 1: Thêm sinh viên
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Nam')
RETURNING id;
-- Giả sử trả về id = 10
-- Bước 2: Đăng ký cho bạn ấy vào khoá học
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 5);
-- Mọi thứ ok? Lưu thay đổi
COMMIT;
Nếu có lỗi thì sao?
Giả sử có lỗi khi đăng ký khoá học: ví dụ khoá học không tồn tại. Nếu bạn quên dùng transaction, thì bản ghi sinh viên vẫn còn trong bảng students, còn bảng enrollments thì không có gì. Như vậy là dữ liệu bị lệch. Để tránh chuyện này, bạn dùng lệnh ROLLBACK.
-- Bắt đầu transaction
BEGIN;
-- Bước 1: Thêm sinh viên
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Nam')
RETURNING id;
-- Bước 2: Thử đăng ký cho bạn ấy vào khoá học
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 999); -- Lỗi: không có khoá học id = 999!
-- Rollback tất cả thay đổi
ROLLBACK;
Kết quả là không thao tác nào được thực hiện, database vẫn như cũ trước khi transaction bắt đầu.
Dùng SAVEPOINT để kiểm soát
Giờ tưởng tượng tình huống phức tạp hơn. Bạn muốn làm nhiều thao tác, nhưng ở một bước nào đó chỉ muốn quay lại một điểm nhất định, không phải huỷ hết mọi thứ.
Cùng làm thử đăng ký sinh viên từng bước nhé
-- Bắt đầu transaction
BEGIN;
-- Thêm sinh viên
SAVEPOINT add_student; -- Tạo điểm lưu
INSERT INTO students (name, age, gender)
VALUES ('Anna Song', 22, 'Nữ');
-- Đăng ký khoá học đầu tiên
SAVEPOINT enroll_course_1; -- Thêm một điểm lưu nữa
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 5);
-- Đăng ký khoá học thứ hai (lỗi ở đây)
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 999); -- Lỗi!
-- Quay lại điểm lưu gần nhất
ROLLBACK TO enroll_course_1;
-- Tiếp tục quá trình
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 6);
-- Lưu thay đổi
COMMIT;
Như vậy, lỗi ở một phần sẽ không ảnh hưởng tới dữ liệu ở phần khác.
Kiểm tra có thay đổi hay không
Nếu câu lệnh SQL có thay đổi gì đó, bạn có thể kiểm tra xem thực sự có gì thay đổi không.
Có thể bạn chạy DELETE nhưng không dòng nào khớp với WHERE. Hoặc chạy UPDATE mà dữ liệu đã giống rồi nên thực ra không có gì thay đổi.
Cho trường hợp này, có biến hệ thống FOUND. Nó cho biết có dòng nào bị ảnh hưởng ở câu lệnh SQL cuối không:
FOUND = TRUE— câu lệnh đã update/delete gì đó;FOUND = FALSE— không có gì bị xoá hay thay đổi.
Với SELECT bình thường thì không dùng được, chỉ để kiểm tra thay đổi thôi nhé.
Ứng dụng thực tế: xử lý thanh toán
Transaction cực kỳ hữu ích trong app tài chính. Cùng quay lại hệ thống chuyển tiền từ tài khoản này sang tài khoản khác nhé.
-- Bắt đầu transaction
BEGIN;
-- Bước 1: Trừ tiền tài khoản đầu tiên
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- Bước 2: Kiểm tra thao tác thành công (có dòng bị thay đổi)
IF NOT FOUND THEN
ROLLBACK; -- Rollback nếu không đủ tiền
RAISE EXCEPTION 'Không đủ tiền!'; -- Lỗi! Ném exception
END IF;
-- Bước 3: Cộng tiền vào tài khoản thứ hai
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- Lưu transaction
COMMIT;
Ở đây, nếu khách chuyển nhiều hơn số dư, transaction sẽ rollback, database không bị "treo" ở trạng thái dở dang.
Những lưu ý và lỗi phổ biến
Quên COMMIT: nếu quên chạy COMMIT cuối transaction, database sẽ "chờ mãi", thay đổi không được lưu.
Quên WHERE: update hay delete mà không có điều kiện có thể gây hậu quả khủng khiếp. Ví dụ, lệnh DELETE FROM students không có WHERE sẽ xoá sạch sinh viên.
Transaction kéo dài: nếu transaction mở quá lâu, nó có thể khoá dữ liệu, gây chậm hệ thống. Luôn kết thúc transaction (COMMIT hoặc ROLLBACK) càng sớm càng tốt.
Transaction là bạn thân duy nhất khi cần đảm bảo tính toàn vẹn dữ liệu. Nó giúp tránh dữ liệu bị lệch, nhất là trong các tình huống phức tạp như đăng ký user, xử lý thanh toán hay update bảng liên quan. Thành thạo các lệnh BEGIN, COMMIT, ROLLBACK và SAVEPOINT sẽ giúp bạn code app chắc chắn và an toàn hơn nhiều.
GO TO FULL VERSION