FlashDB将MQTT待发送消息落盘,设备掉电后从未完成记录继续发送
MQTT消息放在RAM队列中,设备一旦掉电,未发出的消息会全部丢失。
将消息先写入FlashDB,收到Broker ACK后再标记删除,设备重启时扫描未删除的记录继续发送,即可解决这个问题。
核心判断标准:记录存在代表消息未完成,标记删除代表处理完成。
RAM队列的问题
MQTT发布包含两个阶段:设备将消息交给客户端,客户端等待Broker回ACK。两个阶段之间如果发生掉电,RAM中的消息无法恢复,Broker永远收不到。
因此需要一个掉电不丢失的本地队列。FlashDB TSDB模式支持追加写入、顺序遍历、重启后数据保留,适合作为这个队列的存储层。
存储设计:数据与状态分离
TSDB每条记录由消息内容和状态索引组成。状态存储在索引中,更新状态时无需重写整条消息。
业务层只需要两个状态:
FDB_TSL_WRITE:记录存在,消息未完成FDB_TSL_DELETED:已收到ACK,标记删除,等待FlashDB回收
"等待ACK"是发送过程中的临时阶段,不需要落盘。重启时该消息会被重新发送,存储该状态没有意义。
发送流程
每轮处理从最老的一条FDB_TSL_WRITE记录开始:
- 查询队头第一条待发送记录
- 读取记录内容,发送消息
- 等待ACK
- 收到ACK后,删除该条记录
- 重新查询队头,处理下一条
发送函数不直接修改数据库状态,删除作为独立操作执行。删除时从数据库重新查找第一条WRITE记录,不依赖发送函数传递的句柄或位置。这样发送和删除职责清晰,故障排查路径明确。
掉电场景与投递语义
ACK与删除是两个独立动作,中间的掉电窗口无法消除。各场景下的行为如下:
| 掉电时机 | 重启后行为 |
|---|---|
| 发送前 | 记录存在,重发 |
| 消息已发出、ACK未到达 | 记录存在,可能重复发送 |
| 等待ACK过程中 | 状态仍为WRITE,回到队头重发 |
| ACK已到达、删除未完成 | 记录存在,会重复发送 |
| 删除完成后 | 记录不存在,不再发送 |
该方案选择**At-Least-Once(至少一次)**投递语义,宁可重复、不丢消息。消息去重由MQTT QoS机制或业务层消息ID处理。
边界与扩展
- 适用消息:Flash写入有擦写寿命和延迟开销,该方案适合QoS 1/2的高价值消息(如控制指令、告警、状态变更、计费数据)。高频、可丢弃的采样数据(如每秒一次的传感器读数)直接走RAM队列即可,落盘收益抵不过Flash磨损和吞吐开销。是否落盘应由业务层按消息类型决定,不要一刀切。
- 并发发送:该设计默认单发送任务。多任务并发时需要增加互斥锁和领取机制,避免多个任务取到同一条记录。
- 容量限制:单条消息长度受TSDB初始化时的最大长度配置约束,总容量受Flash扇区大小限制,需要根据业务场景设计旧消息淘汰策略。
- 状态扩展:TSDB自定义状态槽位数量固定,不建议占用状态位存储重试次数、消息类型等信息。这类数据可以放在消息内容的业务头部,自定义格式解析。