4. 异步任务编排基础:Python asyncio 原理、事件循环、协程、Future 与 Task,Celery/RQ 任务队列简介

在构建高吞吐、低延迟的数据库与缓存联合调试系统时,异步编程与任务编排是核心支撑技术。本章将深入剖析 Python asyncio 的底层原理,并对比 Celery 与 RQ 两种主流任务队列的适用场景。

4.1 Python asyncio 核心概念

asyncio 是 Python 用于编写并发代码的标准库,它通过单线程事件循环实现协作式多任务处理,特别适合 I/O 密集型操作(如数据库查询、缓存读写)。

4.1.1 事件循环(Event Loop)

事件循环是 asyncio 的调度核心,它不断检查是否有待处理的协程、回调或 I/O 事件,并决定下一步执行哪个任务。事件循环运行在单线程中,通过 await 挂起当前协程,将控制权交还给循环。

4.1.2 协程(Coroutine)

协程是通过 async def 定义的函数。调用协程函数不会立即执行,而是返回一个协程对象。协程必须被事件循环调度才能运行,通过 await 关键字挂起并等待另一个协程完成。

4.1.3 Future 与 Task

  • Future:一个低层级对象,代表异步操作的最终结果。通常不直接使用,而是作为 Task 的底层实现。
  • Task:是 Future 的子类,用于将协程包装成一个可被事件循环调度的独立任务。通过 asyncio.create_task() 创建,任务会立即被调度执行。
关键区别:Future 是“结果容器”,Task 是“协程执行器”。在应用开发中,99% 的场景使用 Task 而非直接操作 Future。

4.1.3 执行流程对比

概念 定义方式 执行时机 典型用途
协程 async def func() 需 await 或包装为 Task 定义异步逻辑
Task asyncio.create_task(coro) 立即调度到事件循环 并发执行多个协程
Future loop.create_future() 手动设置结果/异常 底层回调、与旧代码集成
asyncio 事件循环调度流程 事件循环 Task 就绪队列 I/O 等待队列 协程 A (await db.query) 协程 B (await cache.get) 协程 C (I/O 挂起) 协程 D (I/O 挂起) 事件循环从就绪队列取出 Task 执行,遇到 await 则挂起并移入 I/O 等待队列;I/O 完成后 Task 回到就绪队列

4.2 异步任务编排实战模式

在联合调试场景中,常需要编排多个异步任务:先查缓存,缓存未命中则查数据库,再回填缓存。asyncio 提供了多种编排原语。

4.2.1 并发执行:gather 与 wait

import asyncio

async def fetch_from_db(key: str) -> str:
    await asyncio.sleep(0.1)  # 模拟数据库查询
    return f"db_value_{key}"

async def fetch_from_cache(key: str) -> str | None:
    await asyncio.sleep(0.02)  # 模拟缓存查询
    return None  # 模拟缓存未命中

async def main():
    # 并发执行两个查询
    results = await asyncio.gather(
        fetch_from_cache("user:1"),
        fetch_from_db("user:1"),
        return_exceptions=True
    )
    cache_result, db_result = results
    print(f"Cache: {cache_result}, DB: {db_result}")

asyncio.run(main())

4.2.2 超时控制:wait_for

async def query_with_timeout():
    try:
        result = await asyncio.wait_for(
            fetch_from_db("slow_query"),
            timeout=0.5
        )
        return result
    except asyncio.TimeoutError:
        return "fallback_value"

4.2.3 任务取消与清理

async def controlled_task():
    task = asyncio.create_task(fetch_from_db("key"))
    # 如果 0.3 秒未完成则取消
    try:
        result = await asyncio.wait_for(task, timeout=0.3)
    except asyncio.TimeoutError:
        task.cancel()
        try:
            await task  # 等待取消完成
        except asyncio.CancelledError:
            pass
        result = "timeout_fallback"
    return result
注意:取消 Task 后必须 await task 以处理 CancelledError,否则事件循环会抛出“Task was destroyed but it is pending!”警告。

4.3 任务队列:Celery 与 RQ

当异步任务需要跨进程、跨机器执行,或需要持久化、重试、定时调度时,需要引入任务队列。Celery 和 RQ 是 Python 生态中最常用的两个选择。

4.3.1 架构对比

特性 Celery RQ (Redis Queue)
Broker 依赖 Redis / RabbitMQ / Amazon SQS 仅 Redis
任务持久化 支持(依赖 Broker) 支持(Redis 持久化)
定时任务 内置 Celery Beat 需配合 RQ Scheduler
任务重试 内置自动重试机制 需手动实现
任务结果存储 支持多种 Backend(Redis/DB) 仅 Redis
学习曲线 较陡峭 平缓
适用场景 复杂工作流、大规模分布式 中小规模、快速原型

4.3.2 Celery 快速示例

# tasks.py
from celery import Celery

app = Celery('tasks', broker='redis://localhost:6379/0')

@app.task(bind=True, max_retries=3)
def sync_cache_to_db(self, cache_key: str, value: str):
    try:
        # 模拟将缓存数据同步到 PostgreSQL
        print(f"Syncing {cache_key} -> {value}")
    except Exception as exc:
        raise self.retry(exc=exc, countdown=60)

# 调用
sync_cache_to_db.delay("user:1", "new_value")

4.3.3 RQ 快速示例

# worker.py
import redis
from rq import Queue, Connection
from rq.worker import Worker

def sync_cache_to_db(cache_key: str, value: str):
    print(f"RQ Worker syncing {cache_key} -> {value}")

# 启动 Worker
with Connection(redis.Redis()):
    q = Queue()
    Worker(q).work()

# 调用
from rq import Queue
from redis import Redis
q = Queue(connection=Redis())
q.enqueue(sync_cache_to_db, "user:1", "new_value")
Celery / RQ 任务队列架构 生产者 (Web App) Broker (Redis) Worker 进程 任务队列 (List) 结果后端 (Redis/DB) PostgreSQL 生产者将任务序列化后推入 Broker 的队列;Worker 从队列拉取任务执行,并将结果写入结果后端 Celery 支持 RabbitMQ 等多种 Broker;RQ 仅支持 Redis,但配置更简单

4.4 选型建议:asyncio vs 任务队列

决策原则:
  • 同进程内 I/O 并发 → 使用 asyncio(协程 + Task)
  • 跨进程/跨机器、需要持久化 → 使用 Celery 或 RQ
  • 需要定时调度、复杂重试策略 → 优先 Celery
  • 快速原型、Redis 已存在 → 优先 RQ