最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

從入門到避坑解析Python FastAPI定時(shí)任務(wù)全攻略

 更新時(shí)間:2026年02月04日 11:34:45   作者:一名程序媛呀  
這篇文章主要為大家詳細(xì)介紹了Python FastAPI實(shí)現(xiàn)定時(shí)任務(wù)的相關(guān)方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

先說(shuō)事實(shí)案例:有個(gè)促銷活動(dòng)需要定時(shí)上線。結(jié)果呢?依賴的云函數(shù)服務(wù)突然抖動(dòng),那個(gè)“簡(jiǎn)單可靠”的crontab腳本愣是沒(méi)觸發(fā)。凌晨三點(diǎn),運(yùn)營(yíng)的電話直接把你的美夢(mèng)干碎。事后復(fù)盤,才意識(shí)到:把定時(shí)任務(wù)寄生于操作系統(tǒng)或者外部黑盒服務(wù),在微服務(wù)架構(gòu)里,就是給自己埋雷。

痛定思痛,最后把定時(shí)任務(wù)“請(qǐng)”回了應(yīng)用內(nèi)部,用APScheduler在FastAPI里搞了個(gè)自治的小鬧鐘。今天,咱們就來(lái)聊聊這套實(shí)戰(zhàn)經(jīng)驗(yàn),連同那些半夜爬起來(lái)填的坑……

本文你能得到什么

1. 為什么說(shuō)FastAPI自帶的BackgroundTasks不適合做定時(shí)任務(wù)。

2. APScheduler的核心概念,用“鬧鐘”和“餐廳”的比喻讓你秒懂。

3. 手把手集成,提供可直接復(fù)制粘貼的代碼塊。

4. 最重要的:多進(jìn)程部署(比如用Uvicorn workers)時(shí),定時(shí)任務(wù)重復(fù)執(zhí)行的“鬼故事”與解決之道。

 第一部分:?jiǎn)栴}與背景 —— 為什么另起爐灶?

FastAPI 的 BackgroundTasks 是個(gè)好同志,但它只是個(gè)“跑腿小哥”。你API請(qǐng)求來(lái)了,它幫你異步處理些雜事,比如發(fā)郵件、寫日志。但它有個(gè)硬傷:它沒(méi)有記憶,也不會(huì)看表。 服務(wù)一重啟,所有計(jì)劃內(nèi)的“跑腿”任務(wù)全忘光光。

定時(shí)任務(wù)呢?它需要的是“忠誠(chéng)的管家”。不管服務(wù)是否重啟,都要記得每天上午10點(diǎn)要發(fā)報(bào)表,每周一凌晨要清緩存。這需要持久化和時(shí)間調(diào)度能力,這正是 APScheduler 的絕活。

你可能會(huì)問(wèn),用Celery行不行?行,但殺雞用牛刀了。APScheduler更輕量,與你FastAPI應(yīng)用同生共死,管理起來(lái)簡(jiǎn)單直接,特別適合業(yè)務(wù)邏輯清晰、不需要分布式協(xié)調(diào)的定時(shí)場(chǎng)景。

第二部分:核心原理 —— APScheduler的三板斧

別被它的名字嚇到,把它想象成一個(gè)高度可定制的智能鬧鐘系統(tǒng)。它主要由三部分組成:

觸發(fā)器 (Trigger): 決定“什么時(shí)候響”。是每天固定時(shí)間(date),還是間隔固定時(shí)間(interval),或者是像crontab那樣的復(fù)雜周期(cron)?

作業(yè)存儲(chǔ)器 (Job Store): 記住“有哪些鬧鐘要響”。默認(rèn)存在內(nèi)存里,重啟就忘。我們可以讓它記在數(shù)據(jù)庫(kù)里(比如SQLite、PostgreSQL),實(shí)現(xiàn)持久化。

執(zhí)行器 (Executor): 負(fù)責(zé)“鬧鐘響了以后具體做什么”。是用線程池還是進(jìn)程池來(lái)執(zhí)行我們的任務(wù)函數(shù)?

調(diào)度器 (Scheduler) 就是總控臺(tái),把上面三個(gè)部件組裝起來(lái),并啟動(dòng)這個(gè)鬧鐘系統(tǒng)。

第三部分:實(shí)戰(zhàn)演示 —— 手把手集成到FastAPI

好,咱們先來(lái)安裝。這步最簡(jiǎn)單:

pip install apscheduler

接下來(lái)重點(diǎn)來(lái)了,初始化并集成到FastAPI的生命周期。這里有個(gè)關(guān)鍵技巧:一定要把scheduler的啟動(dòng)和關(guān)閉掛在FastAPI的應(yīng)用事件上,保證應(yīng)用啟動(dòng)時(shí)它啟動(dòng),應(yīng)用優(yōu)雅關(guān)閉時(shí)它也停下。千萬(wàn)別學(xué)我當(dāng)初,直接在模塊層面scheduler.start(),導(dǎo)致測(cè)試時(shí)腳本跑完不退出。

# 項(xiàng)目結(jié)構(gòu)建議
# app/
#   __init__.py
#   main.py       # FastAPI 應(yīng)用創(chuàng)建和事件處理
#   scheduler.py  # 調(diào)度器配置和任務(wù)定義
#   tasks.py      # 具體的任務(wù)函數(shù)

# app/scheduler.py
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ThreadPoolExecutor

# 1. 配置組件
jobstores = {
    'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')  # 使用SQLite持久化
}
executors = {
    'default': ThreadPoolExecutor(20)  # 線程池執(zhí)行
}
job_defaults = {
    'coalesce': False,  # 錯(cuò)過(guò)的任務(wù)是否合并執(zhí)行(一般False)
    'max_instances': 3  # 同一個(gè)任務(wù)同時(shí)運(yùn)行的最大實(shí)例數(shù)
}

# 2. 創(chuàng)建調(diào)度器實(shí)例
scheduler = AsyncIOScheduler(
    jobstores=jobstores,
    executors=executors,
    job_defaults=job_defaults,
    timezone="Asia/Shanghai"  # 時(shí)區(qū)!時(shí)區(qū)!時(shí)區(qū)!重要的事說(shuō)三遍
)

# 3. 定義任務(wù)函數(shù) (可以放在同文件,也可從其他模塊導(dǎo)入)
def my_sync_job():
    print("同步任務(wù)執(zhí)行了!")

async def my_async_job():
    print("異步任務(wù)執(zhí)行了!")
    # 這里可以愉快地調(diào)用其他async函數(shù)

# 4. 添加任務(wù)的函數(shù) (通常在應(yīng)用啟動(dòng)時(shí)調(diào)用)
def add_jobs():
    # 間隔任務(wù):每30秒執(zhí)行一次
    scheduler.add_job(my_sync_job, 'interval', seconds=30, id='sync_interval_job')
    # Cron任務(wù):每分鐘的第30秒執(zhí)行
    scheduler.add_job(my_async_job, 'cron', second=30, id='async_cron_job')
    # 單次任務(wù):2023年10月1日?qǐng)?zhí)行
    # scheduler.add_job(xxx, 'date', run_date='2023-10-01 00:00:00')    

然后在你的main.py里,把它和FastAPI綁在一起:

# app/main.py
from fastapi import FastAPI
from .scheduler import scheduler, add_jobs

app = FastAPI(title="定時(shí)任務(wù)演示")

@app.on_event("startup")
async def startup_event():
    # 應(yīng)用啟動(dòng)時(shí),添加任務(wù)并啟動(dòng)調(diào)度器
    if not scheduler.running:
        add_jobs()
        scheduler.start()
        print("APScheduler 已啟動(dòng)")

@app.on_event("shutdown")
async def shutdown_event():
    # 應(yīng)用關(guān)閉時(shí),優(yōu)雅地關(guān)閉調(diào)度器
    if scheduler.running:
        scheduler.shutdown()
        print("APScheduler 已關(guān)閉")

#from contextlib import asynccontextmanager
#@asynccontextmanager
#async def lifespan(app: FastAPI):
#    # 應(yīng)用啟動(dòng)時(shí),添加任務(wù)并啟動(dòng)調(diào)度器
#    if not scheduler.running:
#        add_jobs()
#        scheduler.start()
#        print("APScheduler 已啟動(dòng)")
#    yield
#    # 應(yīng)用關(guān)閉時(shí),優(yōu)雅地關(guān)閉調(diào)度器
#    if scheduler.running:
#        scheduler.shutdown()
#        print("APScheduler 已關(guān)閉")
#app = FastAPI(title="定時(shí)任務(wù)演示", lifespan=lifespan)

@app.get("/")
async def root():
    return {"message": "Hello World"}

# 可選:提供一個(gè)API來(lái)手動(dòng)觸發(fā)或查看任務(wù)狀態(tài)
@app.get("/jobs")
async def list_jobs():
    jobs = scheduler.get_jobs()
    return {"jobs": [{"id": j.id, "next_run": str(j.next_run_time)} for j in jobs]}    

這里保留了舊式的on_event生命周期管理函數(shù),方便理解scheduler的開(kāi)啟與關(guān)閉邏輯,開(kāi)發(fā)時(shí)改為lifespan進(jìn)行更優(yōu)雅的生命周期管理。

跑起來(lái)試試吧!你會(huì)看到控制臺(tái)每隔30秒和每分鐘的第30秒都有輸出。到數(shù)據(jù)庫(kù)里看看,jobs.sqlite里已經(jīng)存下了我們的任務(wù)配置,重啟應(yīng)用任務(wù)也不會(huì)丟失。

第四部分:天坑預(yù)警 —— 多進(jìn)程部署與重復(fù)執(zhí)行

是不是以為這樣就萬(wàn)事大吉了?最大的坑才剛剛浮出水面。

當(dāng)你用生產(chǎn)模式啟動(dòng)FastAPI,比如:

uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4

--workers 4意味著啟動(dòng)了4個(gè)獨(dú)立的進(jìn)程。那么,app.on_event("startup")會(huì)在這4個(gè)進(jìn)程里各執(zhí)行一次!結(jié)果就是,你的定時(shí)任務(wù)被添加了4次,會(huì)被重復(fù)執(zhí)行4次!想象一下,每小時(shí)發(fā)一次的報(bào)表郵件,突然變成了每小時(shí)發(fā)四封,老板和用戶都會(huì)瘋掉。

解決方案:文件鎖與領(lǐng)導(dǎo)者選舉

核心思路很簡(jiǎn)單:確保在多個(gè)進(jìn)程中,只有一個(gè)進(jìn)程能真正啟動(dòng)和添加定時(shí)任務(wù)。 這里分享兩種我們線上在用的方法。

方案一:簡(jiǎn)單粗暴的文件鎖(適合大部分場(chǎng)景)

利用fcntl(Linux)或msvcrt(Windows)給一個(gè)文件加鎖,只有拿到鎖的進(jìn)程才能初始化調(diào)度器。

# 在 scheduler.py 或 startup 事件中
import os
import sys

def try_acquire_lock(lock_file):
    try:
        import fcntl
        f = open(lock_file, 'w')
        # 嘗試獲取非阻塞的獨(dú)占鎖
        fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
        return f  # 返回文件對(duì)象,保持打開(kāi)狀態(tài)以持有鎖
    except (BlockingIOError, ImportError):
        # 獲取失?。ㄆ渌M(jìn)程已持有鎖)或不支持的系統(tǒng)
        return None

lock_file = "/tmp/fastapi_scheduler.lock"
lock_fd = try_acquire_lock(lock_file)

@app.on_event("startup")
async def startup_event():
    if lock_fd is not None:
        # 只有拿到鎖的進(jìn)程才啟動(dòng)調(diào)度器
        if not scheduler.running:
            add_jobs()
            scheduler.start()
            print(f"進(jìn)程 {os.getpid()} 成功啟動(dòng) APScheduler")
    else:
        print(f"進(jìn)程 {os.getpid()} 未獲得鎖,跳過(guò)調(diào)度器啟動(dòng)")        

方案二:利用數(shù)據(jù)庫(kù)原子操作(更分布式)

在數(shù)據(jù)庫(kù)里建一張表,用原子性的“插入或競(jìng)爭(zhēng)”操作來(lái)選舉一個(gè)“領(lǐng)導(dǎo)者”進(jìn)程。

# 假設(shè)使用SQLAlchemy ORM
from sqlalchemy.ext.asyncio import AsyncSession
from your_app.models import SchedulerLock
import datetime

async def acquire_db_lock(session: AsyncSession, timeout_minutes=10):
    try:
        # 嘗試插入一條鎖記錄,host和pid標(biāo)識(shí)當(dāng)前進(jìn)程
        lock = SchedulerLock(
            id=1,  # 固定ID
            host="my_host",
            pid=os.getpid(),
            last_heartbeat=datetime.datetime.utcnow()
        )
        session.add(lock)
        await session.commit()
        return True  # 插入成功,獲得鎖
    except IntegrityError:  # 唯一約束沖突,記錄已存在
        await session.rollback()
        # 檢查已有的鎖是否已過(guò)期
        existing_lock = await session.get(SchedulerLock, 1)
        if existing_lock and (datetime.datetime.utcnow() - existing_lock.last_heartbeat).seconds > timeout_minutes * 60:
            # 鎖已過(guò)期,更新為當(dāng)前進(jìn)程
            existing_lock.host = "my_host"
            existing_lock.pid = os.getpid()
            existing_lock.last_heartbeat = datetime.datetime.utcnow()
            await session.commit()
            return True
        return False  # 未能獲得鎖

# 在 startup 事件中調(diào)用 acquire_db_lock 判斷        

記住,多進(jìn)程部署下定時(shí)任務(wù)初始化,不加鎖等于制造線上事故。 我個(gè)人更推薦方案一,足夠簡(jiǎn)單可靠,除非你已經(jīng)是跨機(jī)器的分布式部署了。

最后啰嗦一句

定時(shí)任務(wù)看似是小功能,但把它做可靠卻需要處處留心。從選擇APScheduler,到正確集成到應(yīng)用生命周期,再到最后用文件鎖避開(kāi)多進(jìn)程的坑,每一步都是我們踩過(guò)的雷。

技術(shù)棧沒(méi)有銀彈,但有了這套組合拳,你的FastAPI后臺(tái)定時(shí)任務(wù),基本可以高枕無(wú)憂了。至少,能讓你睡個(gè)安穩(wěn)覺(jué),不用再擔(dān)心凌晨三點(diǎn)的電話。

到此這篇關(guān)于從入門到避坑解析Python FastAPI定時(shí)任務(wù)全攻略的文章就介紹到這了,更多相關(guān)Python FastAPI定時(shí)任務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

哈尔滨市| 汝城县| 安达市| 新巴尔虎右旗| 霍邱县| 新源县| 延津县| 东乌珠穆沁旗| 牙克石市| 绥滨县| 渭源县| 疏勒县| 墨玉县| 图木舒克市| 汶上县| 黄石市| 连云港市| 桃江县| 巨野县| 沛县| 青河县| 龙川县| 宁海县| 六枝特区| 邹城市| 赤峰市| 古丈县| 大港区| 旺苍县| 马鞍山市| 乌拉特前旗| 大丰市| 张掖市| 彰武县| 彭水| 姜堰市| 兴宁市| 乌拉特前旗| 开平市| 德令哈市| 秭归县|