定时任务如何避免重复执行:状态文件、互斥锁与失败关闭
定时任务重复执行,不一定是调度器触发了两次。上一次任务尚未结束、失败后的自动重试、进程中途退出,或者远程操作已经成功但本地尚未记账,都可能造成重复。
避免重复不能只靠定时器,还需要分别处理并发、状态记录和不确定结果。
一、互斥锁先解决并发
同一台机器上的多个进程,可以用 flock 将关键步骤保护起来。拿不到锁就退出,通常比排队后再次执行更适合定时检查任务。
这里讨论的是同机、本地文件系统的用法;它不能自动协调多台机器,也不能替代数据库唯一约束或远程服务的幂等机制。网络文件系统还需要核对具体锁语义,不能照搬本地假设。
二、成功确认后再写完成
“启动过”不等于“成功了”。开始时就写完成标记,可能让实际失败的任务永远失去补偿机会;等操作结束才记账,则存在“动作成功、标记尚未落盘就崩溃”的窗口。
因此,至少应区分运行中和已确认成功。读取到运行中、未知或损坏状态时,先停止并核对,而不是自动把它改回未执行。这样会牺牲一部分自动恢复能力,但能降低盲目重试造成的重复风险。
三、用原子替换避免半截状态
直接覆盖状态文件,可能留下不完整内容。更稳妥的方式是先写同一文件系统内的临时文件,再通过 mv 替换正式文件。读者看到的是替换前或替换后的版本,而不是逐步写入的半成品。
原子可见性不等于断电持久性。对掉电后也必须可靠的业务,还需要考虑 fsync、目录同步、存储语义等问题,不能只凭一次 mv 就宣称绝不会丢失。
四、教学示例:只写本地文件
下面的 Bash 示例只在当前目录下创建演示文件,不访问真实账户、不兑换权益,也不调用远程服务。首次缺少状态文件时允许启动,仅是演示初始化约定;真实业务应明确由谁初始化,不能把状态丢失直接当成从未执行。
#!/usr/bin/env bash
set -Eeuo pipefail
dir="./demo-job-state"
mkdir -p "$dir"
exec 9>"$dir/job.lock"
flock -n 9 || exit 0
state_file="$dir/state"
if [[ -e "$state_file" ]]; then
status="$(cat "$state_file")"
[[ "$status" == succeeded ]] && exit 0
printf '发现未确认状态,请人工核对:%s\n' "$status" >&2
exit 1
fi
tmp="$(mktemp "$dir/.state.XXXXXX")"
result_tmp=""
trap 'rm -f -- "$tmp" "${result_tmp:-}"' EXIT
printf 'running\n' > "$tmp"
mv -f -- "$tmp" "$state_file"
# 模拟本地动作,并核对结果
result_tmp="$(mktemp "$dir/.result.XXXXXX")"
printf 'local-demo-ok\n' > "$result_tmp"
mv -f -- "$result_tmp" "$dir/result"
grep -Fxq 'local-demo-ok' "$dir/result"
printf 'succeeded\n' > "$tmp"
mv -f -- "$tmp" "$state_file"
这个示例再次运行时会跳过已成功任务;如果遗留 running 或其他状态,则停止等待核对。它不会擅自清除不确定状态。实际系统还应记录任务标识、执行时间和结果摘要,帮助排查。
五、远程结果不明,先查再重试
远程请求可能已经被服务端执行,客户端却在收到响应前断网。此时本地看到的是失败,业务结果却不一定失败。再次发送,可能造成重复创建或重复提交。
优先使用服务端支持的业务幂等键,并查询已有结果;无法确定时进入人工核对,而不是换个进程再试一次。客户端状态文件只能记录本地掌握的事实,不能证明远端动作只发生一次。
状态文件、互斥锁和失败关闭各自解决一部分问题,但不能单独提供端到端 exactly-once。稳健自动化的关键,是定义清楚成功证据、模糊结果的处理方式,以及哪些情况下宁可停下来,也不能盲目重试。
- 点赞
- 收藏
- 关注作者
评论(0)