ARQ 定时任务

核心问题解答

Q: 有多个消费者(worker)时,定时任务会运行一次还是多次?

A: 取决于 unique 参数:

  • unique=True(默认):即使有多个 worker,定时任务只会执行一次

    • arq 使用 Redis 事务确保任务唯一性

    • 第一个获取到任务的 worker 会执行,其他 worker 会跳过

  • unique=False:每个 worker 都会执行该任务

    • 适用于需要在每个 worker 上执行的场景(如缓存预热)

测试场景

1. 每月1号凌晨执行

cron(
    monthly_task,
    day=1,          # 每月1号
    hour=0,         # 0点
    minute=0,       # 0分
    second=0,       # 0秒
    unique=True,    # 多 worker 只执行一次
)

2. 每两小时执行

cron(
    hourly_task,
    hour={0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22},  # 每2小时
    minute=0,
    second=0,
    unique=True,
)

3. 指定时间执行(2026-1-23 15:50:00)

cron(
    specific_time_task,
    month={1},          # 1月
    day={23},           # 23号
    hour=15,            # 15点
    minute=50,          # 50分
    second=0,           # 0秒
    unique=True,
)

测试步骤

步骤 1: 运行测试脚本

python test_arq_cron_complete.py

这会:

  • 入队几个延迟任务

  • 显示配置的定时任务

  • 检查任务状态

步骤 2: 启动单个 Worker

arq test_arq_cron_complete.WorkerSettings

观察输出,确认定时任务正常执行。

步骤 3: 测试多个 Worker(关键测试)

在三个不同的终端中分别运行:

终端 1:

arq test_arq_cron_complete.WorkerSettings1

终端 2:

arq test_arq_cron_complete.WorkerSettings2

终端 3:

arq test_arq_cron_complete.WorkerSettings3

步骤 4: 观察输出

观察每分钟(minute='*')的输出:

unique=True 的任务(unique_task):

[唯一任务] 2026-01-23 15:30:00 - Worker worker1 执行唯一任务

只有一个 worker 会执行,其他两个不会

unique=False 的任务(non_unique_task):

[非唯一任务] 2026-01-23 15:30:00 - Worker worker1 执行非唯一任务
[非唯一任务] 2026-01-23 15:30:00 - Worker worker2 执行非唯一任务
[非唯一任务] 2026-01-23 15:30:00 - Worker worker3 执行非唯一任务

每个 worker 都会执行

Cron 参数说明

参数

说明

值范围

示例

month

月份

1-12

{1, 6} (1月和6月)

day

日期

1-31

{1, 15} (1号和15号)

weekday

星期

0-6 或 mon-sun

{1, 3, 5} (周二、四、六)

hour

小时

0-23

{9, 12, 18} (9点、12点、18点)

minute

分钟

0-59

{0, 30} (0分和30分)

second

0-59

0 (0秒)

microsecond

微秒

0-999999

123456 (默认)

注意:

  • None 相当于 crontab 的 *(每...)

  • 可以用集合 {} 指定多个值

  • second 默认为 0,避免每秒执行

  • microsecond 默认为 123456,避免整秒时的并发高峰

延迟任务 vs 定时任务

延迟任务(一次性)

# 延迟 10 秒执行
await redis.enqueue_job('task_name', _defer_by=10)
# 延迟 1 分钟执行
await redis.enqueue_job('task_name', _defer_by=timedelta(minutes=1))
# 指定时间执行
await redis.enqueue_job('task_name', _defer_until=datetime(2026, 1, 23, 15, 50, 0))

定时任务(周期性)

cron_jobs = [
    cron(task_name, hour={9, 12, 18}, minute=0),
]

区别:

  • 延迟任务:只执行一次,需要手动入队

  • 定时任务:周期性执行,worker 启动后自动调度

任务唯一性

使用 job_id 确保唯一性

# 第一次入队
job1 = await redis.enqueue_job('task_name', _job_id='my_unique_task')
# 返回: <arq job my_unique_task>
# 第二次入队(相同 job_id)
job2 = await redis.enqueue_task('task_name', _job_id='my_unique_task')
# 返回: None(任务已存在)

Cron 任务的 unique 参数

# unique=True(默认)
cron(task_name, minute='*', unique=True)
# 多个 worker 只执行一次
# unique=False
cron(task_name, minute='*', unique=False)
# 每个 worker 都执行

常见问题

Q: 如何修改定时任务时间?

A: 修改 WorkerSettings.cron_jobs 配置,然后重启 worker。

Q: 如何停止定时任务?

A: 从 cron_jobs 列表中移除该任务,然后重启 worker。

Q: 如何查看定时任务下次执行时间?

A: arq 不会存储定时任务的执行时间,但可以通过日志观察。

Q: 定时任务执行失败会重试吗?

A: 默认 max_tries=1,不会重试。如需重试:

cron(
    task_name,
    minute='*',
    max_tries=3,  # 最多重试3次
)

实际应用建议

  1. 生产环境使用 unique=True:避免重复执行

  2. 重要任务使用幂等设计:即使意外重复执行也不会出问题

  3. 监控任务执行:记录日志,便于排查问题

  4. 合理设置超时:避免任务长时间阻塞

  5. 使用健康检查:arq --check WorkerSettings

参考资源


相关测试脚本:

"""
arq 定时任务完整测试 Demo

测试场景:
1. 每月1号凌晨执行
2. 每两小时执行
3. 指定时间执行(2026-1-23 15:50:00)
4. 测试多个 worker 情况下定时任务的执行次数
"""
import asyncio
from datetime import datetime, timedelta, timezone
from arq import create_pool, cron
from arq.connections import RedisSettings

from src.core.config import settings

# ==================== 定时任务函数 ====================

async def monthly_task(ctx):
    """每月1号凌晨执行的任务"""
    print(f'[每月任务] {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - 执行每月1号凌晨的任务')
    # 模拟业务逻辑
    await asyncio.sleep(1)
    return f'monthly_task_completed_{datetime.now().strftime("%Y%m%d")}'


async def hourly_task(ctx):
    """每两小时执行的任务"""
    print(f'[两小时任务] {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - 执行每两小时的任务')
    # 模拟业务逻辑
    await asyncio.sleep(1)
    return f'hourly_task_completed_{datetime.now().strftime("%Y%m%d_%H")}'


async def specific_time_task(ctx):
    """指定时间执行的任务(2026-1-23 15:50:00)"""
    print(f'[指定时间任务] {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - 执行指定时间的任务')
    # 模拟业务逻辑
    await asyncio.sleep(1)
    return f'specific_time_task_completed_{datetime.now().strftime("%Y%m%d_%H%M%S")}'


async def unique_task(ctx):
    """测试 unique 参数的任务"""
    worker_id = ctx.get('worker_id', 'unknown')
    print(f'[唯一任务] {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - Worker {worker_id} 执行唯一任务')
    await asyncio.sleep(1)
    return f'unique_task_worker_{worker_id}_completed'


async def non_unique_task(ctx):
    """测试 non-unique 参数的任务"""
    worker_id = ctx.get('worker_id', 'unknown')
    print(f'[非唯一任务] {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - Worker {worker_id} 执行非唯一任务')
    await asyncio.sleep(1)
    return f'non_unique_task_worker_{worker_id}_completed'


# ==================== Worker 启动/关闭钩子 ====================

async def startup(ctx):
    """Worker 启动时执行"""
    worker_id = ctx.get('worker_id', 'default')
    print(f'[启动] Worker {worker_id} 启动于 {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    ctx['startup_time'] = datetime.now()


async def shutdown(ctx):
    """Worker 关闭时执行"""
    worker_id = ctx.get('worker_id', 'default')
    startup_time = ctx.get('startup_time')
    runtime = datetime.now() - startup_time if startup_time else timedelta(0)
    print(f'[关闭] Worker {worker_id} 关闭于 {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}, 运行时长: {runtime}')


# ==================== Worker 配置 ====================

class WorkerSettings:
    """Worker 配置类 - 基础版本"""
    functions = [
        monthly_task,
        hourly_task,
        specific_time_task,
        unique_task,
        non_unique_task,
    ]
    
    on_startup = startup
    on_shutdown = shutdown
    
    # Redis 配置 - 必须作为类属性
    redis_settings = RedisSettings(
        host=settings.redis.host,
        port=settings.redis.port,
        password=settings.redis.password if settings.redis.password else None,
        database=settings.redis.database,
        conn_timeout=5,
        conn_retry_delay=2,
        conn_retries=5,
    )
    
    # 设置时区,避免时区相关的错误
    timezone = timezone.utc
    
    # 定时任务配置
    cron_jobs = [
        # 1. 每月1号凌晨执行(0点0分)
        cron(
            monthly_task,
            day=1,
            hour=0,
            minute=0,
            second=0,
            unique=True,  # 多个 worker 只执行一次
            name='monthly_1st_midnight'
        ),
        
        # 2. 每两小时执行(0点、2点、4点、...、22点)
        cron(
            hourly_task,
            hour={0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22},
            minute=0,
            second=0,
            unique=True,
            name='every_two_hours'
        ),
        
        # 3. 指定时间执行(2026-1-23 15:50:00)- 一次性任务
        cron(
            specific_time_task,
            month={1},
            day={23},
            hour=15,
            minute=50,
            second=0,
            unique=True,
            name='specific_time_20260123_155000'
        ),
        
        # 4. 测试 unique=True(多 worker 只执行一次)- 每分钟执行
        cron(
            unique_task,
            minute=None,  # None 表示每分钟执行
            unique=True,
            name='unique_test_task'
        ),
        
        # 5. 测试 unique=False(每个 worker 都执行)- 每分钟执行
        cron(
            non_unique_task,
            minute=None,  # None 表示每分钟执行
            unique=False,
            name='non_unique_test_task'
        ),
    ]


class WorkerSettings1(WorkerSettings):
    """Worker 1 配置"""
    # 显式声明继承的属性,避免继承问题
    functions = WorkerSettings.functions
    cron_jobs = WorkerSettings.cron_jobs
    redis_settings = WorkerSettings.redis_settings
    on_startup = WorkerSettings.on_startup
    on_shutdown = WorkerSettings.on_shutdown
    timezone = WorkerSettings.timezone


class WorkerSettings2(WorkerSettings):
    """Worker 2 配置"""
    functions = WorkerSettings.functions
    cron_jobs = WorkerSettings.cron_jobs
    redis_settings = WorkerSettings.redis_settings
    on_startup = WorkerSettings.on_startup
    on_shutdown = WorkerSettings.on_shutdown
    timezone = WorkerSettings.timezone


class WorkerSettings3(WorkerSettings):
    """Worker 3 配置"""
    functions = WorkerSettings.functions
    cron_jobs = WorkerSettings.cron_jobs
    redis_settings = WorkerSettings.redis_settings
    on_startup = WorkerSettings.on_startup
    on_shutdown = WorkerSettings.on_shutdown
    timezone = WorkerSettings.timezone


# ==================== 测试脚本 ====================

async def test_enqueue_deferred_jobs():
    """测试入队延迟任务"""
    redis = await create_pool(WorkerSettings.redis_settings)
    
    print('\n========== 测试入队延迟任务 ==========')
    
    # 1. 延迟 10 秒执行
    job1 = await redis.enqueue_job('monthly_task', _defer_by=10)
    print(f'✓ 已入队延迟10秒的任务: {job1.job_id if job1 else "任务已存在"}')
    
    # 2. 延迟 30 秒执行
    job2 = await redis.enqueue_job('hourly_task', _defer_by=30)
    print(f'✓ 已入队延迟30秒的任务: {job2.job_id if job2 else "任务已存在"}')
    
    # 3. 指定时间执行(当前时间 + 1 分钟)
    future_time = datetime.now() + timedelta(minutes=1)
    job3 = await redis.enqueue_job('specific_time_task', _defer_until=future_time)
    print(f'✓ 已入队指定时间的任务: {job3.job_id if job3 else "任务已存在"}')
    
    # 4. 使用 job_id 确保唯一性
    job4 = await redis.enqueue_job('monthly_task', _job_id='unique_monthly_task')
    print(f'✓ 已入队唯一ID的任务: {job4.job_id if job4 else "任务已存在"}')
    
    # 5. 尝试再次入队相同 job_id 的任务(应该返回 None)
    job5 = await redis.enqueue_job('monthly_task', _job_id='unique_monthly_task')
    print(f'✓ 尝试入队重复ID的任务: {job5.job_id if job5 else "任务已存在(预期结果)"}')
    
    await redis.aclose()


async def test_cron_schedules():
    """测试定时任务调度"""
    redis = await create_pool(WorkerSettings.redis_settings)
    
    print('\n========== 测试定时任务调度 ==========')
    print('配置的定时任务:')
    print('  1. 每月1号凌晨0点执行')
    print('  2. 每两小时执行(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22点)')
    print('  3. 2026年1月23日 15:50:00 执行(一次性)')
    print('  4. 每分钟执行(unique=True,多 worker 只执行一次)')
    print('  5. 每分钟执行(unique=False,每个 worker 都执行)')
    
    await redis.aclose()


async def test_check_jobs():
    """检查任务状态"""
    from arq.jobs import Job
    
    redis = await create_pool(WorkerSettings.redis_settings)
    
    print('\n========== 检查任务状态 ==========')
    
    # 入队一个测试任务
    job = await redis.enqueue_job('monthly_task', _defer_by=5)
    if job:
        print(f'任务 ID: {job.job_id}')
        
        # 等待任务完成
        try:
            result = await job.result(timeout=10)
            print(f'任务结果: {result}')
        except Exception as e:
            print(f'任务执行出错: {e}')
    
    await redis.aclose()


async def main():
    """主函数"""
    print('=' * 60)
    print('arq 定时任务完整测试 Demo')
    print('=' * 60)
    
    # 运行测试
    await test_enqueue_deferred_jobs()
    await test_cron_schedules()
    await test_check_jobs()
    
    print('\n' + '=' * 60)
    print('测试完成!')
    print('=' * 60)
    print('\n启动 Worker 的命令:')
    print('  单个 Worker:')
    print('    arq test_arq_cron_complete.WorkerSettings')
    print('\n  多个 Worker 测试(在不同终端运行):')
    print('    arq test_arq_cron_complete.WorkerSettings1')
    print('    arq test_arq_cron_complete.WorkerSettings2')
    print('    arq test_arq_cron_complete.WorkerSettings3')
    print('\n  使用 burst 模式(处理完所有任务后退出):')
    print('    arq test_arq_cron_complete.WorkerSettings --burst')
    print('\n  监控模式(自动重载):')
    print('    arq test_arq_cron_complete.WorkerSettings --watch')
    print('=' * 60)


if __name__ == '__main__':
    asyncio.run(main())


# # 1. 运行测试脚本
#   python test_arq_cron_complete.py
#
#   # 2. 启动单个 worker
#   arq test_arq_cron_complete.WorkerSettings
#
#   # 3. 测试多 worker(在3个不同终端运行)
#   arq test_arq_cron_complete.WorkerSettings1
#   arq test_arq_cron_complete.WorkerSettings2
#   arq test_arq_cron_complete.WorkerSettings3
#   arq test_arq_cron_complete.WorkerSettings3

# 启动 Worker 的命令:
#   单个 Worker:
#     arq test_arq_cron_complete.WorkerSettings
#
#   多个 Worker 测试(在不同终端运行):
#     arq test_arq_cron_complete.WorkerSettings1
#     arq test_arq_cron_complete.WorkerSettings2
#     arq test_arq_cron_complete.WorkerSettings3
#
#   使用 burst 模式(处理完所有任务后退出):
#     arq test_arq_cron_complete.WorkerSettings --burst
#
#   监控模式(自动重载):
#     arq test_arq_cron_complete.WorkerSettings --watch

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值