Trong bài này
Cô Mai và thầy Nam cùng cộng xu cho một học viên sau buổi sinh hoạt. Cả hai đều nhìn thấy số dư cũ rồi nhập số dư mới vào biểu mẫu. Mỗi người nhận thông báo lưu thành công, nhưng học viên chỉ nhận được một lần cộng. Muốn sửa lỗi này, bạn cần xem hai lần đọc và hai lần ghi đã xen kẽ thế nào.
Một lần ghi thành công vẫn có thể làm mất lần cộng khác
Mất cập nhật (lost update) xảy ra khi một lần ghi dựa vào dữ liệu cũ đè lên thay đổi cần giữ. Vấn đề nằm ở giá trị được gửi đi, không nhất thiết ở câu lệnh bị báo lỗi. Hãy xem số dư trước khi hai giáo viên thao tác:
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 |
Lịch dưới đây là tình huống giả định. Hai biểu mẫu đều đã đọc xong trước khi biểu mẫu đầu tiên lưu:
| Bước | Cô Mai | Thầy Nam |
|---|---|---|
| 1 | Đọc số dư 120 | Chưa thao tác |
| 2 | Tính số mới 130 | Đọc số dư 120 |
| 3 | Lưu số mới 130 | Tính số mới 130 |
| 4 | Đã lưu xong | Lưu số mới 130 |
Ta có thể chạy hai lần gán tương ứng để xem hậu quả trên dữ liệu. Đây là phép dựng lại các giá trị ghi, chưa phải hai kết nối chạy đồng thời:
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 = 130
WHERE id = 1;
UPDATE students
SET coins = 130
WHERE id = 1;Bảng students sau khi chạy
students1 dòng thay đổi| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 130 | 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 kết thúc với 130 xu. Lần ghi sau vẫn gửi số đã tính từ 120, nên không cộng thêm vào kết quả lần trước. Hai khoản cộng 10 cần cho kết quả 140, nhưng một khoản đã mất. Đọc lại lịch giúp bạn thấy vì sao thông báo lưu thành công chưa đủ.
Nếu thầy Nam đọc sau khi cô Mai lưu, biểu mẫu có thể nhìn thấy số mới. Chính thời điểm đọc làm kết quả khác đi. Tranh chấp thời điểm (race condition) là tình huống kết quả phụ thuộc cách các thao tác xen kẽ. Một lần thử tình cờ chạy lần lượt có thể không bộc lộ lỗi.
Để cơ sở dữ liệu tính từ số đang được cập nhật
Cập nhật tương đối (relative update) diễn tả lượng thay đổi thay vì gửi một số dư đã tính sẵn. Với phép cộng một dòng, hãy đặt biểu thức ngay sau SET. Cơ sở dữ liệu áp dụng phép tính khi xử lý lần cập nhật đó:
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 + 12
WHERE id = 1;
UPDATE students
SET coins = coins + 12
WHERE id = 1;Bảng students sau khi chạy
students1 dòng thay đổi| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 144 | 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 |
Ở ví dụ này, mỗi khoản là 12 xu, nên An có 144 xu sau hai lần cộng. Lần thứ hai cộng vào kết quả mà lần thứ nhất đã để lại. Các học viên khác giữ số dư vì điều kiện chỉ chọn mã 1. Biểu mẫu không cần gửi lại bản sao số dư cũ.
Hai ví dụ dùng cùng bảng đầu vào ban đầu. Mỗi khối chạy trên một bản dữ liệu mới, nên bạn so sánh được cách gán với cách cộng. Nếu chạy liên tiếp trong một bàn thực hành thật, bạn phải tự theo dõi trạng thái đã thay đổi.
- Dùng phép gán khi yêu cầu thật sự là đặt một giá trị xác định.
- Dùng phép cộng khi yêu cầu là tăng thêm một lượng so với số đang lưu.
- Dùng điều kiện lọc để giới hạn đúng những dòng được phép thay đổi.
Biểu thức này giải quyết dạng cộng một dòng đang xét. Nó không tự ghi lịch sử hay chống gửi lại cùng một phần thưởng. Nếu một yêu cầu gồm nhiều thay đổi, bạn vẫn cần giao dịch (transaction) bao trùm những thay đổi liên quan.
Gộp kiểm số dư với phép trừ trong cùng câu lệnh
Giả sử cần trừ xu chỉ khi học viên còn đủ số dư. Đọc số dư rồi quyết định ở biểu mẫu tạo thêm khoảng thời gian dữ liệu có thể đổi. Cập nhật có điều kiện (conditional update) đặt cả yêu cầu về số dư vào WHERE:
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 - 100
WHERE id = 1 AND coins >= 100;
UPDATE students
SET coins = coins - 100
WHERE id = 1 AND coins >= 100;Bảng students sau khi chạy
students1 dòng thay đổi| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 20 | 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 còn 20 xu. Lần đầu chọn được dòng và trừ 100. Lần sau không còn dòng thỏa điều kiện, nên không trừ thêm. Trong ứng dụng, bạn phải kiểm số dòng bị thay đổi để phân biệt thành công với việc không đủ điều kiện.
Không có dòng thỏa WHERE thường không phải lỗi SQL. Điều này khác với việc một dòng được chọn nhưng giá trị mới vi phạm ràng buộc. Câu sau thiếu điều kiện đủ xu và bị quy tắc không âm chặn lại:
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 - 200
WHERE id = 1;Thông báo lỗi
coins>=0
Thông báo lỗi cho biết giá trị mới vi phạm CHECK(coins >= 0). Ràng buộc là lớp bảo vệ dữ liệu, còn điều kiện cập nhật giúp diễn tả khi nào nghiệp vụ được phép thực hiện. Nếu bước sau còn cộng cho người nhận, ứng dụng phải dừng khi bước trừ không thành công.
Khóa và mức cô lập bảo vệ các quyết định nhiều bước
Khóa (lock) điều phối những thao tác có thể xung đột. Mức cô lập (isolation level) quy định một giao dịch nhìn thấy và tương tác với thay đổi của giao dịch khác ra sao. Chúng cần được chọn theo hệ quản trị và quy tắc của thao tác.
Chẳng hạn, hai người cùng kiểm tra chỗ cuối trong một lớp rồi cùng đăng ký. Phép cộng số dư ở mục trước không giải quyết quyết định liên quan nhiều dòng này. Hệ thống cần bảo vệ cả bước kiểm tra lẫn bước ghi, thay vì chỉ sửa cách tính một ô.
Bắt đầuMở giao dịchBảo vệChọn cơ chế phù hợpĐọcKiểm điều kiện mới nhấtGhiThực hiện đủ các bướcKết thúcXác nhận hoặc hủy
Trong PostgreSQL, khóa dòng bằng SELECT ... FOR UPDATE là một công cụ cho các dòng đã chọn. Khóa ấy không tự bảo vệ mọi điều kiện như tổng số chỗ của một lớp.
| Cơ chế trong PostgreSQL | Cách đọc cần nhớ | Việc ứng dụng cần làm |
|---|---|---|
READ COMMITTED | Mỗi câu lệnh lấy ảnh chụp mới của dữ liệu đã xác nhận | Xét thay đổi giữa hai câu lệnh |
REPEATABLE READ | Giữ ảnh chụp từ câu lệnh dữ liệu đầu tiên của giao dịch | Xử lý xung đột và quy tắc nhiều dòng |
SERIALIZABLE | Chỉ cho các giao dịch cùng thành công khi tương đương một thứ tự tuần tự | Chuẩn bị thử lại khi bị từ chối |
Ảnh chụp (snapshot) là góc nhìn dữ liệu tại một thời điểm, không phải một tệp sao lưu. Góc nhìn ổn định giúp đọc nhất quán, nhưng chưa tự diễn tả mọi quy tắc nghiệp vụ. Vì vậy, mức cô lập cao hơn không thay cho việc xác định điều kiện đúng.
Thử lại phải bắt đầu từ một quyết định mới
Khi một giao dịch bị hủy vì xung đột, dữ liệu mà nó từng đọc có thể đã cũ. Thử lại (retry) nghĩa là mở giao dịch mới, đọc lại và tính lại thao tác. Gửi riêng COMMIT hoặc gửi lại số dư tính cũ không làm quyết định trở nên đúng.
Đường xử lý có thể đi theo các bước sau:
- Xác định giao dịch đã bị từ chối hay kết quả chỉ chưa rõ do mất mạng.
- Với giao dịch bị hủy, kết thúc lần cũ và chờ một khoảng có giới hạn.
- Mở lần mới, đọc lại điều kiện rồi tính lại toàn bộ thay đổi.
- Với kết quả chưa rõ, tra mã yêu cầu trước khi quyết định thực hiện lại.
Mã yêu cầu duy nhất giúp nhận ra cùng một thao tác được gửi lại. Tính lũy đẳng (idempotency) ở đây nghĩa là gửi lại cùng yêu cầu không tạo thêm tác động. Khóa điều phối thao tác đồng thời, còn mã yêu cầu xử lý một rủi ro khác: thực hiện trùng sau lỗi mạng.
Bài sau sẽ ghép cập nhật số dư với lịch sử trong một giao dịch, thay vì chỉ kiểm ô số dư.
Lỗi thường gặp
| Lỗi | Vì sao sai | Cách sửa |
|---|---|---|
| Hai biểu mẫu đều gán số dư tính từ bản đọc cũ | Lần ghi sau có thể đè một khoản cộng | Dùng biểu thức cộng cho nghiệp vụ cộng một dòng |
| Coi không có lỗi là đã trừ thành công | WHERE có thể chọn không dòng nào | Kiểm số dòng thay đổi trước bước tiếp |
| Chỉ thử lại lệnh ghi cuối | Các quyết định trước đó có thể đã cũ | Đọc và tính lại trong giao dịch mới |
| Dùng kết quả tuần tự để kết luận về đồng thời | Chưa có hai kết nối thực sự xen kẽ | Kiểm lịch thực thi riêng trên môi trường phù hợp |
Tóm tắt
- Một lần lưu thành công vẫn có thể ghi đè thay đổi khác nếu gửi số tính từ dữ liệu cũ.
- Biểu thức trong
UPDATEtính lượng thay đổi từ giá trị đang được cập nhật. - Điều kiện trong cùng câu lệnh cần đi kèm kiểm số dòng bị thay đổi.
- Khóa, mức cô lập và chống gửi trùng có các nhiệm vụ khác nhau.
- Khi giao dịch bị hủy do xung đột, lần thử mới phải đọc và tính lại.