当应用从单体演进到需要后台任务处理时,引入消息队列往往意味着引入第二个数据存储。Redis + Celery 是常见方案,但这带来了双写一致性、备份复杂度以及运维额外开销。 honker 给出了一种更简洁的思路:让队列直接存在于 SQLite 文件内部,业务表与队列表共享同一事务、同一 WAL、同一崩溃恢复单元。
队列即表:原子性入队的实现原理
honker 将队列实现为 SQLite 表中的特殊行组,本质上是一张带有部分索引的普通表。默认使用 _honker_live 表存储活跃任务,关键字段包括 payload(JSON 负载)、priority(优先级,数值越高越优先)、run_at(可执行时间戳)、state(pending/processing/claimed)以及 attempts(已重试次数)。入队操作本质上是向该表插入一行,与业务表的 INSERT 在同一事务内提交。
这种设计的核心优势在于原子性。假设订单系统需要在用户下单时同时触发邮件通知通知:
with db.transaction() as tx:
tx.execute("INSERT INTO orders (id, total) VALUES (?, ?)", [42, 99])
q.enqueue({"to": "alice@example.com", "order_id": 42}, tx=tx)
若事务回滚,订单记录与队列任务同时消失,不存在「订单已创建但通知任务丢失」的中间状态。反之,若提交成功,两者持久化到同一 WAL 日志段,崩溃恢复时可保证一致的可见性。
免轮询唤醒:PRAGMA data_version 的工程技巧
传统轮询方案需要_worker 定时执行 SELECT 查询是否有新任务,这会造成无意义的磁盘 I/O 与锁竞争。 honker 采用了更巧妙的机制:每秒 1000 次读取 SQLite 的 PRAGMA data_version。该值是单调递增计数器,SQLite 在每次提交后自动递增,且读取操作仅涉及共享内存页缓存,不触发磁盘写入,实测延迟约 3 微秒。
一个后台线程轮询该值,当检测到变化时向所有订阅者发出信号。订阅者随后执行 SELECT ... WHERE id > last_seen 获取新增行。关键在于无论多少个订阅者同时监听,SQLite 侧仅产生一个轻量级 SELECT,真正实现了「一个 poller 线程服务整个数据库」的高效架构。官方测试数据显示,在 M 系列 MacBook 上跨进程唤醒延迟中位数约 0.7 毫秒。
该方案的代价是空闲状态下每毫秒一次轻量 SELECT,对现代 SSD 完全可以忽略。无需内核级文件监听(如 inotify),也无需额外的通知守护进程。
可见性超时与心跳保活
队列任务的「可见性超时」机制用于处理 Worker 崩溃或处理超时场景。默认超时为 300 秒,Worker 认领任务时会更新 claim_expires_at 字段。若超时前未收到 ACK,任务自动解除锁定供其他 Worker 重新认领。
对于长时间运行的任务,honker 提供心跳接口定期延长锁定期:
async for job in q.claim("worker-1"):
async with heartbeat(job, every_s=60):
await long_running(job.payload)
job.ack()
心跳扩展本质上是更新 claim_expires_at,属于极轻量的 UPDATE 操作,不影响其他任务的并发处理。
崩溃恢复:WAL 模式下的事务一致性
honker 依赖 SQLite 的 WAL 模式实现崩溃安全。 WAL 模式下,所有修改先写入预写日志(WAL file),主数据库文件仅在检查点操作时更新。这带来了几个工程上的关键特性:
原子性提交:当事务提交时,SQLite 将 WAL 页面刷盘(通过 fsync),此时即使系统突然断电,恢复时可通过重放 WAL 恢复到一致状态。 honker 的任务状态与业务数据处于同一 WAL 范围内,因此不存在「业务提交成功但任务丢失」或「任务已提交但业务回滚」的割裂。
检查点策略:建议生产环境配置 PRAGMA wal_autocheckpoint = 1000(每 1000 页约 4MB 自动检查点),或根据业务负载手动调用 PRAGMA wal_checkpoint(FULL)。检查点操作会阻塞写入但不影响读取,适合在低峰期调度。
死信与重试:重试超过 max_attempts(默认 5 次)的任务自动移入 _honker_dead 表,该表不在常规索引范围内,查询不受影响。死信表不会自动清理,需要管理员手动检查 last_error 字段定位问题。
多语言绑定与跨进程协作
honker 提供七种语言的绑定(Python、Node、Rust、Go、Ruby、Bun、Elixir),但底层共享同一套表结构和 WAL 格式。这意味着可以用 Python 入队、用 Go 消费,数据格式完全兼容。对于微服务架构中不同服务使用不同技术栈的场景,这一特性显著降低了队列基础设施的统一成本。
集成层面, honker 设计为「即插即用」:只需在现有数据库连接上加载扩展或初始化绑定,即可在同一事务内混合执行业务 SQL 与队列操作。框架层面的集成(如 FastAPI、Django、Express、Rails)大约需要 20 行胶水代码,官方仓库的 examples 目录提供了参考模式。
工程参数清单
若计划在生产环境部署 honker,建议关注以下参数: WAL 检查点频率根据写入吞吐量调整,高写入场景可降低至 500 页以减少恢复窗口;队列任务默认可见性超时 300 秒,长任务需配合心跳机制并适当延长;优先级队列使用 priority DESC, run_at 组合索引确保 O (log n) 查询性能;批量认领时单次建议不超过 100 条任务以平衡吞吐量与事务时长。
数据来源: honker 官方文档(https://honker.dev)。