russellromney/honker
honker:20+ SQL 函數讓 SQLite 跑起生產級佇列與 Kafka 式持久串流,1ms 輪詢近即時
- 1ms stat 輪詢感知 .db-wal 變動,不執行完整 SQL 查詢就能實現近即時通知
- 交易式發件匣保證:訊息與業務資料原子 commit,進程崩潰也不會丟失訊息
- 一個 .db 檔跑佇列、持久串流、SQL 廣播三合一,不需要外部 Redis 或 Kafka
用 1ms 的輪詢頻率、不到十行 Python 程式碼,honker 把 PostgreSQL 的 pub/sub(發布/訂閱)機制搬進了 SQLite——這個 Rust 擴充套件附帶超過 20 個自訂 SQL 函數,讓本來只做嵌入式儲存的輕量資料庫,也能跑生產等級的訊息佇列與事件串流。Simon Willison 在部落格上評為「設計相當紮實」,背後核心靠的是 transactional outbox pattern(交易式發件匣)——一個在 Postgres 世界被反覆驗證有效的可靠訊息模式。
SQLite 版 Postgres 廣播語意:honker 的定位
PostgreSQL(開源關聯式資料庫)原生提供 NOTIFY/LISTEN 機制,讓不同資料庫連線之間可以互相廣播訊息:當一個連線執行 NOTIFY channel_name,所有正在 LISTEN 的連線會即時收到通知。這個機制讓 Postgres 可以不依賴外部 message broker(訊息代理,如 RabbitMQ 或 Kafka)就實現輕量的事件驅動架構,是許多 Postgres-based 系統的骨幹設施。
SQLite 沒有這個功能。它設計上是嵌入式資料庫,以函式庫形式直接運行在應用程式進程內,原本就不支援跨進程的即時通知。honker(russellromney/honker)是用 Rust 編寫的 SQLite 擴充套件(extension),目標是在 SQLite 上重現同等的廣播語意,並提供 Python 等語言的綁定(binding,讓其他語言呼叫 Rust 程式庫的橋接層),讓開發者能以高階 API 操作佇列與串流,而不必自行實作底層機制。
訊息佇列與 Kafka 式持久串流的雙 API 設計
honker 提供兩套截然不同的抽象介面,對應不同的使用場景。
第一套是訊息佇列(queue),語意接近傳統工作佇列:生產者呼叫 .enqueue() 把任務推入佇列,消費者在獨立的 worker 進程裡以非同步迭代方式 claim 取件,處理完呼叫 .ack() 標記完成。claim 方法支援具名 worker(如 "worker-1"),用來追蹤哪個 worker 正在處理哪筆任務,防止重複消費。整套 Python API 的設計極為精簡——建立連線、建立佇列、推入、取出、確認,每個步驟僅需一行程式碼。
第二套是持久事件串流(durable stream),語意更接近 Apache Kafka(分散式事件串流平台):訊息不會在被讀取後消失,不同的 consumer 可以從串流的任意偏移量(offset)開始讀取,彼此完全獨立互不干擾。最關鍵的設計是,stream.publish() 可以帶入 tx=tx 參數,把發布動作綁定在現有的資料庫交易(transaction)內——以更新使用者名稱為例,UPDATE users 語句與發布事件包在同一個 transaction 區塊裡,只要 UPDATE 成功 commit,事件才進串流;UPDATE 失敗回滾,事件也同步取消,兩者原子一致,這正是 transactional outbox pattern 的核心保證。
超過 20 個自訂 SQL 函數:從查詢層直接廣播
除了語言層 API,honker 還在 SQLite 內建超過 20 個自訂 SQL 函數,讓開發者可以直接在 SQL 查詢中操作通知與串流,不需要切換到應用程式層:
SELECT notify('orders', '{"id": 42}');
SELECT honker_stream_read_since('orders', 0, 1000);
notify() 對指定頻道廣播一則 JSON 訊息;honker_stream_read_since() 從串流的某個偏移量開始讀取最多 N 筆事件,第三個參數是批次上限。後者對熟悉 Kafka consumer offset 機制的開發者會非常眼熟——本質上就是「從第 X 筆開始、最多取 N 筆」的視窗查詢。能從 SQL 層直接操作,意味著 honker 不限於 Python 使用者,任何能執行 SQLite 查詢的工具或語言都能接入,工具鏈相容性相當廣泛。
WAL 模式 + 1ms stat 輪詢的近即時通知底層
honker 要求開啟 WAL 模式(Write-Ahead Logging,預寫日誌模式)。WAL 是 SQLite 的一種日誌策略,與預設的 DELETE 模式不同,WAL 在寫入時先把資料記錄至 .db-wal 暫存檔,讀寫操作可並行進行而互不阻塞,適合多 worker 並發的生產場景。
利用 WAL 的特性,honker 的 worker 採用 stat 系統呼叫每 1ms 監測一次 .db-wal 檔案是否有變動——有新資料寫入時,.db-wal 的修改時間戳(mtime)就會更新,worker 隨即察覺並去讀新資料。這個設計巧妙繞開了定期執行完整 SQL 查詢的開銷:stat 呼叫極為輕量,每毫秒觸發一次也不會對系統造成明顯負擔。從資料寫入到 worker 察覺,延遲最多 1ms,在不引入任何額外基礎設施的前提下,已相當接近即時通知的效果。
Transactional Outbox Pattern 在 SQLite 的落地意義
Simon Willison 特別強調,honker 實作了 transactional outbox pattern(交易式發件匣模式),並推薦 Brandur Leach 的文章《Transactionally Staged Job Drains in Postgres》作為這個模式的最佳解說。
這個模式解決的是一個經典的分散式系統問題:若應用程式先寫資料庫、再推訊息到 message broker,在「寫完資料庫但還沒推訊息」的瞬間若進程崩潰,訊息就會永久遺失。解法是把「要發出的訊息」也寫入同一個資料庫交易——業務資料與訊息要嘛一起 commit,要嘛一起 rollback,在原子層面徹底消除丟訊息的可能性。honker 把這套保證帶進 SQLite,讓開發者在不依賴 Redis、Kafka 等外部基礎設施的情況下,也能做到分散式系統等級的訊息可靠性,是這個函式庫最具工程說服力的設計決策。
下次評估是否引入 Kafka 或 Redis 之前,先問一句:workload 真的超出一個 .db 檔加 honker 的能力範圍了嗎?