Menu

Chế độ WAL trong SQLite: đọc, ghi đồng thời và checkpoint

Cách write-ahead logging của SQLite thay đổi chuyện truy cập đồng thời: đọc và ghi không còn chặn nhau, và các tệp -wal, -shm thực sự làm gì.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Chế độ mặc định và giới hạn của nó

Mặc định, SQLite dùng rollback journal. Khi bạn ghi, SQLite sao chép các trang gốc vào một tệp -journal, sửa cơ sở dữ liệu chính, rồi xóa journal khi commit. Nếu tiến trình bị sập giữa chừng, journal được phát lại theo chiều ngược để hoàn tác phần thay đổi dở dang.

Cách này đơn giản và an toàn, nhưng có một đặc điểm khó chịu: tiến trình ghi và tiến trình đọc tranh nhau cùng một tệp. Khi tiến trình ghi giữ khóa cơ sở dữ liệu, không tiến trình đọc nào bắt đầu được transaction mới. Khi các tiến trình đọc đang hoạt động, tiến trình ghi phải chờ. Với một ứng dụng bận rộn, chẳng hạn một web server có vài request đồng thời, bạn sẽ gặp lỗi SQLITE_BUSY sớm hơn bạn nghĩ.

Chế độ WAL thay đổi điều đó.

WAL thực sự làm gì

Write-ahead logging đảo ngược mô hình. Thay vì sửa tệp cơ sở dữ liệu chính tại chỗ, tiến trình ghi nối thêm các trang đã commit vào một tệp riêng có hậu tố -wal. Các tiến trình đọc vẫn đọc tệp chính, nhưng cũng xem qua WAL để lấy phiên bản mới hơn của những trang chúng cần.

Kết quả: một tiến trình ghi và bao nhiêu tiến trình đọc cũng được có thể hoạt động cùng lúc. Mỗi tiến trình đọc thấy một bản chụp nhất quán từ thời điểm transaction của nó bắt đầu, còn tiến trình ghi bận thêm dữ liệu vào WAL mà không đụng tới thứ các tiến trình đọc đang xem.

Chỉ một pragma đó là đủ để chuyển chế độ cho cơ sở dữ liệu. Chế độ này được giữ lâu dài: nó được lưu trong header của tệp, nên mọi kết nối sau này đều tự động dùng WAL. Bạn không cần chạy lệnh này ở mỗi kết nối, chỉ cần một lần khi khởi tạo cơ sở dữ liệu (hoặc trong công cụ chạy migration).

Pragma trả về chế độ mới. Nếu nó trả về wal, bạn đã xong. Nếu trả về thứ khác, có lẽ hệ thống tệp không hỗ trợ bộ nhớ chia sẻ (sẽ nói thêm ở phần dưới).

Bật và kiểm tra

Bạn có thể kiểm tra chế độ hiện tại bất cứ lúc nào:

Lần gọi đầu bật WAL và trả về chế độ mới. Lần gọi thứ hai (không có =) chỉ truy vấn chế độ. Sau đó, thư mục chứa messages.db sẽ có ba tệp khi có hoạt động: messages.db, messages.db-wal và messages.db-shm. Hai tệp sau xuất hiện và biến mất tùy theo việc có kết nối nào đang mở hay không.

Tệp -wal và -shm

WAL đi kèm hai tệp phụ, và bạn nên biết chúng làm gì:

  • -wal chứa các transaction đã commit nhưng chưa được gộp lại vào cơ sở dữ liệu chính. Nó lớn dần khi có lệnh ghi và co lại (hoặc được đặt lại) vào lúc checkpoint.
  • -shm là một tệp bộ nhớ chia sẻ. Nó là chỉ mục vào WAL để mọi kết nối thống nhất được trang nào nằm ở đâu mà không phải quét WAL ở mỗi truy vấn.

Hệ quả thực tế: đừng bao giờ sao chép một cơ sở dữ liệu ở chế độ WAL bằng cách chỉ sao chép tệp .db. Dữ liệu mới nhất nằm trong -wal, và thiếu nó thì bản sao của bạn sẽ cũ hoặc hỏng. Hãy sao chép cả ba tệp khi không có kết nối nào đang ghi, hoặc tốt hơn nhiều, dùng backup API của SQLite (được trình bày ở chương tiếp theo).

Truy cập đồng thời: một ghi, nhiều đọc

WAL không cho bạn ghi đồng thời. SQLite vẫn tuần tự hóa các lệnh ghi: tại mỗi thời điểm, đúng một transaction giữ khóa ghi. Điều thay đổi là lệnh ghi không chặn lệnh đọc và lệnh đọc không chặn lệnh ghi.

Vì vậy một ứng dụng web điển hình chạy với WAL sẽ hoạt động như sau:

  • Các endpoint đọc nhiều chạy song song mà không tranh chấp.
  • Các endpoint ghi xếp hàng chờ nhau trong chốc lát nhưng không chặn lệnh đọc.
  • Các tiến trình đọc chạy lâu (truy vấn phân tích, xuất dữ liệu) không bắt tiến trình ghi phải chờ.

Nếu hai kết nối cùng cố ghi một lúc, kết nối thứ hai sẽ nhận SQLITE_BUSY. Cách khắc phục thường là đặt một busy timeout hợp lý, tức là bảo SQLite chờ một chút trước khi bỏ cuộc:

busy_timeout=5000 nghĩa là "nếu khóa đang bị giữ, hãy chờ tối đa 5 giây trước khi báo lỗi". Kết hợp với WAL, thiết lập này xử lý được mức tranh chấp mà phần lớn ứng dụng thực sự gặp. Dạng BEGIN IMMEDIATE lấy khóa ghi ngay khi transaction bắt đầu thay vì ở lần ghi đầu tiên, giúp tránh cả một nhóm lỗi deadlock khi nâng cấp khóa trong trường hợp nhiều kết nối cùng định ghi.

Checkpoint: gộp WAL trở lại

Tệp WAL không thể lớn mãi. Checkpoint là quá trình lấy các trang đã commit trong WAL, ghi chúng vào cơ sở dữ liệu chính, rồi đặt lại WAL.

SQLite tự động checkpoint khi WAL vượt quá khoảng 1000 trang (giá trị mặc định của wal_autocheckpoint). Với phần lớn ứng dụng, bạn cứ để nguyên. Nếu muốn tinh chỉnh hoặc tự kích hoạt một lần checkpoint:

Pragma wal_checkpoint nhận một chế độ:

  • PASSIVE: checkpoint nhiều nhất có thể mà không làm gián đoạn các tiến trình đọc và ghi. Đây là mặc định.
  • FULL: chờ các tiến trình ghi đang hoạt động xong việc, rồi checkpoint mọi thứ đã commit.
  • RESTART: giống FULL, và chặn thêm các tiến trình đọc mới dùng WAL cũ.
  • TRUNCATE: giống RESTART, và thu nhỏ tệp WAL về lại 0 byte.

Phần lớn server không bao giờ cần gọi thủ công. Nếu bạn phát hành một ứng dụng desktop muốn giữ kích thước tệp gọn gàng khi tắt, một lần checkpoint TRUNCATE trước khi đóng kết nối cuối cùng là một thói quen hợp lý.

Vài pragma nên đi cùng WAL

Riêng WAL đã tốt. WAL cộng thêm vài thiết lập khác thường là thứ các ứng dụng production dùng:

Điểm qua nhanh:

  • synchronous=NORMAL là thiết lập được khuyên dùng cùng WAL. Nó an toàn trước sự cố sập ứng dụng và sập hệ điều hành; chỉ có mất điện đúng thời điểm xấu mới có thể làm mất các transaction gần nhất, và kể cả khi đó cơ sở dữ liệu vẫn nhất quán. Mặc định FULL an toàn hơn nhưng chậm hơn rõ rệt.
  • busy_timeout đã nói ở trên.
  • foreign_keys=ON không liên quan tới WAL nhưng nên đặt ở mọi kết nối: SQLite mặc định tắt việc kiểm tra khóa ngoại để tương thích ngược.

Các thiết lập này áp dụng theo từng kết nối (trừ journal_mode, vốn được giữ lại). Hãy chạy chúng ngay sau khi mở kết nối trong code ứng dụng.

Khi nào WAL không phải lựa chọn đúng

WAL là khuyến nghị mặc định, nhưng có vài trường hợp ngược lại:

  • Hệ thống tệp qua mạng. WAL dựa vào bộ nhớ chia sẻ (mmap) giữa các tiến trình truy cập cơ sở dữ liệu. NFS, SMB và các hệ thống tương tự không hỗ trợ điều này một cách ổn định. Nếu cơ sở dữ liệu nằm trên ổ chia sẻ qua mạng, hãy giữ rollback journal, hoặc tốt hơn, đừng đặt SQLite trên ổ chia sẻ qua mạng.
  • Thiết bị lưu trữ chỉ đọc. WAL cần ghi các tệp -wal và -shm. Một cơ sở dữ liệu trên CD-ROM hoặc thứ tương tự phải dùng chế độ journal không ghi (hoặc được mở ở chế độ chỉ đọc với mode=ro).
  • Tác vụ xử lý hàng loạt có một tiến trình ghi và không có tiến trình đọc đồng thời. WAL không gây hại, nhưng bạn cũng chẳng được lợi gì. Rollback journal mặc định là đủ.

Với 95% ứng dụng, gồm backend web, ứng dụng desktop, ứng dụng di động, thiết bị nhúng có bộ nhớ cục bộ, WAL là lựa chọn đúng.

Một cấu hình thực tế

Đây là cấu hình mà phần lớn hệ thống SQLite production dùng, rút gọn thành các pragma chạy được:

temp_store=MEMORY giữ các bảng và index tạm trong RAM thay vì trên đĩa: một cải thiện nhỏ, miễn phí nếu bạn còn dư bộ nhớ.

Thiết lập những điều này một lần khi tạo kết nối trong phần cấu hình cơ sở dữ liệu của ứng dụng, và bạn đã xử lý được phần lớn những gì một ứng dụng dùng SQLite cần để chạy tốt dưới tải đồng thời.

Tiếp theo: sao lưu và khôi phục

Giờ cơ sở dữ liệu của bạn đã có các tệp -wal và -shm đi kèm, sao chép tệp không còn là cách sao lưu an toàn nữa. Chương tiếp theo trình bày cách đúng để sao lưu một cơ sở dữ liệu SQLite đang chạy: lệnh .backup, online backup API, và cần làm gì khi bạn muốn một bản chụp nhất quán mà không phải tắt ứng dụng.

Câu hỏi thường gặp

Chế độ WAL trong SQLite là gì?

WAL là viết tắt của write-ahead logging. Thay vì ghi thay đổi thẳng vào tệp cơ sở dữ liệu chính và dùng rollback journal để hoàn tác khi gặp sự cố, SQLite nối các thay đổi vào một tệp -wal riêng và định kỳ gộp chúng trở lại. Lợi ích lớn là khả năng truy cập đồng thời: các tiến trình đọc và một tiến trình ghi có thể làm việc cùng lúc mà không chặn nhau.

Làm sao bật chế độ WAL trong SQLite?

Chạy PRAGMA journal_mode=WAL; một lần. Thiết lập này được giữ lâu dài: nó được lưu trong header của tệp cơ sở dữ liệu, nên các kết nối sau cũng tự động dùng WAL. Bạn không cần đặt lại ở mỗi kết nối. Khi thành công, pragma trả về chế độ mới (wal).

Chế độ WAL có cho phép ghi đồng thời không?

Không. SQLite vẫn tuần tự hóa các lệnh ghi. Mỗi lúc chỉ một tiến trình ghi giữ được khóa ghi. Điều WAL thay đổi là tiến trình đọc không còn chặn tiến trình ghi, và tiến trình ghi không còn chặn tiến trình đọc. Với phần lớn ứng dụng, đó mới là điểm nghẽn thực sự.

Tệp -wal và -shm là gì?

Tệp -wal chứa các thay đổi đã commit nhưng chưa được gộp lại vào cơ sở dữ liệu chính. Tệp -shm là một chỉ mục bộ nhớ chia sẻ nhỏ giúp các kết nối nhanh chóng tìm thấy các trang bên trong WAL. Cả hai đều được tự động tạo lại, nhưng nếu sao chép cơ sở dữ liệu, bạn phải sao chép chúng cùng nhau hoặc dùng backup API.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu