Trong bài này
Sau buổi sinh hoạt, An muốn chuyển một phần xu cho Bình để góp quỹ nhóm. Cô Mai cần nhìn được cả số dư mới lẫn bằng chứng về khoản chuyển. Nếu chỉ sửa số dư, cô không biết vì sao số xu thay đổi. Nếu chỉ ghi lịch sử, hai bạn vẫn nhìn thấy số dư cũ.
Một lần chuyển có bốn thay đổi cần đi cùng nhau
Ta sẽ chuyển 15 xu từ An sang Bình trên dữ liệu mẫu. Sổ biến động (ledger) lưu mỗi lần tăng hoặc giảm bằng một dòng riêng. Trong bảng ledger, cột delta mang dấu: số âm là giảm, số dương là tăng.
Đọc số dư và các dòng lịch sử đang có trước khi chuyển:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
SELECT id, name, coins
FROM students
WHERE id IN (1, 2)
ORDER BY id;Kết quả
| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
An có 120 xu, còn Bình có 90. Sau khi chuyển, số dư của An phải giảm đúng lượng Bình nhận. Bảng lịch sử cũng cần thêm một dòng âm cho An và một dòng dương cho Bình. Bốn thay đổi ấy mô tả cùng một việc, nên không thể chọn chỉ làm một phần.
ledger6 dòng| id | student_id | delta |
|---|---|---|
| 1 | 1 | 20 |
| 2 | 1 | -10 |
| 3 | 2 | 15 |
| 4 | 3 | 30 |
| 5 | 4 | -5 |
| 6 | 5 | 25 |
Các dòng đã có vẫn thuộc lịch sử trước đó. Ta thêm dòng mới thay vì sửa một dòng cũ thành khoản chuyển này. Nếu sửa lịch sử, người đối chiếu sẽ không còn biết lần thay đổi trước đã diễn ra thế nào.
- Số dư người gửi giảm theo lượng chuyển.
- Số dư người nhận tăng cùng lượng đó.
- Lịch sử người gửi thêm một biến động âm.
- Lịch sử người nhận thêm một biến động dương.
Giữ tổng số dư là điều cần kiểm, nhưng chưa đủ. Một lần chuyển thiếu cả hai dòng lịch sử vẫn có thể giữ tổng đúng. Ngược lại, hai dòng lịch sử đối dấu chưa chứng minh số dư đã được cập nhật.
Xác nhận sau khi đã ghi đủ số dư và lịch sử
Giao dịch (transaction) gom những thay đổi liên quan thành một đơn vị. BEGIN mở giao dịch, còn COMMIT xác nhận các thay đổi. Nếu một bước thất bại, ứng dụng phải hủy toàn bộ bằng ROLLBACK thay vì báo thành công một phần.
Ví dụ dùng hai mã lịch sử chưa có là 40 và 41. Các mã người gửi, người nhận đều đã tồn tại, và An có đủ xu. Ta ghi đủ bốn thay đổi rồi mới xác nhận:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
ledger6 dòng| id | student_id | delta |
|---|---|---|
| 1 | 1 | 20 |
| 2 | 1 | -10 |
| 3 | 2 | 15 |
| 4 | 3 | 30 |
| 5 | 4 | -5 |
| 6 | 5 | 25 |
Câu lệnh
BEGIN;
UPDATE students
SET coins = coins - 15
WHERE id = 1;
UPDATE students
SET coins = coins + 15
WHERE id = 2;
INSERT INTO ledger (id, student_id, delta)
VALUES (40, 1, -15), (41, 2, 15);
COMMIT;Bảng ledger sau khi chạy
ledger2 dòng thay đổi| id | student_id | delta |
|---|---|---|
| 1 | 1 | 20 |
| 2 | 1 | -10 |
| 3 | 2 | 15 |
| 4 | 3 | 30 |
| 5 | 4 | -5 |
| 6 | 5 | 25 |
| 40 | 1 | -15 |
| 41 | 2 | 15 |
Hai dòng cuối có cùng lượng chuyển nhưng trái dấu. Bảng sau câu lệnh hiện lịch sử, nên ta cần đọc thêm số dư để kiểm phần còn lại. Phần thiết lập dưới đây dựng lại đúng lần chuyển vừa mô tả cho bảng đối chiếu kế tiếp:
Đọc số dư sau khi xác nhận lần chuyển:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 105 |
| 2 | Bình | 105 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
SELECT id, name, coins
FROM students
WHERE id IN (1, 2)
ORDER BY id;Kết quả
| id | name | coins |
|---|---|---|
| 1 | An | 105 |
| 2 | Bình | 105 |
Hai bạn cùng có 105 xu, còn tổng vẫn là 210. Kết quả này khớp hai biến động mới -15 và 15. Mỗi ví dụ chạy trên bản mẫu riêng, nên khối thiết lập giúp xem trạng thái sau cùng mà không phụ thuộc khối trước.
Thất bại ở bước nào cũng phải hủy cả lần chuyển
Điều kiện thành công là những điều phải đúng trước khi ứng dụng báo đã chuyển. Ngoài đủ xu, người gửi và người nhận phải tồn tại. Mỗi phép cập nhật số dư cần thay đổi đúng một dòng. Câu lệnh hợp lệ về cú pháp vẫn có thể không chọn được ai.
Xem lỗi gõ sai mã người nhận. Câu lệnh này chạy được nhưng không cộng cho người nào:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
UPDATE students
SET coins = coins + 15
WHERE id = 99;Bảng students sau khi chạy
students6 dòng| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 120 | Cô Mai |
| 2 | Bình | 7 | Đà Nẵng | 90 | NULL |
| 3 | Chi | 8 | Hà Nội | 180 | Thầy Nam |
| 4 | Dung | 6 | Huế | 60 | NULL |
| 5 | Huy | 9 | Cần Thơ | 180 | Cô Mai |
| 6 | Lan | 7 | Hà Nội | 150 | Thầy Nam |
Toàn bộ số dư vẫn như ban đầu vì điều kiện không khớp dòng nào. Nếu người gửi đã bị trừ trước đó, tiếp tục xác nhận sẽ làm mất xu. Ứng dụng cần kiểm số dòng thay đổi trong cùng giao dịch rồi hủy khi thiếu người nhận.
Trong ví dụ kế tiếp, ta thử sửa cả số dư và lịch sử nhưng quyết định hủy trước khi xác nhận. Bạn quan sát trạng thái số dư sau khi hủy:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
ledger6 dòng| id | student_id | delta |
|---|---|---|
| 1 | 1 | 20 |
| 2 | 1 | -10 |
| 3 | 2 | 15 |
| 4 | 3 | 30 |
| 5 | 4 | -5 |
| 6 | 5 | 25 |
Câu lệnh
BEGIN;
UPDATE students
SET coins = coins - 15
WHERE id = 1;
UPDATE students
SET coins = coins + 15
WHERE id = 2;
INSERT INTO ledger (id, student_id, delta)
VALUES (40, 1, -15), (41, 2, 15);
ROLLBACK;Bảng students sau khi chạy
students6 dòng| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 120 | Cô Mai |
| 2 | Bình | 7 | Đà Nẵng | 90 | NULL |
| 3 | Chi | 8 | Hà Nội | 180 | Thầy Nam |
| 4 | Dung | 6 | Huế | 60 | NULL |
| 5 | Huy | 9 | Cần Thơ | 180 | Cô Mai |
| 6 | Lan | 7 | Hà Nội | 150 | Thầy Nam |
An vẫn có 120 và Bình vẫn có 90. Hai dòng vừa thêm cũng bị hủy vì nằm trong cùng giao dịch. ROLLBACK ở đây minh họa đường hủy, chưa phải chương trình tự phát hiện mọi điều kiện thất bại.
Kiểm ngườiMã người gửi và người nhận tồn tạiKiểm xuĐủ lượng muốn chuyểnGhi số dưMỗi bên đúng một dòngGhi lịch sửĐủ hai biến độngQuyết địnhXác nhận hoặc hủy tất cả
Tổng biến động không phải số dư đang có
Biến động ròng (net change) là tổng các lần tăng và giảm trong khoảng lịch sử đang xét. Bạn tính bằng SUM(delta), giữ dấu âm trong phép cộng. Trên lịch sử ban đầu, hãy tổng hợp riêng cho từng học viên:
Dữ liệu đầu vào
ledger6 dòng| student_id | delta |
|---|---|
| 1 | 20 |
| 1 | -10 |
| 2 | 15 |
| 3 | 30 |
| 4 | -5 |
| 5 | 25 |
Câu lệnh
SELECT student_id, SUM(delta) AS net_change
FROM ledger
GROUP BY student_id
ORDER BY student_id;Kết quả
| student_id | net_change |
|---|---|
| 1 | 10 |
| 2 | 15 |
| 3 | 30 |
| 4 | -5 |
| 5 | 25 |
Dòng của An có net_change = 10, vì 20 cộng với -10 bằng 10. Đây không phải số dư 120 trong bảng học viên. Muốn dựng số dư cuối, bạn còn cần số dư đầu và toàn bộ lịch sử của khoảng đang tính.
Các bạn chưa có dòng lịch sử không xuất hiện trong phép gom nhóm này. Nếu báo cáo cần đủ mọi học viên, bạn có thể dùng phép nối giữ đủ dòng đã học ở bài 15. Yêu cầu của báo cáo quyết định người chưa có lịch sử cần được hiện hay được bỏ.
| Câu hỏi | Dữ liệu cần đọc |
|---|---|
| Hiện còn bao nhiêu xu? | Số dư trong students.coins |
| Trong lịch sử này tăng giảm ròng bao nhiêu? | Tổng có dấu của ledger.delta |
| Hai dòng có thuộc một lần chuyển không? | Cần mã lần chuyển hoặc liên kết sự kiện |
Một cặp giá trị đối dấu không đủ để khẳng định chúng thuộc cùng sự kiện. Nhiều người có thể chuyển cùng lượng xu trong cùng ngày. Bảng mẫu chỉ lưu mã dòng, mã người và lượng thay đổi, nên không có thông tin liên kết hai phía.
Gửi lại yêu cầu cần kiểm lần chuyển đã thực hiện
Mạng có thể ngắt sau khi máy chủ xác nhận nhưng trước khi trình duyệt nhận phản hồi. Người dùng bấm lại vì chưa biết kết quả. Nếu máy chủ coi lần gửi lại là lần chuyển mới, số dư tiếp tục đổi dù tổng xu vẫn được bảo toàn.
Tính lũy đẳng (idempotency) giúp cùng mã yêu cầu chỉ gây tác động một lần. Máy chủ lưu mã cùng kết quả trong phạm vi ghi đáng tin cậy. Khi nhận lại mã đó, nó trả kết quả đã lưu thay vì chuyển thêm.
Các kiểm tra phục vụ những mục đích khác nhau:
- Giao dịch giữ các thay đổi cùng thành công hoặc cùng bị hủy.
- Kiểm đủ xu ngăn một người chi quá số đang có.
- Mã yêu cầu giúp nhận ra một lần gửi trùng sau lỗi mạng.
Hai mã lịch sử cố định trong ví dụ không phải hệ thống chống gửi trùng hoàn chỉnh. Chạy lại sẽ gặp trùng khóa, nhưng ứng dụng chưa biết kết quả cần trả cho yêu cầu cũ. Một hệ thống thật cần quy ước mã và lưu kết quả ở máy chủ.
Lỗi thường gặp
| Lỗi | Vì sao sai | Cách sửa |
|---|---|---|
| Chỉ cập nhật số dư | Không có bằng chứng biến động theo yêu cầu | Thêm cả hai dòng lịch sử trong giao dịch |
| Cộng cho người nhận sau phép trừ không thành công | Có thể tạo xu từ một lần chuyển bị từ chối | Kiểm số dòng thay đổi và hủy khi thiếu điều kiện |
| Bỏ dấu âm khi cộng lịch sử | Khoản giảm bị tính thành khoản nhận | Cộng trực tiếp giá trị có dấu |
| Gửi lại toàn bộ SQL sau khi mất phản hồi | Có thể thực hiện một lần chuyển lần nữa | Tra mã yêu cầu và kết quả đã lưu |
Tóm tắt
- Một lần chuyển có lịch sử gồm hai thay đổi số dư và hai biến động đối dấu.
- Chỉ xác nhận sau khi đủ điều kiện và mọi bước ghi thành công.
- Tổng biến động phản ánh một khoảng lịch sử, không tự bằng số dư hiện tại.
- Mã dòng liên tiếp không thay thế liên kết sự kiện chuyển.
- Giao dịch nguyên tử vẫn cần cơ chế chống gửi trùng ở ứng dụng.