测试标题001:Python 异步任务队列的工程化实践
一、背景与痛点
在现代后端服务中,耗时操作(邮件发送、图片处理、第三方回调)如果直接阻塞在请求链路里,会拖垮接口时延。以 Python 为例,requests 同步调用在 QPS 较高时容易造成线程堆积。本文用一个轻量方案演示:基于 asyncio + 内存队列实现任务异步化,并给出工程化的降级与监控思路。
二、核心设计
整体分为三部分:
- 任务模型:定义统一的数据结构,携带任务类型、参数、重试次数。
- 异步队列:生产者投递任务,消费者循环拉取执行。
- 降级策略:队列满时走同步兜底,避免丢任务。
from dataclasses import dataclass, field
from typing import Any, Callable
import asyncio
import logging
logger = logging.getLogger("task_queue")
@dataclass
class Task:
name: str
payload: dict = field(default_factory=dict)
retry: int = 3
三、生产者与消费者
class AsyncTaskQueue:
def __init__(self, maxsize: int = 1000):
self.queue: asyncio.Queue = asyncio.Queue(maxsize=maxsize)
self.handlers: dict[str, Callable] = {}
def register(self, name: str, handler: Callable):
self.handlers[name] = handler
async def push(self, task: Task) -> bool:
try:
self.queue.put_nowait(task)
return True
except asyncio.QueueFull:
logger.warning("队列已满,任务 %s 走同步兜底", task.name)
return False
async def worker(self):
while True:
task: Task = await self.queue.get()
try:
await self._run_with_retry(task)
finally:
self.queue.task_done()
async def _run_with_retry(self, task: Task):
handler = self.handlers.get(task.name)
if handler is None:
logger.error("未注册处理器: %s", task.name)
return
for attempt in range(task.retry):
try:
await handler(task.payload)
return
except Exception as exc:
logger.exception("任务 %s 第 %d 次失败: %s", task.name, attempt + 1, exc)
await asyncio.sleep(2 ** attempt)
四、业务接入示例
async def send_email(payload: dict):
# 模拟异步邮件发送
await asyncio.sleep(0.1)
print(f"发送邮件给 {payload['to']}")
async def main():
queue = AsyncTaskQueue(maxsize=100)
queue.register("send_email", send_email)
consumer = asyncio.create_task(queue.worker())
ok = await queue.push(Task("send_email", {"to": "user@example.com"}))
if not ok:
# 降级:直接同步执行
await send_email({"to": "user@example.com"})
await queue.queue.join()
consumer.cancel()
asyncio.run(main())
五、测试与验证
import asyncio
import pytest
@pytest.mark.asyncio
async def test_push_and_execute():
queue = AsyncTaskQueue()
executed = []
queue.register("echo", lambda p: executed.append(p))
await queue.push(Task("echo", {"v": 1}))
await queue.queue.join() # 等待消费完成
assert executed == [{"v": 1}]
运行测试:pytest -q,结果应显示 1 passed。
六、工程化注意点
- 并发控制:生产环境应限制 worker 数量,避免无界占用协程与连接池。
- 持久化:内存队列重启即丢,重要任务建议写入 Redis Stream 或消息中间件。
- 监控指标:记录队列深度、消费耗时、失败率,接入 Prometheus。
- 超时保护:单任务应设置超时,防止挂起拖垮消费者。
以上方案在小流量场景下可快速落地,兼顾简洁性与可扩展性。
海量精选技术文档和实战案例持续更新,敬请关注【风骏时光少年】