← 返回文章归档

进程重启以后,任务还在吗:FlashFlow 的租约、重试与库存回补

从已接单却未发送、回补失败和旧执行者迟到三个场景,理解数据库任务表、租约令牌及业务幂等如何共同工作。

内存队列会跟着进程一起消失

异步下单常见的第一版写法是:接口收请求,放进队列,后台线程稍后处理。正常运行时很顺,但进程退出后,尚未处理的请求也可能跟着内存一起没了。

FlashFlow 把请求先保存到数据库的 order_request。派发器查找仍是 QUEUED 且达到执行时间的记录,再尝试推进。这张表既记录用户能查询到的请求状态,也保存后台尚未完成的工作。

因此,接口说“已经接受”时,应该已经有一份能在重启后查到的记录。具体数据库可用性与持久化能力仍是这个承诺的前提。

扫描到任务,不代表拿到了执行权

两个执行者可能同时扫描到同一行。DurableTasks.claim 会用带条件的 UPDATE 领取任务:只有状态仍满足条件、next_attempt_at 已到期时,才写入新的租约令牌、增加尝试次数,并把下次可领取时间推到租约截止点。

受影响行数为零,就说明这次没有领到。扫描可以重复,领取却必须检查数据库中的当前状态。

项目用数据库时间计算到期条件,减少不同应用进程各看一块本机时钟带来的偏差。时间字段的语义也必须保持一致,尤其是在 H2 和 MySQL 之间读取时间类型时。

租约超时会带来第二个执行者

租约解决的不是“任务永远只执行一次”,而是让失联执行者占着的工作能够被重新领取。代价是:旧执行者如果只是执行得慢,并没有真正消失,租约过期后它可能与新执行者同时活动。

假设执行者 A 领到令牌 T1,停顿后租约到期;B 领到 T2。A 迟到的失败记录不应把 B 的新状态覆盖掉,所以记录失败和完成释放时都会带上当前令牌条件。

但这里的令牌不是覆盖所有外部系统的通用 fencing token。Kafka 发送、Redis 操作和数据库出票仍需要各自处理重复。特别是业务结果,最终仍依赖请求行锁、状态迁移与约束,不能因为“有租约”就删掉这些校验。

库存回补也需要一份待办记录

取消或超时后,数据库事务会修改订单状态、回补数据库库存,并插入 reservation_release 待办。后续任务再去释放 Redis 中对应的预占。

如果 Redis 当时不可用,数据库里的待办还在,下次可以继续。若先修改数据库,再临时调用 Redis,却没有持久化后续工作,一次异常就可能留下无人知道的半成品。

release.lua 会记录 released 标记。对已释放请求的重复执行,不应继续增加库存;迟到的预占也会看到这份标记,避免把已经结束的请求重新占住。

这是一组相互配合的状态约定,而不是一段 Lua 把所有系统包进了同一个事务。

重试要留下排查线索

项目失败后会安排下一次尝试,而不是无限紧密循环。attempts、next_attempt_at 和错误类别帮助判断任务是偶发失败,还是长期积压。错误记录只保存类别,避免把连接字符串或凭据带进数据库。

我会用下面的故障顺序检查它:

  1. 接受请求后重启,确认未完成请求仍可继续处理。
  2. 让一次 Redis 释放失败,确认待办没有被提前标记完成。
  3. 恢复依赖后重试,确认最终状态收敛。
  4. 重复触发释放,确认不会反复增加可售库存。

面试学习网站里常问“为什么消息队列还要幂等”。用这个项目回答时,可以先说清楚:重试是恢复手段,重复是恢复过程必须承受的正常输入,业务约束才是结果不重复的边界。

阅读入口是 DurableTasks.java、OrderWorker.java、OrderService.release 和 release.lua。先看一行任务在失败前后留下什么,再看定时器多久触发一次。

✳

感谢阅读,下一篇再见。

回到文章开头 ↑