Cơ sở dữ liệu
Hai sinh viên tranh chỗ cuối: suy luận về giao dịch SQLite
Dựng tình huống ghi đồng thời, bảo vệ bất biến bằng câu lệnh có điều kiện và kiểm tra yêu cầu gửi lại.
Thích và đánh giá Bình luận và hỏi đáp1. Vì sao đọc còn chỗ rồi mới ghi là chưa đủ
Một lớp chỉ còn một chỗ. Hai sinh viên gửi yêu cầu gần như đồng thời. Nếu mỗi yêu cầu đọc con_lai, thấy giá trị 1 rồi ghi ở một giao dịch khác, cả hai có thể ra quyết định dựa trên cùng thông tin cũ. Thử nghiệm từng yêu cầu riêng lẻ có thể đều đạt, vì lỗi chỉ xuất hiện khi các thao tác xen kẽ.
Ta cần hai bất biến. Số chỗ còn lại không âm, và mỗi lượt giữ chỗ thành công tạo đúng một bản ghi đăng ký. Ràng buộc CHECK bảo vệ miền giá trị. Khóa chính ghép (lop, sinh_vien) ngăn một sinh viên có hai đăng ký cùng lớp. Giao dịch bảo đảm trừ chỗ và thêm đăng ký cùng thành công hoặc cùng được hoàn tác.
2. Đặt quyết định trong câu lệnh ghi có điều kiện
Lệnh UPDATE lop SET con_lai=con_lai-1 WHERE ma=? AND con_lai>0 chỉ trừ chỗ nếu tại thời điểm ghi lớp vẫn còn chỗ. Sau khi chạy, số hàng thay đổi là tín hiệu để quyết định bước tiếp theo. Nếu bằng 0, ví dụ gộp hết chỗ và không tìm thấy mã lớp vào một kết quả. Khi API cần phân biệt hai trường hợp, thực hiện kiểm tra bổ sung trong cùng giao dịch.
Ví dụ dùng BEGIN IMMEDIATE để bắt đầu giao dịch ghi ngay. Khi một kết nối đang ghi, kết nối còn lại cần đợi hoặc nhận lỗi bận, tùy cấu hình và thời gian chờ. Sau khi được phép ghi, nó nhìn thấy số chỗ mới. Giữ giao dịch ngắn. Nếu phải gọi một dịch vụ bên ngoài, hãy xác định giao thức phù hợp để tránh giữ khóa trong thời gian chờ mạng.
3. Dùng hai kết nối để kiểm tra chỗ cuối cùng
Môi trường thực hành. Python 3.10 trở lên. Ví dụ dùng thư viện chuẩn Python.
"""Hai yêu cầu cạnh tranh một chỗ học, sử dụng SQLite và hai kết nối."""
from concurrent.futures import ThreadPoolExecutor
from contextlib import closing
from pathlib import Path
import sqlite3
from tempfile import TemporaryDirectory
def dang_ky(path, lop, sinh_vien):
with closing(sqlite3.connect(path, timeout=5, isolation_level=None)) as db:
try:
db.execute("BEGIN IMMEDIATE")
if db.execute("SELECT 1 FROM dangky WHERE lop=? AND sinh_vien=?",
(lop, sinh_vien)).fetchone():
db.rollback()
return "đã đăng ký"
changed = db.execute(
"UPDATE lop SET con_lai=con_lai-1 WHERE ma=? AND con_lai>0",
(lop,),
).rowcount
if changed == 0:
db.rollback()
return "hết chỗ hoặc mã lớp không tồn tại"
db.execute("INSERT INTO dangky VALUES(?,?)", (lop, sinh_vien))
db.commit()
return "đăng ký thành công"
except sqlite3.Error:
db.rollback()
raise
if __name__ == "__main__":
with TemporaryDirectory() as directory:
path = Path(directory) / "sample.db"
with closing(sqlite3.connect(path)) as db:
db.executescript("""
CREATE TABLE lop(ma TEXT PRIMARY KEY, con_lai INTEGER NOT NULL CHECK(con_lai>=0));
CREATE TABLE dangky(lop TEXT NOT NULL, sinh_vien TEXT NOT NULL,
PRIMARY KEY(lop,sinh_vien));
INSERT INTO lop VALUES ('CTDL',1);
""")
with ThreadPoolExecutor(max_workers=2) as pool:
pending = [pool.submit(dang_ky, path, "CTDL", sv) for sv in ("SV01", "SV02")]
results = [job.result() for job in pending]
with closing(sqlite3.connect(path)) as db:
assert db.execute("SELECT con_lai FROM lop").fetchone() == (0,)
[winner] = db.execute("SELECT sinh_vien FROM dangky").fetchone()
assert results.count("đăng ký thành công") == 1
assert dang_ky(path, "CTDL", winner) == "đã đăng ký"
print(results, "| SV được giữ chỗ:", winner)Chạy python giao_dich_cho_hoc.py. Chương trình tạo cơ sở dữ liệu tạm rồi gửi hai thao tác từ hai luồng, mỗi luồng mở một kết nối. Một yêu cầu thành công, yêu cầu còn lại báo hết chỗ. Sinh viên nào thắng có thể khác giữa các lần chạy. Điều cần khẳng định là sau khi cả hai kết thúc, số chỗ là 0 và chỉ có một bản ghi đăng ký.
Chương trình còn gửi lại yêu cầu của sinh viên đã thắng. Nhánh kiểm tra đăng ký tồn tại chạy bên trong giao dịch, nên phản hồi “đã đăng ký” và giữ nguyên số chỗ. Đây là phép thử tính lặp an toàn trong ví dụ. Với API thực tế, có thể cần mã yêu cầu để khôi phục phản hồi chính xác sau khi mạng ngắt, đặc biệt khi cùng yêu cầu còn phát sinh các tác động khác.
4. Thiết kế ca để phân biệt thất bại nghiệp vụ và lỗi kỹ thuật
- Không còn chỗ. Dùng lớp đã hết chỗ và kiểm tra bảng đăng ký vẫn như cũ.
- Ghi bản đăng ký thất bại. Chủ động tạo lỗi sau lệnh trừ chỗ rồi kiểm tra giao dịch hoàn tác. Nếu kiểm tra chỉ ở giao diện, việc mất chỗ có thể không lộ ra ngay.
- Cơ sở dữ liệu bận. Giữ một giao dịch ghi quá thời hạn chờ trong môi trường thử. Phân biệt lỗi bận với phản hồi hết chỗ để ứng dụng quyết định có nên thử lại.
- Phản hồi bị mất. Sau khi ghi thành công, mô phỏng máy khách không nhận được xác nhận. Gửi lại yêu cầu rồi kiểm tra bất biến của dữ liệu.
Đọc thêm tại SQLite · Giao dịch và SQLite · Cách ly giữa các kết nối.