项目概述
本项目实现基于租约的分布式任务调度器:调度到期任务到多个工作进程,确保恰好一个进程运行某个到期任务,并在进程崩溃后让其租约过期而被他人回收。目标是把“恰好一次执行”变成数据库的性质而非期望。
架构与关键决策
任务行携带调度时间、状态与带持有者与到期时间的租约;工作进程以会跳过已锁定行的加锁查询认领到期任务,从而两个进程运行同一查询也不会同时拿到同一任务。完成时清除租约并计算下次运行时间;失败则累加尝试次数并按退避策略顺延;耗尽尝试后进入死信。回收是一条查询——任何超过到期的租约都视为被弃并回到可认领集合,使崩溃成为例行事件。
实施与测试
两个工作进程对同一数据库断言每个到期任务只运行一次;留置租约并把时钟推过到期点断言被另一进程回收并执行;断言失败任务退避而非立即重试,并在达到上限后进入死信;断言未来任务不被认领。
限制与后续
- 面向单一主库,多区域需全局时钟或分区。
- 无任务间依赖图,顺序完全由调度决定。
- 租约到期是唯一活性信号,长任务须显式续租。
代码示例
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