FreeBSD定时任务防重入
网站备份、数据统计和文件同步通常会交给定时任务处理。当一次任务的执行时间超过调度间隔,下一轮任务可能在上一轮尚未结束时启动,造成重复写入或资源争用。FreeBSD可以通过cron负责调度,再使用lockf控制同一任务的并发执行。
下面先用只输出日志和等待的测试脚本验证效果,确认锁机制正常后,再接入实际业务脚本。整个示例使用普通用户的个人crontab,不修改系统已有任务。
一、确认环境与执行账号
查看系统版本和工具位置。
freebsd-version
command -v lockf
command -v crontab
确认cron服务正在运行。
service cron status
如果服务未启动,或当前账号无权使用crontab,应先由管理员处理。
后续操作直接登录需要运行任务的普通用户账号执行,不要在操作过程中随意切换账号。进入sh环境。
/bin/sh
创建独立测试目录。
umask 077
demo_dir=$(mktemp -d "$HOME/cron-lock-demo.XXXXXX") || exit 1
printf 'Demo directory: %s\n' "$demo_dir"
记下输出的绝对路径。后续命令中的demo_dir变量需要在同一个Shell会话中使用;重新登录后,应先将变量恢复为原来的目录路径。
示例目录应位于本机文件系统,不使用共享网络目录。
二、创建测试任务
创建一个记录开始时间、等待指定秒数、再记录结束时间的脚本。
cat > "$demo_dir/job.sh" <<'EOF'
#!/bin/sh
duration=${1:-90}
printf 'start pid=%s time=%s\n' \
"$$" "$(/bin/date -u '+%Y-%m-%dT%H:%M:%SZ')"
/bin/sleep "$duration" || exit 1
printf 'finish pid=%s time=%s\n' \
"$$" "$(/bin/date -u '+%Y-%m-%dT%H:%M:%SZ')"
EOF
chmod 700 "$demo_dir/job.sh"
不传入参数时,脚本等待90秒。手动测试时可以指定较短的等待时间。
/bin/sh "$demo_dir/job.sh" 1
正常情况下,应先出现start记录,约一秒后出现finish记录。
这里没有读取数据库、删除文件或修改业务数据,便于单独检查调度和锁是否生效。
三、为任务增加文件锁
创建统一的任务入口。
cat > "$demo_dir/run.sh" <<'EOF'
#!/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
export PATH
umask 077
work_dir=$(CDPATH= cd "$(dirname "$0")" && pwd -P) || exit 1
exec /usr/bin/lockf -k -t 0 \
"$work_dir/job.lock" \
/bin/sh "$work_dir/job.sh" "${1:-90}"
EOF
chmod 700 "$demo_dir/run.sh"
检查两个脚本的语法。
/bin/sh -n "$demo_dir/job.sh"
/bin/sh -n "$demo_dir/run.sh"
没有报错后再继续。
这条锁命令中,-k用于在任务结束后保留锁文件,-t 0表示不等待锁。如果上一轮任务仍持有锁,本次调用立即退出,不启动新的业务脚本。
锁文件存在,不代表任务正在运行。 是否被占用由文件锁状态决定,不能仅通过job.lock是否存在来判断。
后续手动运行和定时运行都应使用run.sh。直接执行job.sh会绕过锁检查。
四、验证重复启动是否被阻止
执行以下测试。第一轮等待15秒,第二轮在一秒后尝试启动。
/bin/sh "$demo_dir/run.sh" 15 \
> "$demo_dir/first.log" 2>&1 &
first_pid=$!
sleep 1
/bin/sh "$demo_dir/run.sh" 1 \
> "$demo_dir/second.log" 2>&1
second_status=$?
wait "$first_pid"
first_status=$?
printf 'First exit status: %s\n' "$first_status"
printf 'Second exit status: %s\n' "$second_status"
cat "$demo_dir/first.log"
cat "$demo_dir/second.log"
正常预期如下。
- 第一轮退出状态为
0,日志包含开始和结束记录。 - 第二轮退出状态为
75,日志提示锁已被占用。 - 第二轮日志中没有新的
start记录。
如果第二轮返回0,先检查第一轮是否启动失败,或是否已经结束,再重新测试。
第一轮结束后,再执行一次。
/bin/sh "$demo_dir/run.sh" 1
printf 'Exit status: %s\n' "$?"
此时应能正常运行。虽然锁文件仍然保留,但上一轮持有的锁已经释放。
接入真实脚本后,退出状态需要结合业务日志判断。业务脚本本身也可能返回75,不能将所有75都认定为锁冲突。
五、备份并配置用户级crontab
先导出现有任务。
crontab -l \
> "$demo_dir/crontab.before" \
2> "$demo_dir/crontab-read.err"
backup_status=$?
printf 'Read crontab exit status: %s\n' "$backup_status"
cat "$demo_dir/crontab-read.err"
返回0时,现有配置已保存到crontab.before。
如果提示当前用户没有crontab,可以继续创建。若出现权限不足等其他错误,应先排查,不能将读取失败当成“没有任务”。
生成本次需要添加的任务行。
printf '* * * * * /bin/sh "%s/run.sh" >> "%s/cron.log" 2>&1\n' \
"$demo_dir" "$demo_dir"
复制命令输出的完整一行,再打开编辑器。
crontab -e
保留已有内容,将新任务追加到末尾并保存。用户级crontab在五个时间字段之后直接填写命令,不要额外加入用户名或root字段。
确认任务已安装。
crontab -l
这条规则每分钟尝试执行一次任务。测试脚本默认运行90秒,下一分钟触发时,如果上一轮尚未结束,新一轮就会因锁被占用而退出。
保存用户级crontab后,不需要为了这一条规则重启cron服务。
六、检查定时执行结果
等待三至四分钟后查看日志。
tail -n 40 "$demo_dir/cron.log"
应能看到任务开始、任务结束,以及部分轮次因锁被占用而未执行的提示。
如果没有日志,依次检查以下内容。
crontab -l中是否保留了新增任务。- 任务行中的绝对路径是否正确。
- 当前账号是否可以读取脚本、写入日志目录。
- cron服务是否正常运行。
- 手动执行
run.sh是否成功。
业务脚本需要的环境变量应明确设置,不能假定cron会加载交互终端中的全部配置。
日期格式中的百分号适合放在脚本内部。直接写入crontab命令部分时,未转义的%会被特殊处理,容易造成命令截断。
七、替换业务脚本与撤销测试
验证完成后,可以将job.sh中的等待操作替换为实际业务逻辑。业务任务应在前台运行,直到本轮工作真正结束,不要简单加上&后立即退出。
还应注意以下边界。
- 同一任务的所有入口应使用同一个锁文件。
- 不同任务需要独立运行时,应使用不同的锁文件。
- 任务执行期间不要删除或替换锁文件。
-t 0只控制获取锁时的等待,不限制业务任务运行时长。- 本例解决单机任务重叠,不作为多台服务器之间的分布式锁方案。
测试结束后,再次编辑crontab。
crontab -e
仅删除本次添加的任务行,保存后检查。
crontab -l
不要使用crontab -r清理单条测试任务,它会移除当前用户的整份crontab。
移除调度规则不会立即终止已经启动的任务。等待当前测试运行结束后,再处理测试目录。
八、常见问题
1. 为什么锁文件一直没有消失?
本例使用了-k,任务结束后会保留文件。文件保留与锁仍被占用是两回事,无需通过删除文件来“解锁”。
2. 能否等待上一轮结束后再执行?
可以将-t 0改为有限的等待时间,例如-t 30。等待超过30秒仍未获取锁时,本轮不执行。设置前应考虑任务频率,避免大量调用排队。
3. 加锁后是否就不会发生重复处理?
加锁能限制按同一规则启动的并发任务,但无法代替业务层的数据校验。任务中途失败、人工绕过入口执行,以及跨服务器处理同一批数据等情况,仍需要单独设计处理规则。
广告合作
QQ群号:4114653
- 本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com(#改为@)。
- 本站原创内容未经允许不得转载,转载请注明出处“站长百科”和原文地址。