一、消息队列与FastAPI集成概述
在分布式系统中,消息队列(如RabbitMQ、Kafka)常用于解耦服务。FastAPI通过异步特性(async/await
)完美支持消息队列的集成。
二、消息事务保障
2.1 问题场景
当业务逻辑需同时操作数据库和发送消息时(例如订单支付成功后更新库存并通知物流),若两者之一失败,会导致数据不一致:
- 数据库更新成功但消息发送失败 → 其他服务未感知状态变更
- 消息发送成功但数据库更新失败 → 其他服务收到无效消息
2.2 解决方案:事务型消息
核心思想:将消息发送纳入数据库事务 。以下使用SQLAlchemy
+pika
(RabbitMQ客户端)实现:
python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import pika
# 初始化数据库和MQ连接
DATABASE_URL = "postgresql://user:pass@localhost/db"
RABBITMQ_URL = "amqp://guest:guest@localhost/"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
mq_connection = pika.BlockingConnection(pika.URLParameters(RABBITMQ_URL))
channel = mq_connection.channel()
# 定义事务函数
def execute_transaction(order_id: int):
db = SessionLocal()
try:
# Step 1: 数据库操作
db.execute("UPDATE orders SET status='paid' WHERE id=:id", {"id": order_id})
# Step 2: 发送消息(临时存储到DB事务中)
channel.basic_publish(
exchange="orders",
routing_key="payment.success",
body=f"Order {order_id} paid"
)
# Step 3: 提交事务(包括消息发送)
db.commit()
except Exception as e:
db.rollback() # 回滚数据库和消息
raise e
finally:
db.close()
2.3 流程图
graph TD
A[开始事务] --> B[更新数据库]
B --> C[发送消息到MQ]
C --> D{成功?}
D -->|是| E[提交事务]
D -->|否| F[回滚事务]
E --> G[结束]
F --> G
三、幂等性保障
3.1 问题场景
消息队列可能因网络抖动导致消息重复投递(例如同一支付回调被发送两次),若服务未处理重复消息,会导致重复扣款、库存超减等严重错误。
3.2 解决方案:幂等性设计
核心思路:为每条消息生成唯一ID,并在处理前校验是否已执行。通过Redis实现幂等令牌:
python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import redis
app = FastAPI()
redis_client = redis.Redis(host='localhost', port=6379, db=0)
class PaymentCallback(BaseModel):
message_id: str # 消息唯一ID
order_id: int
amount: float
@app.post("/payment/callback")
async def handle_payment(callback: PaymentCallback):
# 检查消息是否已处理
if redis_client.get(callback.message_id):
return {"status": "ignored", "reason": "Duplicate message"}
# 业务逻辑(如更新订单状态)
# ...
# 标记消息已处理(有效期24小时)
redis_client.setex(callback.message_id, 86400, "processed")
return {"status": "success"}
3.3 关键设计原则
- 全局唯一ID:使用UUID或雪花算法生成ID,确保跨服务唯一性
- 原子操作 :Redis的
SETNX
(Set If Not Exists)是原子操作,避免并发问题 - 过期机制:防止存储空间无限增长
四、课后Quiz
-
问题 :为什么消息队列场景中,仅靠数据库事务不能解决数据一致性问题?
答案:消息发送是跨网络操作,可能成功但数据库事务失败(或反之),需将消息暂存至事务边界内。 -
问题 :如何防止Redis宕机导致幂等性失效?
答案:采用多级降级方案:- 优先使用Redis
- 若Redis不可用,改用数据库的唯一约束
- 最坏情况下记录日志人工介入
五、常见报错与解决方案
5.1 报错:422 Validation Error
场景 :FastAPI自动校验请求数据时失败
原因:
- 请求体不符合Pydantic模型定义(如缺失字段、类型错误)
- 路径参数/查询参数格式错误
解决方案:
- 检查Swagger文档中的请求体示例
- 使用try-except捕获详细错误:
python
from fastapi import Request
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={"detail": exc.errors(), "body": exc.body},
)
- 确保模型定义完整(使用
Field
定义必填项):
python
class PaymentCallback(BaseModel):
message_id: str = Field(..., min_length=32) # 必须包含且长度≥32
5.2 报错:503 Service Unavailable
场景 :消息队列服务连接失败
解决方案:
- 添加重试机制(指数退避算法):
python
import tenacity
@tenacity.retry(
wait=tenacity.wait_exponential(min=1, max=30),
stop=tenacity.stop_after_attempt(5)
)
def connect_to_rabbitmq():
return pika.BlockingConnection(pika.URLParameters(RABBITMQ_URL))
- 使用连接池(如
pika.ConnectionPool
) - 部署MQ集群实现高可用
六、示例依赖
第三方库及其版本(通过PyPI官方验证):
text
fastapi==0.109.0
uvicorn==0.27.0
pydantic==2.6.0
sqlalchemy==2.0.28
redis==5.0.1
pika==1.3.2
tenacity==8.2.3
运行命令:
bash
uvicorn main:app --reload --port 8000