1. 问题背景:多进程部署的定时任务困境
在FastAPI生产环境部署中,使用Uvicorn或Gunicorn启动多个工作进程(Worker)是提升并发处理能力的标准做法。但许多开发者第一次将应用从开发环境迁移到生产环境时,都会遇到一个令人头疼的问题——原本在单进程中运行良好的定时任务,突然开始重复执行。
这种情况就像是在一家连锁餐厅的后厨,原本只有一个厨师负责每日食材盘点,现在突然变成了四个厨师同时进行盘点。每个厨师都严格按照排班表工作,结果同一批食材被清点了四次,不仅浪费人力,更可能导致后续的库存统计完全混乱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多进程环境下的三大典型问题
2.1 定时任务重复触发
当使用BackgroundScheduler或asyncio创建定时任务时,每个工作进程都会独立初始化并运行自己的任务调度器。这就好比给每个分店都配备了一个完全相同的闹钟,到点就会同时响起。如果你设置了每小时清理临时文件的任务,实际上每小时会执行N次(N等于Worker数量)。
实际案例:某外卖平台后台使用
gunicorn -w 4启动服务后,凌晨的临时文件清理任务被执行了四次,险些删除正在上传的商户Logo文件。
2.2 共享资源竞争冲突
多进程同时读写共享资源(如文件、缓存等)时,会出现经典的竞态条件问题。想象两个进程同时打开同一个日志文件:
- 进程A打开文件,准备写入
- 进程B也打开同一文件
- 进程A写入一行日志
- 进程B也写入一行日志
- 结果:进程A的写入被覆盖,日志丢失
2.3 依赖项重复初始化
在全局范围或依赖项中初始化的昂贵资源(如数据库连接池、机器学习模型等),会被每个工作进程独立初始化一次。这不仅浪费资源,还可能导致:
- 数据库连接数迅速耗尽
- 内存占用成倍增长
- 初始化耗时显著增加
3. 解决方案:跨进程锁机制
3.1 为什么线程锁不够用?
Python标准库中的threading.Lock只能在单个进程内的多个线程间同步。在多进程部署场景下,每个Worker都是独立的Python解释器进程,内存空间完全隔离,线程锁无法跨进程生效。
3.2 跨进程锁的工作原理
跨进程锁的核心思想是:利用所有进程都能访问的共享资源作为同步媒介。常见的实现方式包括:
- 文件锁:通过操作系统提供的文件锁定机制
- Redis锁:利用Redis的原子操作
- 数据库锁:通过数据库的行锁或乐观锁
本文将重点介绍最通用、依赖最少的文件锁实现方案。
4. 文件锁的完整实现
4.1 基础实现代码
python复制import os
import sys
import time
class FileLock:
"""跨进程文件锁实现"""
def __init__(self, lock_file: str):
self.lock_file = lock_file
self._fd = None
def acquire(self, blocking: bool = True, timeout: float = 10):
"""
