cron脚本重复执行会破坏一致性,因其默认不检查运行状态、执行结果或目标状态是否已满足;应基于目标状态(如文件存在且内容正确)而非进程是否存在来判断,优先使用原子操作(如mv)、声明式配置和systemd条件触发实现幂等。

为什么 cron 脚本重复执行会出事
因为绝大多数脚本默认不检查自己是否已在运行、上次是否成功、目标状态是否已满足。比如用 curl 下载文件却没加 -f 或校验逻辑,任务每分钟触发一次,就可能反复写入相同数据、重复创建用户、多次解压同个包——结果不是“多做点”,而是破坏一致性。
常见错误现象:Permission denied(因上轮没清理临时锁)、数据库唯一键冲突、磁盘被填满(日志/缓存未去重)、服务启动报 Address already in use。
- 判断依据不是“脚本有没有在跑”,而是“目标状态是否已达成”——例如“
/opt/app/config.yaml是否存在且内容正确”比“pidof myscript.sh”更可靠 - 避免用时间戳或随机数作为幂等性依据,它们无法跨重启或跨机器收敛
- 写入类操作优先用
mv替代cp:先写到config.yaml.tmp,校验通过再mv config.yaml.tmp config.yaml,原子替换能防止中间态被读取
systemd 定时器如何天然支持幂等语义
它不像 cron 那样只管“按时触发”,而是把任务建模为“单元(unit)”,天然带状态机:可以定义 ConditionPathExists=、ExecStartPre= 做前置断言,失败则整个任务跳过,不产生副作用。
使用场景:部署静态资源、初始化数据库表、生成证书——只要目标文件/记录已存在,就该静默退出。
- 在
.service文件里加ConditionPathExists=/etc/myapp/ready,服务就不会在条件不满足时启动 - 用
ExecStartPre=/usr/bin/test -f /var/lib/myapp/schema_migrated替代脚本内 if 判断,失败时systemctl start myapp-init.service直接返回非零且不执行后续 - 注意
OnCalendar=触发的定时器,若前次任务未结束,新实例默认被跳过(除非显式设Persistent=true),这本身就是一种轻量级互斥
用 flock 做简单互斥但别当万能解
flock 是最易上手的进程级锁,适合单机、短时、无状态任务,但它不解决“状态是否已达成”,只解决“同一时刻只跑一个”。很多人误以为加了 flock 就等于幂等,其实只是把并发问题变成了串行问题。
常见错误现象:锁文件路径写错(如用了相对路径)、没检查 flock 返回值、锁粒度太粗(整个脚本一把锁,但其中只有 3 行需要保护)。
- 必须用绝对路径:
flock /var/lock/mytask.lock -c "do_something",否则不同工作目录下锁无效 - 加
-n非阻塞模式,并检查退出码:flock -n /var/lock/mytask.lock -c 'exit 0' || exit 0,避免任务卡死 - 不要对整个部署脚本加锁,而应针对具体操作段落封装,比如单独锁住
apt-get update && apt-get install -y nginx这一行
幂等脚本里怎么安全改配置文件
直接 sed -i 或 echo >> 是最大雷区:重复执行会导致配置重复追加、参数被多次替换、注释行错位。真正安全的方式是“声明式覆盖”+“校验驱动”。
使用场景:修改 /etc/ssh/sshd_config、注入环境变量到 /etc/default/myapp、更新 Nginx server block。
- 用
grep -q '^Port 2222$' /etc/ssh/sshd_config || echo 'Port 2222' >> /etc/ssh/sshd_config仍不安全——可能已有#Port 22注释行干扰匹配 - 推荐做法:生成完整新配置(用
cat 或模板工具),用 <code>diff -q对比旧文件,仅当不同时才mv new.conf old.conf - 敏感操作加
set -e -u -o pipefail,并在关键步骤后加[[ $(sshd -t 2>&1) == "" ]] || exit 1校验语法,防止写坏后服务起不来
真正的难点不在加锁或判断,而在定义“什么是完成状态”。比如“证书已部署”是指文件存在?权限正确?Nginx 已重载?还是 HTTPS 端口能响应 200?每个环节都得可验证、可回退、不依赖外部时序。










