脚本如何规避任务重复执行问题

wen 实用脚本 25

本文目录导读:

脚本如何规避任务重复执行问题

  1. 文件锁(最简单,适合单机单进程)
  2. 数据库乐观锁 / 分布式锁(适合集群/微服务)
  3. 状态机 + 幂等性设计(最可靠)
  4. 系统级调度工具(推荐用于定时任务)
  5. 针对不同场景的建议
  6. 最优雅的写法(Python + Redis)

对于脚本任务重复执行的问题,通常可以在任务启动前任务执行中任务完成后三个阶段设置防护机制,以下是几种成熟且经过实践检验的解决方案,按复杂度和适用场景排序:

文件锁(最简单,适合单机单进程)

这是最经典的Unix方式,通过创建一个临时文件来标识任务正在运行。

原理:尝试创建一个独占锁文件,如果文件已存在则退出。

Bash/Shell 示例:

LOCKFILE="/tmp/myscript.lock"
# 强制删除过期锁(防止脚本崩溃后锁残留,建议保留)
if [ -f "$LOCKFILE" ]; then
    # 检查持有锁的进程是否还在运行
    if kill -0 $(cat "$LOCKFILE") 2>/dev/null; then
        echo "[ERROR] 脚本已在运行中 (PID: $(cat $LOCKFILE))"
        exit 1
    else
        # 进程已死,清理旧锁
        rm -f "$LOCKFILE"
    fi
fi
# 创建新锁,记录当前PID
echo $$ > "$LOCKFILE"
# ---- 在这里写你的任务逻辑 ----
sleep 30  # 模拟耗时任务
# -------------------------
# 任务结束,释放锁
rm -f "$LOCKFILE"

Python 示例(使用fcntl多进程安全):

import fcntl
import sys
lock_file = '/tmp/myapp.lock'
with open(lock_file, 'w') as f:
    try:
        fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except IOError:
        print("脚本已在运行中", file=sys.stderr)
        sys.exit(1)
    # 你的任务逻辑
    ...
    # 自动释放锁(with语句结束)

缺点:如果服务器重启,锁文件可能残留,可配合信号处理或定时清理。

数据库乐观锁 / 分布式锁(适合集群/微服务)

当多个服务实例可能同时触发任务时,需要共享的锁存储。

方案A:MySQL 行锁

-- 创建任务状态表
CREATE TABLE task_lock (
    task_name VARCHAR(50) PRIMARY KEY,
    status ENUM('idle', 'running', 'finished') DEFAULT 'idle',
    start_time DATETIME,
    node_id VARCHAR(50)
);
-- 获取锁(原子操作)
UPDATE task_lock 
SET status = 'running', start_time = NOW(), node_id = 'node-001'
WHERE task_name = 'daily_report' AND status = 'idle';
-- 如果影响行数>0,说明获取锁成功

方案B:Redis 分布式锁(高并发推荐)

import redis
import time
r = redis.Redis()
lock_key = "task:report:lock"
# 尝试获取锁,过期时间5分钟,防止死锁
if r.setnx(lock_key, "locked"):
    r.expire(lock_key, 300)  # 5分钟超时
    try:
        # 执行业务逻辑
        run_report()
    finally:
        r.delete(lock_key)  # 释放锁
else:
    print("任务正在其他节点执行中")

注意:使用 Redis 时务必设置过期时间,防止节点崩溃导致锁永久占用。

状态机 + 幂等性设计(最可靠)

从业务层面保证即使多次执行也不会产生副作用,这是最彻底的方案。

核心原则

  1. 任务具有唯一ID(如日期、批次号)。
  2. 执行前检查:去数据库/文件检查该ID是否已被处理。
  3. 执行后记录:将处理结果写入持久化存储。

示例:

def process_daily_order(today):
    # 1. 幂等性检查
    if db.exists("processed_dates", today):
        print(f"{today} 已处理过,跳过")
        return
    # 2. 执行实际逻辑
    data = fetch_orders(today)
    process(data)
    # 3. 记录已完成(事务性写入)
    db.insert("processed_dates", date=today, status="done")

优点:即使脚本被多次触发,或中途崩溃重启,也不会重复处理数据。

系统级调度工具(推荐用于定时任务)

如果任务是定时执行的,使用专业调度器比写脚本来控制重复更可靠。

Linux crontab 结合 flock

# 使用flock命令包裹,自动处理文件锁
* * * * * /usr/bin/flock -n /tmp/myjob.lock /usr/local/bin/my_script.sh

systemd timer + 服务单元

[Unit]
Description=My Daily Job
[Service]
Type=oneshot
ExecStart=/usr/local/bin/my_script.sh

专业调度系统:Airflow、Celery Beat、Jenkins等,自带去重和状态管理。

针对不同场景的建议

场景 最佳方案 原因
单机Cron任务 flock 命令 零代码侵入,Linux自带
Kubernetes Job Leader Election + 状态数据库 适合容器化环境
微服务定时任务 分布式锁 (Redis/Etcd) 跨节点协调
数据仓库ETL 幂等性设计 + 调度框架 数据准确性优先
低代码/快速开发 文件锁 + PID检测 实现简单,依赖少

最优雅的写法(Python + Redis)

如果你想找一个几乎通用的方案,可以考虑:

import redis
from contextlib import contextmanager
@contextmanager
def task_lock(task_id, ttl=600):
    """分布式任务锁上下文管理器"""
    r = redis.Redis()
    lock_key = f"lock:{task_id}"
    if not r.setnx(lock_key, 'locked'):
        raise RuntimeError(f"任务 {task_id} 已在执行")
    r.expire(lock_key, ttl)
    try:
        yield
    finally:
        r.delete(lock_key)
# 使用
with task_lock("daily_cleanup"):
    # 这里面的代码保证只会有一个实例执行
    run_cleanup()

为什么推荐这个?

  • 原子性:setnx 保证并发安全
  • 自动过期:防止死锁
  • 上下文管理:自动释放,不怕异常
  • 可扩展:只需改为 Redis Cluster 即可支持集群

不要试图在脚本内部通过“等待再检查”来规避重复——这只会增加不确定性。最可靠的方法是在外部设置一把锁(文件/数据/分布式),然后让脚本在获取锁失败时直接退出,加上幂等设计作为兜底,就能彻底解决重复执行问题。

抱歉,评论功能暂时关闭!