在大数据服务接口面临大量数据查询和下载得时候,我们为了控制服务得资源占用,要考虑防止同维度重复下载得功能。
我这里得背景是 业务方下载大量明细数据得时候,需要控制每个处理人、所属经营主体、所属门店、所查询得时间窗口 得下载频率。
我使用的架构是 FastApi
在FastAPI路由处理函数中,需要添加一个30秒内防止同维度重复下载的功能。
我当前的代码已经使用了BackgroundTasks来启动后台任务,但希望在出现重复请求时返回错误并且给出提示,而不是重复执行任务。
原本的代码:
# 生成下载清单接口
@router.post("/xxxx")
async def create_cloth_state_download_list(cloth_state_model: ClothStateConfigModel, background_tasks: BackgroundTasks):
try:
# 使用 BackgroundTasks 在后台运行下载任务
background_tasks.add_task(ttDownloadClothStateTimeService, cloth_state_model)
return {"code": 200, "message": "下载任务已在后台启动"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
通过 内存缓存+时间戳标记 实现
cloth_state_model
中的关键字段(如经营主体/处理人/时间)作为唯一标识asyncio.Lock
防止多线程并发问题pydantic模型: 此模型是用户筛选数据的条件维度
class ClothStateConfigModel(BaseModel):
date_min: str # 筛选时间
date_max: str
is_directly: str
branch_office: str # 城市
shop_name: str
pageNum: int
pageSize: int
apply_operator_name: str # 处理人 名称
cloth_type: str # 衣物大类 20250225加入
公共类声明:
我们把用户筛选的条件存入缓存
# 内存缓存结构:{ "branch_office:xxx|apply_operator_name:yyy|date_min:zzz"|date_max:zzz": 时间戳 }
download_cache = {}
cache_lock = asyncio.Lock() # 异步锁 安装asyncio
CACHE_TIMEOUT = 30 # 单位:秒
维度标识生成
generate_cache_key()
提取模型中的关键业务字段(示例为经营主体/处理人/时间)def generate_cache_key(model: ClothStateConfigModel) -> str:
"""生成唯一缓存标识(根据业务维度自定义)"""
return f"branch_office:{model.branch_office}|apply_operator_name:{model.apply_operator_name}|date_min:{model.date_min}|date_max:{model.date_max}"
缓存自动清理
# 清理过期缓存(每次有新请求时清理)
expired_keys = [
k for k, t in download_cache.items()
if current_time - t > CACHE_TIMEOUT
]
for k in expired_keys:
del download_cache[k]
异步锁保护
asyncio.Lock
确保多线程下对缓存的修改是原子操作 async with cache_lock: # 加锁保证线程安全
# 清理过期缓存(每次有新请求时清理)
expired_keys = [
k for k, t in download_cache.items()
if current_time - t > CACHE_TIMEOUT
]
for k in expired_keys:
del download_cache[k]
# 检查是否已有重复请求
if cache_key in download_cache:
# 计算剩余冷却时间
remaining_time = CACHE_TIMEOUT - (current_time - download_cache[cache_key])
raise HTTPException(
status_code=429,
detail=f"30秒内请勿重复请求相同维度下载,剩余等待时间: {int(remaining_time)}秒"
)
# 记录新请求
download_cache[cache_key] = current_time
精准错误提示
# 失败时移除缓存
async with cache_lock:
if cache_key in download_cache:
del download_cache[cache_key]
Redis缓存
高并发场景下,可将缓存迁移到 Redis,实现多实例共享和自动过期
# 示例:使用 Redis 的 EXPIRE 命令
await redis.setex(cache_key, CACHE_TIMEOUT, current_time)
日志监控
记录重复请求日志,用于后续分析
logger.warning(f"重复请求被拦截: {cache_key}")
动态超时配置
通过环境变量动态调整超时时间
CACHE_TIMEOUT = int(os.getenv("DOWNLOAD_CACHE_TIMEOUT", 30))
通过这种设计,既能有效防止短时间内的重复请求,又能保持接口的响应速度和可维护性。
欢迎大家指出可优化的地方
import asyncio as asyncio
from pydantic import BaseModel
class ClothStateConfigModel(BaseModel):
date_min: str
date_max: str
is_directly: str
branch_office: str
shop_name: str
pageNum: int
pageSize: int
apply_operator_name: str # 处理人 名称
cloth_type: str # 衣物大类 20250225加入
# 内存缓存结构:{ "branch_office:xxx|apply_operator_name:yyy|date_min:zzz"|date_max:zzz": 时间戳 }
download_cache = {}
cache_lock = asyncio.Lock() # 异步锁 安装asyncio
CACHE_TIMEOUT = 30 # 单位:秒
def generate_cache_key(model: ClothStateConfigModel) -> str:
"""生成唯一缓存标识(根据业务维度自定义)"""
return f"branch_office:{model.branch_office}|apply_operator_name:{model.apply_operator_name}|date_min:{model.date_min}|date_max:{model.date_max}"
# 生成下载清单接口
@router.post("/createClothStateDownloadList")
async def create_cloth_state_download_list(cloth_state_model: ClothStateConfigModel, background_tasks: BackgroundTasks):
cache_key = generate_cache_key(cloth_state_model)
current_time = time.time()
async with cache_lock: # 加锁保证线程安全
# 清理过期缓存(每次有新请求时清理)
expired_keys = [
k for k, t in download_cache.items()
if current_time - t > CACHE_TIMEOUT
]
for k in expired_keys:
del download_cache[k]
# 检查是否已有重复请求
if cache_key in download_cache:
# 计算剩余冷却时间
remaining_time = CACHE_TIMEOUT - (current_time - download_cache[cache_key])
raise HTTPException(
status_code=429,
detail=f"30秒内请勿重复请求相同维度下载,剩余等待时间: {int(remaining_time)}秒"
)
# 记录新请求
download_cache[cache_key] = current_time
try:
# 使用 BackgroundTasks 在后台运行下载任务
background_tasks.add_task(ttDownloadClothStateTimeService, cloth_state_model)
return {"code": 200, "message": "下载任务已在后台启动"}
except Exception as e:
# 失败时移除缓存
async with cache_lock:
if cache_key in download_cache:
del download_cache[cache_key]
raise HTTPException(status_code=500, detail=str(e))