Kịch bảnScript Python của bạn chạy hoàn hảo khi hoạt động riêng lẻ. Tuy nhiên, ngay khi bạn mở rộng quy mô—có lẽ bằng cách thêm multi-threading hoặc mở cơ sở dữ liệu trong một GUI như DB Browser—bạn đột nhiên gặp phải một lỗi gây khó chịu: sqlite3.OperationalError: database is locked.
Điều này thường xảy ra khi một tiến trình đang ghi vào cơ sở dữ liệu trong khi một tiến trình khác cố gắng ghi (hoặc đôi khi là đọc) trước khi giao dịch (transaction) đầu tiên kết thúc. SQLite cực kỳ nhẹ, nhưng cơ chế khóa mặc định của nó khá cứng nhắc.
Tại sao điều này xảy raSQLite tuân theo triết lý "single-writer, multiple-reader" (một người ghi, nhiều người đọc). Mặc dù nó xử lý đồng thời nhiều người đọc mà không gặp vấn đề gì, nhưng nó chỉ cho phép một người ghi tại một thời điểm. Khi một thao tác ghi bắt đầu, SQLite sẽ đặt một khóa 'RESERVED' lên tệp tin. Nếu một kết nối thứ hai cố gắng sửa đổi dữ liệu khi khóa đó đang hoạt động, công cụ sẽ trả về lỗi database is locked ngay lập tức hoặc sau một khoảng thời gian chờ (timeout) mặc định rất ngắn.
Cách khắc phục nhanh: Tăng Connection TimeoutCách nhanh nhất để xử lý các khóa tạm thời là yêu cầu SQLite đợi lâu hơn một chút để khóa được giải phóng. Các kết nối sqlite3 tiêu chuẩn trong Python mặc định có thời gian chờ là 5.0 giây. Trong môi trường có tính đồng thời cao, năm giây có thể trôi qua trong nháy mắt.
import sqlite3
# Tăng timeout lên 20 giây để các tiến trình khác có thời gian hoàn tất
conn = sqlite3.connect('my_database.db', timeout=20)
cursor = conn.cursor()
Điều này không ngăn chặn việc khóa xảy ra, nhưng nó giúp ứng dụng của bạn linh hoạt hơn. Thay vì gặp lỗi ngay lập tức, script của bạn sẽ thử lại thao tác nội bộ trong tối đa 20 giây.
Giải pháp lâu dài: Kích hoạt chế độ WALNếu ứng dụng của bạn xử lý việc đọc và ghi thường xuyên, chế độ 'rollback journal' mặc định có thể là nút thắt cổ chai của bạn. Chuyển sang chế độ Write-Ahead Logging (WAL) là một bước ngoặt. Nó cho phép người đọc tiếp tục làm việc ngay cả khi người ghi đang bận, giúp giảm đáng kể sự tranh chấp.
Chạy lệnh này một lần khi bạn khởi tạo cơ sở dữ liệu:
import sqlite3
conn = sqlite3.connect('my_database.db')
# Kích hoạt chế độ WAL để cải thiện tính đồng thời
conn.execute('PRAGMA journal_mode=WAL;')
cursor = conn.cursor()
cursor.execute("INSERT INTO logs (message) VALUES ('test')")
conn.commit()
conn.close()
Chế độ WAL có tính bền vững. Sau khi được kích hoạt, cơ sở dữ liệu sẽ duy trì ở chế độ này trừ khi bạn thay đổi lại một cách rõ ràng. Bạn sẽ nhận thấy hai tệp tạm thời—-wal và -shm—xuất hiện cùng với tệp .db của bạn khi kết nối đang hoạt động. Đây là điều bình thường và cần thiết để chế độ này hoạt động.
Quản lý Giao dịch (Transaction) đúng cáchCác giao dịch chưa hoàn tất là nguyên nhân phổ biến gây ra lỗi. Nếu một script kích hoạt lệnh BEGIN nhưng không bao giờ đạt đến COMMIT hoặc ROLLBACK, cơ sở dữ liệu sẽ bị khóa vô thời hạn. Điều này thường xảy ra khi có ngoại lệ (exception) phát sinh và kết nối không được dọn dẹp đúng cách.
Sử dụng một context manager là cách sạch sẽ nhất để đảm bảo các giao dịch được hoàn tất.
import sqlite3
def update_data(val):
try:
# Câu lệnh 'with' tự động commit khi thành công hoặc rollback khi có lỗi
with sqlite3.connect('my_database.db', timeout=20) as conn:
conn.execute('PRAGMA journal_mode=WAL;')
conn.execute("UPDATE settings SET value = ? WHERE id = 1", (val,))
except sqlite3.OperationalError as e:
print(f"Database error: {e}")
update_data("active")
Kiểm tra các khóa bên ngoàiĐôi khi vấn đề không nằm ở mã nguồn của bạn. Nếu bạn đang mở một công cụ như DBeaver hoặc DB Browser for SQLite, nó có thể đang giữ một giao dịch chưa được commit từ một chỉnh sửa thủ công mà bạn đã thực hiện. Hãy đảm bảo bạn đã nhấp vào 'Write Changes' hoặc 'Commit' trong công cụ GUI của mình.
- Windows: Sử dụng Resource Monitor để tìm kiếm các handle liên quan đến tệp
.dbcủa bạn.- Linux/macOS: Chạy lệnhlsofđể xác định tiến trình nào đang chiếm giữ tệp tin:``` lsof | grep my_database.db
## Các bước xác minhĐể xác nhận giải pháp của bạn, hãy mô phỏng môi trường tải cao bằng script này. Nó khởi chạy năm luồng (thread) đồng thời, tất cả đều cố gắng ghi vào cùng một tệp cùng một lúc:
import sqlite3 import threading import time
def worker(thread_id): try: conn = sqlite3.connect('test.db', timeout=10) conn.execute('PRAGMA journal_mode=WAL;') conn.execute("INSERT INTO test_table VALUES (?)", (thread_id,)) time.sleep(0.5) # Mô phỏng một thao tác ghi nặng conn.commit() conn.close() print(f"Thread {thread_id} successful") except sqlite3.OperationalError as e: print(f"Thread {thread_id} failed: {e}")
Khởi tạo bảng
conn = sqlite3.connect('test.db') conn.execute("CREATE TABLE IF NOT EXISTS test_table (id INTEGER)") conn.close()
Kích hoạt 5 luồng
threads = [threading.Thread(target=worker, args=(i,)) for i in range(5)] for t in threads: t.start() for t in threads: t.join()
Nếu tất cả các luồng đều báo cáo thành công, triển khai của bạn hiện đã đủ mạnh mẽ để truy cập đồng thời ở cấp độ production.

