问题背景
在构建后台任务处理系统时,开发者常面临两难选择:引入 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 字段驱动状态机流转。
核心实现机制
状态机流转
任务在系统中经历四个明确状态:
- pending:任务已入队,等待消费
- in_progress:任务已被取出处理中
- done:任务处理成功,可归档或清理
- failed:任务处理失败,可能进入重试或死信队列
状态转换通过 gen_server 的回调函数实现:
enqueue/1:向内存队列插入任务 ID,同步写入 SQLite 并标记为 pendingdequeue/0:从内存队列取出任务 ID,更新 SQLite 状态为 in_progress 并记录 started_atack/1:确认任务完成,更新状态为 done 并记录 completed_atfail/2:标记任务失败,可选择重试(retry_count 递增,状态回置 pending)或进入失败状态
崩溃恢复策略
Erlang 的 "let it crash" 哲学在此场景下需要配合持久化层实现数据安全。当 gen_server 进程异常退出时,监督树(supervisor)会重启该进程。重启后的初始化逻辑需执行以下恢复步骤:
- 重新建立 SQLite 连接
- 查询所有状态为
in_progress且started_at超过超时阈值的记录 - 将这些记录状态回置为
pending,并递增retry_count - 将所有
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 交互模式
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。