← 中文资源

简体中文版本

构建带租约的分布式任务调度器

一个完整的调度器:以租约领取到期任务、处理重试与死信,并在工作进程崩溃时不重复执行。

阅读英文原文

项目概述

本项目实现基于租约的分布式任务调度器:调度到期任务到多个工作进程,确保恰好一个进程运行某个到期任务,并在进程崩溃后让其租约过期而被他人回收。目标是把“恰好一次执行”变成数据库的性质而非期望。

架构与关键决策

任务行携带调度时间、状态与带持有者与到期时间的租约;工作进程以会跳过已锁定行的加锁查询认领到期任务,从而两个进程运行同一查询也不会同时拿到同一任务。完成时清除租约并计算下次运行时间;失败则累加尝试次数并按退避策略顺延;耗尽尝试后进入死信。回收是一条查询——任何超过到期的租约都视为被弃并回到可认领集合,使崩溃成为例行事件。

实施与测试

两个工作进程对同一数据库断言每个到期任务只运行一次;留置租约并把时钟推过到期点断言被另一进程回收并执行;断言失败任务退避而非立即重试,并在达到上限后进入死信;断言未来任务不被认领。

限制与后续

  • 面向单一主库,多区域需全局时钟或分区。
  • 无任务间依赖图,顺序完全由调度决定。
  • 租约到期是唯一活性信号,长任务须显式续租。

代码示例

pnpm init
pnpm add pg express
pnpm add -D typescript tsx vitest
task-scheduler/
├── src/dispatch.ts
├── src/lease.ts
├── src/retry.ts
├── src/dead-letter.ts
├── tests/scheduler.test.ts
└── package.json
SELECT id FROM tasks
WHERE status = 'pending' AND next_run_at <= now()
ORDER BY next_run_at FOR UPDATE SKIP LOCKED LIMIT 10;
UPDATE tasks SET lease_owner = $1, lease_expires_at = now() + interval '60s'
WHERE id = ANY($2);
next_run_at = now() + (interval '1 second' * power(2, attempts));
UPDATE tasks SET status = 'dead_letter' WHERE id = $1 AND attempts >= max_attempts;
UPDATE tasks SET status = 'pending', lease_owner = NULL
WHERE status = 'running' AND lease_expires_at < now();
pnpm vitest run