sqlite 在 multi-threads 情況下同時寫入的問題

在多執行緒與多核心 CPU 的環境下,使用 Python FastAPI 搭配 SQLite 處理多人同時寫入,確實有可能遇到存取錯誤。這主要與 SQLite 的鎖定機制以及 Python 的資料庫驅動程式行為有關。

SQLite 的鎖定機制

SQLite 是一個基於檔案的資料庫,它的併發控制相對單純。當一個進程或執行緒想要寫入資料時,SQLite 會對整個資料庫檔案加上排他鎖(Exclusive Lock)。在預設的資料庫模式下,當某個連線正在寫入,其他連線的讀取或寫入請求都會被阻擋。如果寫入操作耗時過長,其他嘗試寫入的連線會因為等不到鎖而拋出 sqlite3.OperationalError: database is locked 的錯誤。

Python 與 FastAPI 的影響

FastAPI 通常運行在非同步環境下,但標準的 sqlite3 套件是同步的。如果你在 FastAPI 中使用多執行緒(例如透過 thread pool 執行同步函式),多個執行緒同時存取同一個資料庫連線會引發程式崩潰,因為 sqlite3 預設不允許跨執行緒共享連線。即使每個執行緒都有獨立連線,上述的檔案鎖問題依然存在。此外,Python 有全域解釋器鎖(GIL),雖然這在處理 I/O 密集任務時影響較小,但它確保了 Python bytecode 一次只會由一個核心執行,這並不能解決資料庫層級的競爭問題。

如何減少存取錯誤

要讓 SQLite 在多人同時寫入時更穩定,最有效的做法是開啟預寫式日誌(WAL)模式。在 WAL 模式下,讀取和寫入可以同時進行,不會互相阻塞。你可以透過執行 PRAGMA journal_mode=WAL; 來切換。另外,設定適當的 timeout 參數也很重要。在建立連線時設定 sqlite3.connect(db_file, timeout=20),這會讓連線在遇到鎖定時先等待一段時間,而不是立即報錯。

替代方案與考量

如果你的應用場景真的有非常頻繁的併發寫入需求,SQLite 可能不是最佳選擇。雖然 WAL 模式能改善情況,但 SQLite 終究是為了單機或中輕量級應用設計的。當寫入衝突頻繁到 timeout 都無法解決時,改用像 PostgreSQL 這種客戶端伺服器架構的資料庫會是比較根本的解決辦法,因為它們能處理更細粒度的行級鎖定(Row-level locking),在多核心環境下發揮更好的效能。


要在 Python 的 sqlite3 模組中啟用 WAL 模式,你需要在建立資料庫連線後執行一段 PRAGMA 指令。這個設定會儲存在資料庫檔案中,但通常建議在每次程式啟動連線時都確認一次。

啟用 WAL 模式的步驟

在 Python 中,你可以透過執行 execute 方法來切換模式。這會將日誌模式從預設的 DELETE 改為 WAL。

import sqlite3

# 建立連線,建議加上 timeout 避免 busy 錯誤
conn = sqlite3.connect('example.db', timeout=10)

# 啟用 WAL 模式
conn.execute('PRAGMA journal_mode=WAL;')

# 驗證是否成功切換
cursor = conn.execute('PRAGMA journal_mode;')
print(f"目前模式為: {cursor.fetchone()[0]}")

conn.close()

為什麼要在連線時設定

雖然 WAL 模式一旦設定後會持久化在資料庫標頭中,但在程式碼中明確執行這行指令有助於確保環境一致。當你啟用 WAL 後,你會發現資料庫目錄下多了兩個暫存檔(結尾為 -wal 與 -shm),這是正常現象。

在 FastAPI 中的建議配置

如果你在 FastAPI 中使用,建議在建立連線的工具函式或是 SQLAlchemy 的 engine 配置中加入這個設定。如果你使用 SQLAlchemy,可以使用 event listener 來自動執行。

from sqlalchemy import create_engine, event

engine = create_engine("sqlite:///./test.db")

@event.listens_for(engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
    cursor = dbapi_connection.cursor()
    cursor.execute("PRAGMA journal_mode=WAL")
    cursor.execute("PRAGMA synchronous=NORMAL")
    cursor.close()

這裡額外加入了 PRAGMA synchronous=NORMAL;,在 WAL 模式下配合這個設定可以大幅提升寫入速度,同時兼顧資料安全性。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *