Hotdry.

Article

Erlang/OTP 配合 SQLite 构建轻量级持久化任务队列

基于 gen_server 进程模型与 SQLite 持久化层,在可靠性与工程复杂度间取得平衡的轻量级任务队列设计方案。

2026-06-13systems

问题背景

在构建后台任务处理系统时,开发者常面临两难选择:引入 Redis、RabbitMQ 等外部中间件会增加部署复杂度与运维成本;而纯内存方案则在进程崩溃时面临数据丢失风险。对于中小型服务或边缘计算场景,一种更轻量的替代方案是结合 Erlang/OTP 的进程模型与 SQLite 的嵌入式持久化能力,在单节点内实现可靠的任务队列。

架构设计

该方案的核心是 gen_server 行为模式与 SQLite 持久化层的协同工作。gen_server 作为队列管理器维护运行时状态,SQLite 则负责跨重启的数据持久化。

进程模型

gen_server 维护三类状态数据:

  • 内存队列:存储待处理任务的 ID,使用 Erlang 标准库的 queue 模块实现 FIFO 语义
  • 数据库引用:SQLite 连接句柄,通过 NIF 绑定(如 erlang-sqlite3)操作
  • 配置参数:包括任务超时阈值、重试次数上限等运行时策略

数据表结构

SQLite 端采用单表设计,字段涵盖任务生命周期全阶段:

CREATE TABLE tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    payload TEXT NOT NULL,
    status TEXT CHECK(status IN ('pending', 'in_progress', 'done', 'failed')),
    enqueued_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    started_at DATETIME,
    completed_at DATETIME,
    retry_count INTEGER DEFAULT 0,
    reason TEXT
);

payload 字段存储序列化后的任务数据(建议使用 JSON 或 Erlang 的 term_to_binary),status 字段驱动状态机流转。

核心实现机制

状态机流转

任务在系统中经历四个明确状态:

  1. pending:任务已入队,等待消费
  2. in_progress:任务已被取出处理中
  3. done:任务处理成功,可归档或清理
  4. failed:任务处理失败,可能进入重试或死信队列

状态转换通过 gen_server 的回调函数实现:

  • enqueue/1:向内存队列插入任务 ID,同步写入 SQLite 并标记为 pending
  • dequeue/0:从内存队列取出任务 ID,更新 SQLite 状态为 in_progress 并记录 started_at
  • ack/1:确认任务完成,更新状态为 done 并记录 completed_at
  • fail/2:标记任务失败,可选择重试(retry_count 递增,状态回置 pending)或进入失败状态

崩溃恢复策略

Erlang 的 "let it crash" 哲学在此场景下需要配合持久化层实现数据安全。当 gen_server 进程异常退出时,监督树(supervisor)会重启该进程。重启后的初始化逻辑需执行以下恢复步骤:

  1. 重新建立 SQLite 连接
  2. 查询所有状态为 in_progressstarted_at 超过超时阈值的记录
  3. 将这些记录状态回置为 pending,并递增 retry_count
  4. 将所有 pending 状态的任务 ID 按 enqueued_at 排序加载到内存队列

这一机制确保了 "处理中" 任务在崩溃后不会丢失,且具备幂等性保证(业务层需实现幂等处理逻辑)。

幂等性保证

任务幂等性通过数据库层的唯一约束与业务层的去重逻辑共同实现。建议在 payload 中嵌入业务层面的唯一标识(如订单号、事件 ID),并在消费端实现幂等处理:

  • 首次处理:正常执行业务逻辑,标记任务为 done
  • 重复处理:检测到该业务 ID 已处理,直接返回 ack 避免副作用

可落地参数配置

超时阈值

参数 建议值 说明
task_timeout 300s 单任务最大执行时间,超过则视为处理节点失联
retry_interval 60s 失败任务重试间隔,指数退避可配置为 60s, 120s, 240s
max_retries 3 最大重试次数,超过则转入死信队列

连接池配置

SQLite 的并发写入受限于文件级锁,建议采用以下策略:

  • 写入串行化:所有写操作(enqueue、ack、fail)通过 gen_server 单进程序列化,避免锁竞争
  • 读取并发:查询操作(如监控统计)可开启独立连接,配置 PRAGMA journal_mode=WAL 提升读并发能力
  • 连接保活:设置 PRAGMA busy_timeout=5000 处理短暂锁冲突,避免立即报错

监控指标

建议暴露以下指标用于运维观测:

  • queue_depth:当前 pending 任务数量
  • in_progress_count:处理中任务数量(用于检测处理节点健康)
  • retry_rate:单位时间内重试任务占比(异常检测)
  • db_write_latency:SQLite 写入延迟 P99

适用边界与权衡

该方案适合以下场景:

  • 单节点部署:边缘计算、IoT 网关、本地后台任务处理
  • 吞吐量适中:SQLite 单文件写入上限约 1-5K TPS(视硬件而定),超出需考虑分区或多实例
  • 延迟容忍:磁盘持久化引入的写入延迟(通常 <10ms)在可接受范围

不适合的场景包括:

  • 高并发写入:需考虑 Redis Streams 或 Kafka 等分布式方案
  • 跨节点协调:SQLite 不支持网络协议,多节点场景需引入分布式锁或外部协调服务
  • 复杂路由需求:如需按优先级、标签路由任务,建议采用专业消息队列

总结

Erlang/OTP 的进程监督模型与 SQLite 的零配置持久化能力相结合,为轻量级任务队列提供了一种工程复杂度与可靠性兼顾的方案。通过 gen_server 管理运行时状态、SQLite 保证数据持久化、状态机驱动任务生命周期,开发者可在数百行代码内实现具备崩溃恢复能力的任务处理系统。对于不需要分布式能力的场景,这种方案避免了引入重量级中间件带来的运维负担。


参考来源

  • Erlang/OTP gen_server 官方文档与回调模式说明
  • erlang-sqlite3 NIF 绑定实现与 SQLite 交互模式

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com