Giao dịch trong SQLite
(reorchestrate.com)- Trong vài năm gần đây, SQLite được chú ý như một bộ máy cơ sở dữ liệu SQL trong tiến trình, độ tin cậy cao và mạnh mẽ, dùng làm backend cho tiến trình máy chủ
- Mức độ phổ biến tăng vọt dù các nhà phát triển SQLite gần như chủ động khuyên không nên dùng SQLite cho mục đích này, thay vì vai trò truyền thống là ứng dụng client hoặc edge
Những lý do chính khiến tôi quan tâm đến SQLite:
- Đơn giản về mặt khái niệm: hãy hình dung một B-tree của các hàng/tuple được phân vùng theo khóa chính. Nó đã được kiểm thử rất rộng rãi để duy trì bền vững trên đĩa, rồi thêm vào một lớp tương tác SQL
- Có thể xây dựng chiến lược sao lưu thực tế thông qua Litestream. Sao lưu WAL tới vị trí từ xa và sao chép liên tục. Có thể tự động khôi phục khi khởi động bằng một lệnh đơn giản
- Tôi vẫn thích một môi trường phát triển hoàn chỉnh có thể chạy ngoại tuyến
- Có thể làm việc trong bộ nhớ thông qua
file::memory:, nên mã kiểm thử có thể dễ dàng khởi động và dừng một instance khi cần
Giới hạn single-writer
- Các nhà phát triển SQLite đã tài liệu hóa rất đầy đủ về "các giới hạn của SQLite trên máy chủ" và phân tích cấu hình phía máy chủ tối ưu. Nhưng hạn chế nổi bật là các website lưu lượng cao, tức là những website có nhiều thao tác ghi
- Ở chế độ WAL, SQLite theo thiết kế sử dụng một Writer duy nhất. Điều này cho phép tối đa 1 giao dịch ghi chạy đồng thời cùng nhiều giao dịch chỉ đọc
- Thiết kế này đặt nút thắt cổ chai của các website lưu lượng cao thiên về ghi vào việc quản lý thông lượng của Writer duy nhất đó. Điều này đưa ta quay lại một trong những thành phần cốt lõi của công nghệ hiện đại
SQLite
- SQLite mặc định cung cấp các giao dịch cô lập nghiêm ngặt ở mức SERIALIZABLE. Đây là mức bảo đảm Isolation mạnh nhất
- Bằng cách dùng một Writer duy nhất, SQLite sử dụng một dạng pessimistic concurrency control có thể dễ dàng bảo đảm rằng dữ liệu nền tảng không bị thay đổi trong khi giao dịch ghi đang diễn ra
Postgres
- Trên thực tế, Postgres khác với mặc định
SERIALIZABLEđược định nghĩa trong chuẩn SQL và chọn mứcREAD COMMITTEDít nghiêm ngặt hơn (dù dùng cơ chế Multiversion concurrency control phức tạp hơn nhiều)- Việc giảm độ nghiêm ngặt này mang theo rủi ro non-repeatable reads, tức là ngay cả trong cùng một giao dịch, việc chạy cùng một truy vấn đọc nhiều lần có thể cho ra kết quả khác nhau nếu giá trị bị thay đổi ở nền bởi các giao dịch COMMITTED khác
- Bằng cách chọn mức Isolation này, Postgres mở ra nguy cơ giao dịch hoạt động trên dữ liệu đã cũ. Lập trình viên cần ghi nhớ điều này
- Khi đặt thành
SERIALIZABLE, Postgres dùng cơ chế optimistic-concurrency control để theo dõi dữ liệu được truy cập trong transaction và xác minh rằng nó không bị thay đổi trước khi commit- Postgres thực hiện điều này dựa trên khóa ở mức row hoặc page tùy theo transaction để quản lý mức sử dụng bộ nhớ
- Mẫu này được gọi là
optimisticvì nó kỳ vọng dữ liệu nền sẽ không thay đổi, do khi transaction commit, dữ liệu được transaction giám sát càng chi tiết thì càng ít có khả năng bị thay đổi
FoundationDB
- Giao dịch không chỉ giới hạn trong cơ sở dữ liệu quan hệ. Nó sử dụng optimistic concurrency control để đạt được bảo đảm SERIALIZABLE trong kho khóa-giá trị phân tán
- Khi NoSQL mới xuất hiện, kho NoSQL phân tán có bảo đảm ACID không phải là chuyện phổ biến. FoundationDB đã viết một bản tuyên ngôn về giao dịch để nhấn mạnh rằng lập trình viên có thể hưởng lợi lớn từ các bảo đảm ACID
- FoundationDB đưa ra lời khuyên về cách và thời điểm viết mã cho optimistic concurrency control, cũng như thực tế là đôi khi dữ liệu thay đổi do xung đột giao dịch đồng thời và transaction sẽ được tự động thử lại
Idempotence
- Giao dịch có tính idempotent là giao dịch tạo ra cùng một hiệu ứng dù commit một lần hay hai lần
- FoundationDB đưa ra các pattern để làm cho transaction có tính idempotent nhằm tránh vấn đề trong trường hợp transaction phải được thử lại nhiều lần do xung đột
Vậy nếu ghi nhớ tất cả điều đó, SQLite cung cấp những lựa chọn nào?
BEGIN …
SQLite cung cấp nhiều cách để nhà phát triển cho engine biết giao dịch nên hoạt động ra sao, dưới dạng các từ khóa IMMEDIATE, EXCLUSIVE, DEFERRED, mà trong chế độ WAL có thể rút gọn còn DEFERRED và IMMEDIATE
DEFERRED
- Giao dịch bắt đầu ở chế độ READ, có thể chạy đồng thời với các giao dịch đọc hoặc ghi khác
- Chỉ được nâng cấp thành giao dịch READ-WRITE có chặn khi thực thi truy vấn sửa đổi trạng thái DB (INSERT, UPDATE, DELETE)
- Khi nâng cấp, nếu DB đang bị khóa bởi giao dịch khác thì trả về lỗi SQLITE_BUSY. Client phải xử lý điều này
IMMEDIATE
- Giao dịch bắt đầu ngay ở chế độ READ-WRITE
- Nếu đã có giao dịch ghi đang chạy thì lập tức trả về SQLITE_BUSY
- Client phải quyết định cách xử lý
CONCURRENT
- SQLite có một nhánh thử nghiệm để chuyển giao dịch từ pessimistic sang optimistic ở mức giới hạn
- Sở dĩ là mức giới hạn vì optimistic locking hoạt động ở mức page của DB (mặc định 4096 bytes), chứ không phải mức hàng/tuple
- Ở chế độ CONCURRENT, SQLite có thể cho phép nhiều giao dịch ghi cùng hoạt động, nhưng trước khi commit sẽ xác minh rằng các page được truy cập trong quá trình thực hiện giao dịch không bị thay đổi kể từ khi giao dịch bắt đầu
- Nếu không có xung đột, các thay đổi sẽ được commit tuần tự và đạt bảo đảm SERIALIZABLE nghiêm ngặt. Nếu có xung đột thì trả về SQLITE_BUSY
HC-Tree
- Một nhánh thử nghiệm khác của SQLite là [HC-Tree], một công việc đang tiếp diễn nhằm cung cấp optimistic locking ở mức hàng/tuple. Một kết quả thú vị là nó cung cấp một bộ benchmark rất tốt cho thấy lợi ích hiệu năng của thiết kế như vậy khi so với nhánh
BEGIN CONCURRENT
Sẽ thế nào nếu lấy cách tiếp cận benchmark của họ và chạy thử với các tùy chọn tiêu chuẩn?
Benchmark
nUpdate=1, nScan=0
- Giao dịch chỉ ghi này cho thấy rõ lợi thế của IMMEDIATE so với DEFERRED. Việc locking diễn ra ngay lập tức và transaction không phải chịu chi phí nâng cấp
- CONCURRENT cho thấy thông lượng tăng khi số luồng tăng và xung đột nhiều hơn
nUpdate=10, nScan=0
- Đúng như dự đoán, việc ghi theo lô giúp rất nhiều cho số hàng được cập nhật ở 16 luồng. CONCURRENT tăng từ khoảng ~12k/giây lên ~19k/giây
- IMMEDIATE so với DEFERRED trở nên kém quan trọng hơn. Bởi vì chi phí của bản thân thao tác cập nhật quan trọng hơn chi phí nâng cấp transaction
nUpdate=1, nScan=10
- Giao dịch này lẽ ra sẽ phơi bày điểm yếu của locking CONCURRENT ở mức page do có random read
- Nó ngay lập tức cho thấy vì sao dùng IMMEDIATE cho các transaction cập nhật lại quan trọng hơn chi phí nâng cấp của DEFERRED
- Với CONCURRENT, các kết quả này rất vững vì xung đột cơ bản thực ra không tăng lên đáng kể
nUpdate=0, nScan=10
- Giao dịch xử lý theo lô chỉ đọc này cho thấy tác động của pessimistic concurrency control
- Nó cho thấy vì sao không nên đặt IMMEDIATE làm mặc định cho mọi giao dịch
- CONCURRENT so với IMMEDIATE cho thấy có một chút bất lợi khi dùng chế độ CONCURRENT. "Hiệu năng giảm nhẹ trong mọi trường hợp"
- Nhưng CONCURRENT sẽ là một tùy chọn mặc định tốt
Chưa có bình luận nào.