核心问题解答
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 参数说明
| 参数 | 说明 | 值范围 | 示例 |
|
| 月份 | 1-12 |
|
|
| 日期 | 1-31 |
|
|
| 星期 | 0-6 或 mon-sun |
|
|
| 小时 | 0-23 |
|
|
| 分钟 | 0-59 |
|
|
| 秒 | 0-59 |
|
|
| 微秒 | 0-999999 |
|
注意:
-
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次
)
实际应用建议
-
生产环境使用
unique=True:避免重复执行 -
重要任务使用幂等设计:即使意外重复执行也不会出问题
-
监控任务执行:记录日志,便于排查问题
-
合理设置超时:避免任务长时间阻塞
-
使用健康检查:
arq --check WorkerSettings
参考资源
-
arq 官方文档: https://arq-docs.helpmanual.io/
-
Cron 表达式参考: https://crontab.guru/
相关测试脚本:
"""
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
1198

被折叠的 条评论
为什么被折叠?



