第一次接触APScheduler时,我被它的灵活性惊艳到了。相比传统的crontab,这个Python库允许你直接用代码管理定时任务,特别适合需要动态调整任务场景的Web应用。记得去年做电商促销系统时,我们就是用它来动态调整秒杀活动的开始和结束时间。
安装APScheduler只需要一条命令:
bash复制pip install apscheduler
这个库有四个核心组件,我习惯用快递站来类比理解:
最简单的定时任务示例:
python复制from apscheduler.schedulers.blocking import BlockingScheduler
def my_job():
print("任务执行中...")
scheduler = BlockingScheduler()
scheduler.add_job(my_job, 'interval', seconds=10)
scheduler.start()
这个例子创建了一个每10秒执行一次的定时任务。实际项目中我建议加上时区配置,避免跨时区部署时出现问题:
python复制scheduler = BlockingScheduler(timezone='Asia/Shanghai')
默认的内存存储(MemoryJobStore)在开发时很方便,但生产环境我强烈推荐使用数据库存储。去年我们有个服务重启导致所有定时任务丢失,就是血的教训。
SQLAlchemy集成示例:
python复制from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
jobstores = {
'default': SQLAlchemyJobStore(url='postgresql://user:pass@localhost/dbname')
}
scheduler = BackgroundScheduler(jobstores=jobstores)
几个实用建议:
线程池和进程池的选择很有讲究:
配置示例:
python复制from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
executors = {
'default': ThreadPoolExecutor(20),
'processpool': ProcessPoolExecutor(5)
}
scheduler = BackgroundScheduler(executors=executors)
我遇到过的一个典型问题:某个数据处理任务耗时过长,导致后续任务堆积。解决方案是设置max_instances:
python复制scheduler.add_job(
data_processing_task,
'interval',
hours=1,
max_instances=2
)
在Flask中我通常这样初始化调度器:
python复制from flask import Flask
from apscheduler.schedulers.background import BackgroundScheduler
app = Flask(__name__)
def init_scheduler():
scheduler = BackgroundScheduler()
scheduler.add_job(...)
return scheduler
scheduler = init_scheduler()
@app.before_first_request
def start_scheduler():
scheduler.start()
@app.teardown_appcontext
def shutdown_scheduler(exception=None):
if scheduler.running:
scheduler.shutdown()
关键点:
Django项目推荐使用django-apscheduler这个第三方包:
python复制from django_apscheduler.jobstores import DjangoJobStore
scheduler = BackgroundScheduler()
scheduler.add_jobstore(DjangoJobStore(), 'default')
@register_job(scheduler, 'interval', hours=2)
def my_django_job():
from django.core.mail import send_mail
send_mail(...)
我在实际项目中总结的几个经验:
当需要跨多台服务器部署时,要特别注意任务重复执行的问题。我的解决方案是:
Redis锁示例:
python复制import redis
from contextlib import contextmanager
redis_client = redis.Redis()
@contextmanager
def job_lock(key, timeout=300):
lock = redis_client.lock(key, timeout=timeout)
acquired = lock.acquire(blocking=False)
try:
if acquired:
yield True
else:
yield False
finally:
if acquired:
lock.release()
def my_distributed_job():
with job_lock("job_key") as acquired:
if not acquired:
return
# 执行实际任务逻辑
完善的监控是生产环境的必备项。我通常采用:
Prometheus集成示例:
python复制from prometheus_client import Counter
JOB_SUCCESS = Counter('job_success', '成功执行的任务数')
JOB_FAILURE = Counter('job_failure', '失败的任务数')
def monitored_job():
try:
# 任务逻辑
JOB_SUCCESS.inc()
except Exception:
JOB_FAILURE.inc()
raise
生产环境必须考虑任务重复执行的情况。我常用的策略:
python复制def idempotent_job(job_id):
if get_job_status(job_id) == 'COMPLETED':
return
set_job_status(job_id, 'RUNNING')
try:
# 实际任务逻辑
set_job_status(job_id, 'COMPLETED')
except Exception:
set_job_status(job_id, 'FAILED')
raise
时区问题困扰了我很久,最终总结出这套方案:
python复制scheduler = BackgroundScheduler(
timezone='UTC',
job_defaults={
'misfire_grace_time': 3600,
'coalesce': True
}
)
遇到任务不执行的情况,我通常这样排查:
对于高频任务,这些优化很有效:
python复制scheduler.add_job(
high_frequency_job,
'interval',
seconds=30,
jitter=10 # 添加随机0-10秒延迟
)
我经常封装一套REST API来管理任务:
python复制@app.route('/jobs', methods=['POST'])
def add_job():
data = request.json
scheduler.add_job(
func=data['func'],
trigger=data['trigger'],
id=data['job_id'],
args=data.get('args', []),
kwargs=data.get('kwargs', {})
)
return jsonify({"status": "success"})
@app.route('/jobs/<job_id>', methods=['DELETE'])
def remove_job(job_id):
scheduler.remove_job(job_id)
return jsonify({"status": "success"})
复杂任务流可以通过信号量实现:
python复制from threading import Event
task1_done = Event()
task2_done = Event()
def task1():
# 任务1逻辑
task1_done.set()
def task2():
task1_done.wait()
# 任务2逻辑
task2_done.set()
scheduler.add_job(task1, 'cron', hour=1)
scheduler.add_job(task2, 'cron', hour=1)
定时任务的测试需要特殊处理:
python复制from unittest.mock import MagicMock
def test_my_job():
scheduler = MagicMock()
my_job_function(scheduler)
scheduler.add_job.assert_called_once()