摘要
一个 Web API 真正进入业务场景后,通常需要连接数据库、缓存和消息队列。MySQL 保存核心业务数据,Redis 提供高速读取、分布式锁和短期状态,消息队列则负责异步任务、事件通知和服务解耦。
如果把这三类基础设施直接写进路由函数,很容易出现连接泄漏、缓存不一致、消息丢失、重复消费和请求阻塞等问题。更合理的方式是围绕连接池、事务边界、缓存策略和消息可靠性建立清晰的服务层与基础设施层。
本文以 FastAPI 为入口,介绍 Python 异步操作 MySQL、Redis 和消息队列的常见方式,重点讨论:
- MySQL 异步连接池和事务;
- Redis 缓存、过期和分布式锁;
- RabbitMQ 消息生产和消费;
- 数据库与消息的一致性;
- 消费者幂等和失败重试;
- 连接超时、资源释放和监控;
- 一个订单服务的完整代码骨架。
示例使用 SQLAlchemy Async、Redis asyncio 客户端和 aio-pika。消息队列部分以 RabbitMQ 为例,Kafka、RocketMQ 等系统的协议和配置不同,但可靠投递、消费幂等和失败处理的工程原则具有共通性。
读完本文后,你应该能够:
- 在 FastAPI 中正确管理 MySQL 异步会话;
- 设计数据库事务和连接池参数;
- 使用 Redis 实现缓存读取和失效;
- 理解缓存穿透、击穿和雪崩;
- 发布和消费 RabbitMQ 消息;
- 处理重复消息、重试和死信;
- 设计订单创建与异步通知流程;
- 建立基础设施层的配置、日志和健康检查。
一、背景与问题
1. Web API 不应该只依赖内存
开发阶段可以使用一个 Python 字典保存数据:
python
orders = {
"ORD-1001": {
"status": "PAID",
"amount": 299.0,
}
}
但服务重启后数据就会丢失,多实例部署后不同进程之间也无法共享。真实业务需要持久化数据库:
text
客户端
-> FastAPI
-> 业务服务
-> MySQL
同时,热点数据如果每次都查询 MySQL,会带来:
- 数据库连接被大量占用;
- 查询延迟升高;
- 数据库 CPU 增加;
- 高峰流量下服务不可用。
因此可以使用 Redis 缓存:
text
请求
-> 先查 Redis
-> 命中则直接返回
-> 未命中再查 MySQL
-> 写入 Redis
-> 返回结果
对于邮件、短信、积分和搜索索引等不需要同步完成的操作,则可以使用消息队列:
text
创建订单
-> 保存订单
-> 发布订单事件
-> 立即返回
消息消费者
-> 发送通知
-> 更新处理状态
2. 三类基础设施分别解决什么问题
| 组件 | 主要职责 | 典型数据 |
|---|---|---|
| MySQL | 持久化和事务 | 用户、订单、库存、支付 |
| Redis | 高速访问和短期状态 | 缓存、验证码、计数器、锁 |
| 消息队列 | 异步解耦和削峰 | 订单事件、通知任务、日志事件 |
它们不是互相替代的关系:
text
MySQL:
事实来源和核心数据
Redis:
快速访问和临时状态
消息队列:
异步传递和任务调度
3. 直接写在路由中的问题
错误示例:
python
@app.post("/orders")
async def create_order(request):
connection = await mysql.connect()
order = await connection.execute(...)
await redis.set(...)
await rabbitmq.publish(...)
return order
这段代码的问题包括:
- 每个请求都创建连接;
- 异常时可能没有关闭连接;
- 数据库事务边界不清楚;
- Redis 写入失败如何处理不明确;
- 消息发送失败是否影响订单提交不明确;
- 路由层同时承担业务和基础设施逻辑;
- 难以测试和替换组件。
推荐的分层:
text
路由层
-> 接收请求、返回响应
服务层
-> 编排订单业务和事务
仓库层
-> 访问 MySQL
缓存层
-> 访问 Redis
消息层
-> 发布和消费事件
基础设施层
-> 管理客户端、连接池和配置
4. 一次订单请求的完整链路
通知消费者 RabbitMQ Redis MySQL 订单服务 FastAPI 客户端 通知消费者 RabbitMQ Redis MySQL 订单服务 FastAPI 客户端 #mermaid-svg-HrQNIP7VFVcOtabY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HrQNIP7VFVcOtabY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HrQNIP7VFVcOtabY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HrQNIP7VFVcOtabY .error-icon{fill:#552222;}#mermaid-svg-HrQNIP7VFVcOtabY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HrQNIP7VFVcOtabY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HrQNIP7VFVcOtabY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HrQNIP7VFVcOtabY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HrQNIP7VFVcOtabY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HrQNIP7VFVcOtabY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HrQNIP7VFVcOtabY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HrQNIP7VFVcOtabY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HrQNIP7VFVcOtabY .marker.cross{stroke:#333333;}#mermaid-svg-HrQNIP7VFVcOtabY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HrQNIP7VFVcOtabY p{margin:0;}#mermaid-svg-HrQNIP7VFVcOtabY .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HrQNIP7VFVcOtabY text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-HrQNIP7VFVcOtabY .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HrQNIP7VFVcOtabY .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-HrQNIP7VFVcOtabY .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-HrQNIP7VFVcOtabY .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-HrQNIP7VFVcOtabY #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-HrQNIP7VFVcOtabY .sequenceNumber{fill:white;}#mermaid-svg-HrQNIP7VFVcOtabY #sequencenumber{fill:#333;}#mermaid-svg-HrQNIP7VFVcOtabY #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-HrQNIP7VFVcOtabY .messageText{fill:#333;stroke:none;}#mermaid-svg-HrQNIP7VFVcOtabY .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HrQNIP7VFVcOtabY .labelText,#mermaid-svg-HrQNIP7VFVcOtabY .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-HrQNIP7VFVcOtabY .loopText,#mermaid-svg-HrQNIP7VFVcOtabY .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-HrQNIP7VFVcOtabY .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HrQNIP7VFVcOtabY .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-HrQNIP7VFVcOtabY .noteText,#mermaid-svg-HrQNIP7VFVcOtabY .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-HrQNIP7VFVcOtabY .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HrQNIP7VFVcOtabY .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HrQNIP7VFVcOtabY .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HrQNIP7VFVcOtabY .actorPopupMenu{position:absolute;}#mermaid-svg-HrQNIP7VFVcOtabY .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-HrQNIP7VFVcOtabY .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HrQNIP7VFVcOtabY .actor-man circle,#mermaid-svg-HrQNIP7VFVcOtabY line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-HrQNIP7VFVcOtabY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建订单校验并执行订单开启本地事务写入订单和订单项提交成功删除订单缓存发布订单创建事件返回订单处理中HTTP 201投递订单事件发送通知确认消费
需要特别注意:数据库提交、Redis 更新和消息发布通常不是一个原子事务。后文会介绍如何使用 Outbox 或本地消息表降低不一致风险。
二、核心概念
1. MySQL 连接池
数据库连接是有限资源。连接池提前创建并复用连接:
text
应用启动
-> 创建连接池
请求到达
-> 获取连接
-> 执行 SQL
-> 提交或回滚
-> 归还连接
应用关闭
-> 关闭连接池
连接池常见参数:
| 参数 | 含义 |
|---|---|
| pool_size | 常驻连接数量 |
| max_overflow | 临时额外连接数量 |
| pool_timeout | 获取连接等待时间 |
| pool_recycle | 连接回收时间 |
| pool_pre_ping | 使用前检查连接是否有效 |
连接池不能无限增大。应用实例数、数据库最大连接数和其他服务连接都需要纳入总量:
text
数据库总连接数
= 应用实例数
× 每个实例的连接池上限
+ 管理任务连接
+ 其他客户端连接
2. 数据库会话和事务
SQLAlchemy AsyncSession 通常表示一次数据库会话。事务可以显式控制:
python
async with session.begin():
session.add(order)
session.add(order_item)
事务成功时提交,异常时回滚。一个事务应该包含业务上必须原子完成的数据库操作:
text
订单主表
+ 订单明细
+ 库存预扣流水
不要把长时间的外部 HTTP 调用放在数据库事务内部,否则会长时间占用连接和锁。
3. Redis 的数据结构
Redis 支持多种数据结构:
| 类型 | 常见用途 |
|---|---|
| String | 缓存、计数器、验证码 |
| Hash | 对象属性、用户状态 |
| List | 简单队列、时间线 |
| Set | 标签、去重、集合关系 |
| Sorted Set | 排行榜、延迟任务 |
| Stream | 消息流和消费组 |
选择结构时要考虑读写方式和过期策略,不要把所有数据都序列化成一个超大的 String。
4. 缓存模式
Cache Aside
最常见的旁路缓存:
text
读取:
先查缓存
-> 未命中查数据库
-> 写入缓存
更新:
先更新数据库
-> 删除缓存
更新时通常选择删除缓存而不是直接更新缓存,因为复杂对象更新容易出现并发覆盖。
Write Through
写请求先写缓存,由缓存同步写数据库。应用逻辑较简单,但需要缓存层支持持久化协作。
Write Behind
先写缓存,稍后异步写数据库。吞吐高,但数据丢失风险和一致性复杂度更高,不适合所有核心业务。
5. 缓存一致性
数据库和缓存是两个独立系统。常见顺序:
text
更新数据库
-> 删除缓存
如果删除缓存失败,可以通过:
- 删除重试;
- 延迟双删;
- 订阅数据库变更;
- 缓存短 TTL;
- 对账任务;
- 更新事件。
不能假设 Redis 永远可用,也不能把缓存当成唯一事实来源。
6. 消息队列的基本组成
消息系统通常包含:
- 生产者;
- 交换机或主题;
- 队列或分区;
- 消费者;
- 消费组;
- 确认机制;
- 重试机制;
- 死信队列。
RabbitMQ 的基本关系可以表示为:
text
Producer
-> Exchange
-> Binding
-> Queue
-> Consumer
消息生产者不一定直接把消息发送给某个消费者,而是通过路由规则将消息投递到队列。
7. 至少一次和至多一次
消息投递常见语义:
至多一次
消息发送一次,不等待确认。可能丢消息,但不会因为重试产生重复。
至少一次
消息处理成功后才确认。消费者崩溃或确认丢失时,消息可能重复投递。
恰好一次
需要端到端协议、事务和幂等设计,现实系统中很难简单保证。多数业务更关注:
text
至少一次投递
+ 消费者幂等
+ 业务状态校验
8. 消费者幂等
同一条消息重复到达时,业务结果应该不变:
text
第一次处理订单事件:发送通知
第二次收到相同事件:发现 event_id 已处理,直接返回
常用实现:
- 消费记录表唯一索引;
- 业务唯一键;
- 状态机;
- Redis 幂等 Key;
- 外部服务幂等请求号。
对于核心业务,最好使用数据库唯一约束,而不是只依赖 Redis,因为数据库更适合作为最终一致性依据。
9. 重试和死信
失败消息可以有限重试:
text
第一次失败:延迟 10 秒
第二次失败:延迟 30 秒
第三次失败:延迟 2 分钟
超过最大次数:进入死信队列
死信消息需要:
- 保存原始消息;
- 保存错误原因;
- 保存重试次数;
- 保存最后处理时间;
- 关联业务 ID;
- 支持人工重放;
- 支持告警。
无限重试会让坏消息长期占用资源,并掩盖真正的业务错误。
10. 消息顺序
有些业务要求同一订单的事件按顺序处理:
text
ORDER_CREATED
-> PAYMENT_PAID
-> ORDER_SHIPPED
-> ORDER_COMPLETED
如果消息被并行消费,可能先处理 PAYMENT_PAID,再处理 ORDER_CREATED。解决方法包括:
- 按业务 ID 分区;
- 同一业务 ID 使用同一队列或分区;
- 消费者检查前置状态;
- 乱序时延迟重试;
- 用状态机拒绝非法迁移。
三、工作原理
1. FastAPI 与基础设施生命周期
基础设施客户端应该在应用生命周期中创建:
#mermaid-svg-Kix0t1F0JlItbfAN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Kix0t1F0JlItbfAN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Kix0t1F0JlItbfAN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Kix0t1F0JlItbfAN .error-icon{fill:#552222;}#mermaid-svg-Kix0t1F0JlItbfAN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Kix0t1F0JlItbfAN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Kix0t1F0JlItbfAN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Kix0t1F0JlItbfAN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Kix0t1F0JlItbfAN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Kix0t1F0JlItbfAN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Kix0t1F0JlItbfAN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Kix0t1F0JlItbfAN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Kix0t1F0JlItbfAN .marker.cross{stroke:#333333;}#mermaid-svg-Kix0t1F0JlItbfAN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Kix0t1F0JlItbfAN p{margin:0;}#mermaid-svg-Kix0t1F0JlItbfAN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Kix0t1F0JlItbfAN .cluster-label text{fill:#333;}#mermaid-svg-Kix0t1F0JlItbfAN .cluster-label span{color:#333;}#mermaid-svg-Kix0t1F0JlItbfAN .cluster-label span p{background-color:transparent;}#mermaid-svg-Kix0t1F0JlItbfAN .label text,#mermaid-svg-Kix0t1F0JlItbfAN span{fill:#333;color:#333;}#mermaid-svg-Kix0t1F0JlItbfAN .node rect,#mermaid-svg-Kix0t1F0JlItbfAN .node circle,#mermaid-svg-Kix0t1F0JlItbfAN .node ellipse,#mermaid-svg-Kix0t1F0JlItbfAN .node polygon,#mermaid-svg-Kix0t1F0JlItbfAN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Kix0t1F0JlItbfAN .rough-node .label text,#mermaid-svg-Kix0t1F0JlItbfAN .node .label text,#mermaid-svg-Kix0t1F0JlItbfAN .image-shape .label,#mermaid-svg-Kix0t1F0JlItbfAN .icon-shape .label{text-anchor:middle;}#mermaid-svg-Kix0t1F0JlItbfAN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Kix0t1F0JlItbfAN .rough-node .label,#mermaid-svg-Kix0t1F0JlItbfAN .node .label,#mermaid-svg-Kix0t1F0JlItbfAN .image-shape .label,#mermaid-svg-Kix0t1F0JlItbfAN .icon-shape .label{text-align:center;}#mermaid-svg-Kix0t1F0JlItbfAN .node.clickable{cursor:pointer;}#mermaid-svg-Kix0t1F0JlItbfAN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Kix0t1F0JlItbfAN .arrowheadPath{fill:#333333;}#mermaid-svg-Kix0t1F0JlItbfAN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Kix0t1F0JlItbfAN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Kix0t1F0JlItbfAN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Kix0t1F0JlItbfAN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Kix0t1F0JlItbfAN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Kix0t1F0JlItbfAN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Kix0t1F0JlItbfAN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Kix0t1F0JlItbfAN .cluster text{fill:#333;}#mermaid-svg-Kix0t1F0JlItbfAN .cluster span{color:#333;}#mermaid-svg-Kix0t1F0JlItbfAN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Kix0t1F0JlItbfAN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Kix0t1F0JlItbfAN rect.text{fill:none;stroke-width:0;}#mermaid-svg-Kix0t1F0JlItbfAN .icon-shape,#mermaid-svg-Kix0t1F0JlItbfAN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Kix0t1F0JlItbfAN .icon-shape p,#mermaid-svg-Kix0t1F0JlItbfAN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Kix0t1F0JlItbfAN .icon-shape .label rect,#mermaid-svg-Kix0t1F0JlItbfAN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Kix0t1F0JlItbfAN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Kix0t1F0JlItbfAN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Kix0t1F0JlItbfAN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用启动
创建 MySQL Engine
创建 Redis 客户端
创建消息连接
创建 Channel 和 Queue
处理 HTTP 请求
应用关闭
关闭消息连接
关闭 Redis
释放数据库连接池
请求中只获取已有资源,不重复创建连接池。
2. MySQL 异步访问流程
text
HTTP 请求
-> FastAPI 依赖获取 AsyncSession
-> 服务层执行校验
-> 仓库层执行 SQL
-> 提交或回滚事务
-> 请求结束关闭 Session
数据库连接池属于应用级资源,Session 通常属于请求级资源。
3. Redis Cache Aside 流程
#mermaid-svg-dPKoh18J3Ep4k9uB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dPKoh18J3Ep4k9uB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dPKoh18J3Ep4k9uB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dPKoh18J3Ep4k9uB .error-icon{fill:#552222;}#mermaid-svg-dPKoh18J3Ep4k9uB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dPKoh18J3Ep4k9uB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dPKoh18J3Ep4k9uB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dPKoh18J3Ep4k9uB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dPKoh18J3Ep4k9uB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dPKoh18J3Ep4k9uB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dPKoh18J3Ep4k9uB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dPKoh18J3Ep4k9uB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dPKoh18J3Ep4k9uB .marker.cross{stroke:#333333;}#mermaid-svg-dPKoh18J3Ep4k9uB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dPKoh18J3Ep4k9uB p{margin:0;}#mermaid-svg-dPKoh18J3Ep4k9uB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dPKoh18J3Ep4k9uB .cluster-label text{fill:#333;}#mermaid-svg-dPKoh18J3Ep4k9uB .cluster-label span{color:#333;}#mermaid-svg-dPKoh18J3Ep4k9uB .cluster-label span p{background-color:transparent;}#mermaid-svg-dPKoh18J3Ep4k9uB .label text,#mermaid-svg-dPKoh18J3Ep4k9uB span{fill:#333;color:#333;}#mermaid-svg-dPKoh18J3Ep4k9uB .node rect,#mermaid-svg-dPKoh18J3Ep4k9uB .node circle,#mermaid-svg-dPKoh18J3Ep4k9uB .node ellipse,#mermaid-svg-dPKoh18J3Ep4k9uB .node polygon,#mermaid-svg-dPKoh18J3Ep4k9uB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dPKoh18J3Ep4k9uB .rough-node .label text,#mermaid-svg-dPKoh18J3Ep4k9uB .node .label text,#mermaid-svg-dPKoh18J3Ep4k9uB .image-shape .label,#mermaid-svg-dPKoh18J3Ep4k9uB .icon-shape .label{text-anchor:middle;}#mermaid-svg-dPKoh18J3Ep4k9uB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dPKoh18J3Ep4k9uB .rough-node .label,#mermaid-svg-dPKoh18J3Ep4k9uB .node .label,#mermaid-svg-dPKoh18J3Ep4k9uB .image-shape .label,#mermaid-svg-dPKoh18J3Ep4k9uB .icon-shape .label{text-align:center;}#mermaid-svg-dPKoh18J3Ep4k9uB .node.clickable{cursor:pointer;}#mermaid-svg-dPKoh18J3Ep4k9uB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dPKoh18J3Ep4k9uB .arrowheadPath{fill:#333333;}#mermaid-svg-dPKoh18J3Ep4k9uB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dPKoh18J3Ep4k9uB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dPKoh18J3Ep4k9uB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dPKoh18J3Ep4k9uB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dPKoh18J3Ep4k9uB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dPKoh18J3Ep4k9uB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dPKoh18J3Ep4k9uB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dPKoh18J3Ep4k9uB .cluster text{fill:#333;}#mermaid-svg-dPKoh18J3Ep4k9uB .cluster span{color:#333;}#mermaid-svg-dPKoh18J3Ep4k9uB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dPKoh18J3Ep4k9uB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dPKoh18J3Ep4k9uB rect.text{fill:none;stroke-width:0;}#mermaid-svg-dPKoh18J3Ep4k9uB .icon-shape,#mermaid-svg-dPKoh18J3Ep4k9uB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dPKoh18J3Ep4k9uB .icon-shape p,#mermaid-svg-dPKoh18J3Ep4k9uB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dPKoh18J3Ep4k9uB .icon-shape .label rect,#mermaid-svg-dPKoh18J3Ep4k9uB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dPKoh18J3Ep4k9uB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dPKoh18J3Ep4k9uB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dPKoh18J3Ep4k9uB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
读取订单
Redis 命中
返回缓存
查询 MySQL
是否存在
缓存空值短期过期
写入 Redis
返回数据库结果
返回不存在
缓存空值可以防止恶意请求不存在的 ID 导致数据库被反复查询,但空值 TTL 应该较短。
4. 消息可靠发布
简单发布流程:
text
业务代码
-> 序列化事件
-> 发布消息
-> 等待 Broker 确认
-> 返回成功
如果业务数据库已经提交,但消息发布失败,就会出现:
text
订单存在
事件不存在
改进方式是使用 Outbox:
text
数据库本地事务
-> 写入订单
-> 写入 outbox_event
-> 一起提交
后台 Relay
-> 扫描 outbox_event
-> 发布消息
-> 标记已发布
5. 消费确认流程
text
消费者收到消息
-> 解析和校验
-> 检查幂等记录
-> 执行业务事务
-> 事务提交成功
-> 确认消息
处理失败
-> 不确认或进入重试队列
-> 超过次数进入死信
确认消息的时机很重要。不能在业务事务提交前确认,否则进程崩溃可能造成消息丢失。
6. 连接超时和取消
每类基础设施都应该设置超时:
text
MySQL 获取连接:1 秒
MySQL 查询:2 秒
Redis 读取:100 毫秒
RabbitMQ 发布确认:1 秒
HTTP 下游:500 毫秒
超时后需要:
- 释放当前资源;
- 判断是否重试;
- 记录 trace_id;
- 返回降级结果或失败;
- 避免后台任务继续无效运行。
7. 配置与密钥
配置可以通过环境变量注入:
text
MYSQL_DSN
REDIS_URL
RABBITMQ_URL
DATABASE_POOL_SIZE
REDIS_TIMEOUT
MESSAGE_RETRY_LIMIT
不要把密码和 Token 写入代码仓库。生产环境可以使用密钥管理系统,并在启动时校验必需配置是否存在。
四、实战示例
1. 项目结构
创建一个订单服务:
text
order-service/
├── app/
│ ├── main.py
│ ├── config.py
│ ├── lifespan.py
│ ├── db.py
│ ├── redis_client.py
│ ├── messaging.py
│ ├── dependencies.py
│ ├── models/
│ │ ├── order.py
│ │ └── outbox.py
│ ├── repositories/
│ │ └── order_repository.py
│ ├── services/
│ │ └── order_service.py
│ ├── consumers/
│ │ └── order_events.py
│ └── routers/
│ └── orders.py
├── migrations/
├── tests/
└── requirements.txt
2. 安装依赖
bash
python -m pip install fastapi uvicorn
python -m pip install sqlalchemy aiomysql
python -m pip install redis
python -m pip install aio-pika
python -m pip install pydantic-settings
保存依赖:
bash
python -m pip freeze > requirements.txt
实际项目应固定主版本范围,并通过锁文件或构建流程保证环境一致。
3. 定义配置
app/config.py:
python
from functools import lru_cache
from pydantic_settings import (
BaseSettings,
SettingsConfigDict,
)
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
extra="ignore",
)
app_name: str = "Order Service"
debug: bool = False
mysql_dsn: str = (
"mysql+aiomysql://app:password@"
"127.0.0.1:3306/order_db"
)
database_pool_size: int = 10
database_max_overflow: int = 5
database_pool_timeout: float = 2.0
database_pool_recycle: int = 1800
redis_url: str = (
"redis://127.0.0.1:6379/0"
)
redis_timeout: float = 0.2
cache_ttl_seconds: int = 300
rabbitmq_url: str = (
"amqp://guest:guest@"
"127.0.0.1:5672/"
)
order_event_exchange: str = "order.events"
order_event_queue: str = "notification.orders"
message_retry_limit: int = 3
@lru_cache
def get_settings() -> Settings:
return Settings()
生产环境不要使用示例默认密码,应通过环境变量提供真实配置。
4. 创建数据库引擎
app/db.py:
python
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from app.config import get_settings
settings = get_settings()
engine = create_async_engine(
settings.mysql_dsn,
pool_size=settings.database_pool_size,
max_overflow=settings.database_max_overflow,
pool_timeout=settings.database_pool_timeout,
pool_recycle=settings.database_pool_recycle,
pool_pre_ping=True,
echo=settings.debug,
)
SessionFactory = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False,
)
async def get_db() -> AsyncGenerator[
AsyncSession,
None,
]:
async with SessionFactory() as session:
yield session
async def close_database():
await engine.dispose()
get_db 是请求级依赖,engine 是应用级资源。
5. 定义数据模型
app/models/order.py:
python
from datetime import datetime
from decimal import Decimal
from sqlalchemy import (
DateTime,
Integer,
Numeric,
String,
)
from sqlalchemy.orm import (
DeclarativeBase,
Mapped,
mapped_column,
)
class Base(DeclarativeBase):
pass
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(
Integer,
primary_key=True,
autoincrement=True,
)
order_no: Mapped[str] = mapped_column(
String(64),
unique=True,
index=True,
)
user_id: Mapped[int] = mapped_column(
Integer,
index=True,
)
status: Mapped[str] = mapped_column(
String(32),
index=True,
)
amount: Mapped[Decimal] = mapped_column(
Numeric(12, 2),
)
created_at: Mapped[datetime] = mapped_column(
DateTime,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
)
金额使用 Decimal 和数据库 Numeric,不要用 float 保存财务金额。
6. 定义 Outbox 表
app/models/outbox.py:
python
from datetime import datetime
from sqlalchemy import (
DateTime,
Integer,
JSON,
String,
Text,
)
from sqlalchemy.orm import (
Mapped,
mapped_column,
)
from app.models.order import Base
class OutboxEvent(Base):
__tablename__ = "outbox_events"
id: Mapped[int] = mapped_column(
Integer,
primary_key=True,
autoincrement=True,
)
event_id: Mapped[str] = mapped_column(
String(64),
unique=True,
index=True,
)
event_type: Mapped[str] = mapped_column(
String(64),
index=True,
)
aggregate_id: Mapped[str] = mapped_column(
String(64),
index=True,
)
payload: Mapped[dict] = mapped_column(JSON)
status: Mapped[str] = mapped_column(
String(16),
index=True,
)
retry_count: Mapped[int] = mapped_column(
Integer,
default=0,
)
last_error: Mapped[str | None] = mapped_column(
Text,
nullable=True,
)
next_retry_at: Mapped[
datetime | None
] = mapped_column(
DateTime,
nullable=True,
)
published_at: Mapped[
datetime | None
] = mapped_column(
DateTime,
nullable=True,
)
created_at: Mapped[datetime] = mapped_column(
DateTime,
)
Outbox 记录和订单记录在同一个数据库事务中写入,后台任务再负责发布消息。
7. 数据库迁移
开发阶段可以创建表:
python
from sqlalchemy.ext.asyncio import (
create_async_engine,
)
async def create_tables():
async with engine.begin() as connection:
await connection.run_sync(
Base.metadata.create_all
)
生产环境建议使用 Alembic 管理迁移:
bash
python -m pip install alembic
alembic init migrations
alembic revision --autogenerate \
-m "create orders and outbox"
alembic upgrade head
不要在每次应用启动时自动执行不可控的建表或迁移操作。数据库结构变更应该经过评审和发布流程。
8. 定义订单仓库
app/repositories/order_repository.py:
python
from sqlalchemy import select
from app.models.order import Order
class OrderRepository:
async def get_by_order_no(
self,
session,
order_no: str,
user_id: int,
) -> Order | None:
result = await session.execute(
select(Order).where(
Order.order_no == order_no,
Order.user_id == user_id,
)
)
return result.scalar_one_or_none()
async def insert(
self,
session,
order: Order,
) -> Order:
session.add(order)
await session.flush()
return order
用户 ID 由认证上下文传入,不能由客户端通过请求体自由指定。
9. 定义请求和响应模型
app/routers/orders.py 中可以使用 Pydantic 模型:
python
from decimal import Decimal
from pydantic import (
BaseModel,
Field,
)
class OrderCreate(BaseModel):
product_id: int = Field(
ge=1,
)
quantity: int = Field(
ge=1,
le=100,
)
class OrderResponse(BaseModel):
order_no: str
status: str
amount: Decimal
请求模型只接收客户端允许提交的字段,响应模型只暴露客户端需要的数据。
10. Redis 客户端生命周期
app/redis_client.py:
python
from redis.asyncio import (
Redis,
from_url,
)
from app.config import get_settings
def create_redis() -> Redis:
settings = get_settings()
return from_url(
settings.redis_url,
socket_connect_timeout=0.2,
socket_timeout=settings.redis_timeout,
decode_responses=True,
)
async def close_redis(
redis: Redis,
):
await redis.aclose()
在应用启动时创建一次 Redis 客户端:
python
app.state.redis = create_redis()
请求中通过依赖读取:
python
from fastapi import Request
def get_redis(request: Request) -> Redis:
return request.app.state.redis
11. 实现订单缓存
缓存 Key 应包含业务和租户范围:
python
def order_cache_key(
user_id: int,
order_no: str,
) -> str:
return (
f"order:{user_id}:{order_no}"
)
查询逻辑:
python
import json
async def get_cached_order(
redis,
user_id: int,
order_no: str,
):
key = order_cache_key(
user_id,
order_no,
)
cached = await redis.get(key)
if cached is None:
return None
return json.loads(cached)
写入缓存:
python
async def cache_order(
redis,
user_id: int,
order_no: str,
data: dict,
ttl: int,
):
key = order_cache_key(
user_id,
order_no,
)
await redis.set(
key,
json.dumps(
data,
ensure_ascii=False,
),
ex=ttl,
)
序列化失败或 Redis 故障时,不应该影响核心数据库查询。可以记录异常并继续走数据库路径。
12. 实现 Redis 缓存服务
python
class OrderCache:
def __init__(
self,
redis,
ttl: int,
):
self.redis = redis
self.ttl = ttl
async def get(
self,
user_id: int,
order_no: str,
):
try:
key = order_cache_key(
user_id,
order_no,
)
value = await self.redis.get(key)
if value is None:
return None
return json.loads(value)
except Exception:
return None
async def set(
self,
user_id: int,
order_no: str,
data: dict,
):
try:
key = order_cache_key(
user_id,
order_no,
)
await self.redis.set(
key,
json.dumps(
data,
ensure_ascii=False,
),
ex=self.ttl,
)
except Exception:
pass
async def delete(
self,
user_id: int,
order_no: str,
):
try:
await self.redis.delete(
order_cache_key(
user_id,
order_no,
)
)
except Exception:
pass
对于可选缓存,Redis 故障可以降级;对于分布式锁、幂等和权限相关数据,则不能无条件忽略 Redis 故障。
13. 防止缓存击穿
同一个热点订单缓存失效时,大量请求可能同时查询数据库。可以使用 Redis 分布式锁:
python
import uuid
async def load_order_with_lock(
redis,
loader,
user_id: int,
order_no: str,
):
key = order_cache_key(
user_id,
order_no,
)
lock_key = key + ":lock"
token = str(uuid.uuid4())
acquired = await redis.set(
lock_key,
token,
nx=True,
ex=5,
)
if acquired:
try:
value = await loader()
if value is not None:
await redis.set(
key,
json.dumps(value),
ex=300,
)
return value
finally:
current = await redis.get(lock_key)
if current == token:
await redis.delete(lock_key)
await asyncio.sleep(0.05)
cached = await redis.get(key)
if cached:
return json.loads(cached)
return await loader()
释放锁最好使用 Lua 脚本保证"比较值和删除"是一个原子操作。示例为了突出流程进行了简化,生产环境还需要设置等待时间和失败降级。
14. RabbitMQ 消息连接
app/messaging.py:
python
import aio_pika
from aio_pika import (
DeliveryMode,
ExchangeType,
Message,
)
class MessageBus:
def __init__(self, url: str):
self.url = url
self.connection = None
self.channel = None
self.exchange = None
async def connect(
self,
exchange_name: str,
):
self.connection = (
await aio_pika.connect_robust(
self.url
)
)
self.channel = await (
self.connection.channel()
)
await self.channel.set_qos(
prefetch_count=20
)
self.exchange = await (
self.channel.declare_exchange(
exchange_name,
ExchangeType.TOPIC,
durable=True,
)
)
async def close(self):
if self.connection is not None:
await self.connection.close()
connect_robust 可以在部分网络故障后尝试恢复连接,但业务仍然需要处理发布确认、重复发布和恢复期间的状态。
15. 发布订单事件
python
import json
import uuid
from datetime import datetime, timezone
async def publish_order_created(
message_bus: MessageBus,
order_no: str,
user_id: int,
):
payload = {
"event_id": str(uuid.uuid4()),
"event_type": "ORDER_CREATED",
"occurred_at": datetime.now(
timezone.utc
).isoformat(),
"data": {
"order_no": order_no,
"user_id": user_id,
},
}
message = Message(
body=json.dumps(
payload,
ensure_ascii=False,
).encode("utf-8"),
delivery_mode=DeliveryMode.PERSISTENT,
content_type="application/json",
message_id=payload["event_id"],
type="ORDER_CREATED",
)
await message_bus.exchange.publish(
message,
routing_key="order.created",
)
RabbitMQ 的持久化消息需要同时满足:
- Exchange 持久化;
- Queue 持久化;
- Message 使用持久化投递模式;
- Broker 磁盘和集群可靠性配置;
- 生产者确认发布成功。
仅仅设置一个 persistent 字段不能保证所有故障场景下消息都不丢。
16. 使用 Outbox 发布事件
订单服务:
python
from datetime import datetime, timezone
async def create_order(
session,
user_id: int,
command: OrderCreate,
):
order_no = generate_order_no()
order = Order(
order_no=order_no,
user_id=user_id,
status="PENDING",
amount=calculate_amount(
command.product_id,
command.quantity,
),
created_at=datetime.now(
timezone.utc
),
updated_at=datetime.now(
timezone.utc
),
)
event_id = str(uuid.uuid4())
event = OutboxEvent(
event_id=event_id,
event_type="ORDER_CREATED",
aggregate_id=order_no,
payload={
"event_id": event_id,
"event_type": "ORDER_CREATED",
"order_no": order_no,
"user_id": user_id,
},
status="PENDING",
created_at=datetime.now(
timezone.utc
),
)
session.add(order)
session.add(event)
await session.commit()
return order
订单和 Outbox 记录一起提交。后台 Relay:
python
async def publish_outbox_batch(
session,
message_bus,
batch_size: int = 100,
):
result = await session.execute(
select(OutboxEvent)
.where(
OutboxEvent.status == "PENDING"
)
.order_by(OutboxEvent.id)
.limit(batch_size)
.with_for_update(
skip_locked=True
)
)
events = result.scalars().all()
for event in events:
try:
await message_bus.publish(
event.event_type,
event.payload,
)
event.status = "PUBLISHED"
event.published_at = datetime.now(
timezone.utc
)
except Exception as exc:
event.retry_count += 1
event.last_error = str(exc)
await session.commit()
真实 Relay 需要防止多个实例重复抢占、处理发送成功但状态更新失败,以及支持重试和死信。
17. 消费 RabbitMQ 消息
python
async def consume_order_events(
message_bus: MessageBus,
handler,
):
queue = await (
message_bus.channel.declare_queue(
"notification.orders",
durable=True,
)
)
await queue.bind(
message_bus.exchange,
routing_key="order.created",
)
async with queue.iterator() as iterator:
async for message in iterator:
async with message.process(
requeue=False
):
payload = json.loads(
message.body.decode("utf-8")
)
await handler(payload)
message.process 在代码块成功结束时确认消息,出现异常时根据配置重新入队或进入失败处理。生产代码需要明确 requeue 策略,不能让坏消息无限循环。
18. 消费幂等表
sql
CREATE TABLE consumed_events (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_id VARCHAR(64) NOT NULL,
consumer_name VARCHAR(128) NOT NULL,
processed_at DATETIME NOT NULL,
UNIQUE KEY uk_event_consumer (
event_id,
consumer_name
)
);
消费者处理:
python
async def handle_order_created(
session,
payload: dict,
):
event_id = payload["event_id"]
inserted = await insert_consumed_event(
session,
event_id=event_id,
consumer_name="notification-service",
)
if not inserted:
return
await notification_service.send(
user_id=payload["user_id"],
message=(
f"订单 {payload['order_no']} "
"创建成功"
),
)
await session.commit()
如果通知服务调用成功但数据库提交失败,消息可能再次投递,通知也可能重复发送。外部通知服务最好支持幂等请求号;如果无法保证,只能通过发送记录和业务策略降低重复风险。
19. 完整订单服务
python
class OrderService:
def __init__(
self,
repository,
cache,
session,
):
self.repository = repository
self.cache = cache
self.session = session
async def get_order(
self,
user_id: int,
order_no: str,
):
cached = await self.cache.get(
user_id,
order_no,
)
if cached is not None:
return cached
order = await (
self.repository.get_by_order_no(
self.session,
order_no,
user_id,
)
)
if order is None:
return None
result = {
"order_no": order.order_no,
"status": order.status,
"amount": str(order.amount),
}
await self.cache.set(
user_id,
order_no,
result,
)
return result
创建订单时:
python
async def create_order(
self,
user_id: int,
command: OrderCreate,
):
async with self.session.begin():
order = await self.repository.create(
self.session,
user_id,
command,
)
await self.repository.create_outbox(
self.session,
order,
)
await self.cache.delete(
user_id,
order.order_no,
)
return order
缓存删除和 Outbox 发布仍然是独立动作,所以需要允许失败并通过后台任务或 TTL 最终修复。
20. 定义 FastAPI 路由
python
from typing import Annotated
from fastapi import (
APIRouter,
Depends,
HTTPException,
)
router = APIRouter(
prefix="/orders",
tags=["订单"],
)
@router.get(
"/{order_no}",
response_model=OrderResponse,
)
async def get_order(
order_no: str,
user=Depends(get_current_user),
session=Depends(get_db),
redis=Depends(get_redis),
):
cache = OrderCache(
redis=redis,
ttl=300,
)
repository = OrderRepository()
service = OrderService(
repository=repository,
cache=cache,
session=session,
)
result = await service.get_order(
user_id=user["id"],
order_no=order_no,
)
if result is None:
raise HTTPException(
status_code=404,
detail="订单不存在",
)
return result
实际项目中可以把 OrderService 组合过程抽成依赖,避免每个路由重复组装。
五、常见问题与实践建议
1. 数据库连接池应该设置多大
没有固定答案。可以先根据数据库容量和服务并发估算:
text
实例连接上限
<= 数据库允许连接数
/ 应用实例数
- 其他服务预留
还要结合:
- 单次查询耗时;
- 请求 QPS;
- 事务持续时间;
- 连接等待时间;
- 数据库 CPU;
- 慢查询;
- 读写比例。
连接池过小会导致请求等待,过大则可能把数据库压垮。
2. 为什么异步代码仍然会阻塞
使用 async def 不代表内部所有操作都是异步。如果在协程中调用同步客户端:
python
async def bad():
response = requests.get(
"https://example.com"
)
return response.json()
requests 会阻塞事件循环。常见阻塞来源:
- requests;
- 同步 MySQL 驱动;
- 同步 Redis 客户端;
- 大量 CPU 计算;
- 大文件同步读写;
- 阻塞式压缩和解析。
应使用异步客户端,或将同步任务放入线程池或后台任务。
3. Redis 故障时接口应该失败吗
取决于 Redis 承担的职责。
可选缓存:
text
Redis 失败
-> 读取数据库
-> 返回结果
关键状态:
text
Redis 失败
-> 无法确认锁或幂等状态
-> 拒绝高风险操作
验证码、限流、分布式锁和幂等状态不能简单地全部降级为放行,否则可能带来安全或数据问题。
4. 缓存应该在什么时候删除
更新数据库后删除缓存是常见做法:
text
UPDATE MySQL
-> DELETE Redis
如果先删缓存再更新数据库,其他请求可能读到旧数据库值并重新写回缓存。
并发条件复杂时还需要:
- 延迟双删;
- 版本号;
- 更新事件;
- 分布式锁;
- 短 TTL;
- 对账和重建。
5. 缓存穿透、击穿和雪崩是什么
缓存穿透
请求大量不存在的数据,缓存和数据库都没有:
text
请求不存在 ID
-> 缓存未命中
-> 数据库未找到
-> 每次都访问数据库
解决:
- 缓存空值;
- 布隆过滤器;
- 参数格式校验;
- 访问频率限制。
缓存击穿
某个热点 Key 失效,大量请求同时查询数据库:
text
热点 Key 同时失效
-> 大量请求回源
解决:
- 互斥锁;
- 逻辑过期;
- 提前刷新;
- 热点数据预热。
缓存雪崩
大量 Key 在同一时间过期,或 Redis 整体不可用:
text
大量缓存同时失效
-> 数据库瞬间承压
解决:
- TTL 增加随机抖动;
- 分批过期;
- 多级缓存;
- 降级;
- 限流;
- Redis 高可用。
6. 消息生产成功但业务返回失败怎么办
如果消息用于非核心通知,可以返回业务成功并异步处理;如果消息是业务流程的关键事件,应使用 Outbox 或事务消息。
不要采用:
text
先提交数据库
-> 直接发送消息
-> 消息失败后把数据库回滚
数据库事务通常无法回滚已经发出的消息。应让消息具备可重试状态,并通过对账保证最终发布。
7. RabbitMQ 消费者为什么会重复收到消息
常见原因:
- 消费者处理成功但确认消息前进程崩溃;
- 网络断开导致 Broker 未收到确认;
- 消费者异常后消息重新入队;
- 多个消费者竞争恢复;
- 业务处理超时。
重复消费是至少一次投递下的正常现象。正确做法是设计幂等,而不是试图完全消除重复投递。
8. 消费者应该先确认消息还是后确认消息
通常应在业务处理成功、事务提交后确认:
text
收到消息
-> 执行业务
-> 提交本地事务
-> 确认消息
先确认再处理可能导致:
text
确认成功
-> 进程崩溃
-> 业务没有执行
-> 消息不会再次投递
9. 消息失败后应该 requeue 吗
临时错误可以重试,永久业务错误不应该无限 requeue:
text
网络超时:
-> 延迟重试
参数错误:
-> 进入失败队列
权限错误:
-> 记录并人工处理
重复消息:
-> 幂等返回成功
可以使用重试队列和死信交换机实现延迟重试。
10. 如何避免消息顺序错乱
可以:
- 使用订单号作为路由或分区 Key;
- 保证同一订单进入同一个顺序队列;
- 消费前检查当前业务状态;
- 乱序消息延迟处理;
- 使用版本号拒绝旧事件;
- 通过状态机限制非法迁移。
顺序保证通常会牺牲部分并行度,必须根据业务需求选择。
11. 如何处理 MySQL 事务中的外部调用
不建议在事务中调用:
- 外部 HTTP;
- Redis 慢操作;
- 消息发送等待;
- 文件上传;
- 邮件和短信。
原因是外部服务慢会让数据库连接和锁长时间占用。更好的方式:
text
数据库事务
-> 写入业务数据和事件
-> 提交
异步消费者
-> 调用外部服务
12. 如何处理数据库死锁
死锁通常是临时错误,可以有限重试:
python
async def run_with_retry(
operation,
attempts: int = 2,
):
for attempt in range(attempts):
try:
return await operation()
except DeadlockError:
if attempt == attempts - 1:
raise
await asyncio.sleep(
0.05 * (attempt + 1)
)
但不能用无限重试掩盖 SQL 顺序、索引和事务设计问题。
13. 如何设置消息预取数量
RabbitMQ 的 prefetch_count 控制消费者未确认消息数量:
python
await channel.set_qos(
prefetch_count=20
)
过大可能导致:
- 单个消费者积压大量消息;
- 内存增加;
- 其他消费者不公平;
- 关闭时恢复时间变长。
过小则可能降低吞吐。需要结合单条消息耗时、消费者数量和内存压测。
14. 如何处理大消息
不要把大文件和完整数据集直接放入消息体。可以:
text
文件上传到对象存储
-> 消息只传 file_id 和 metadata
-> 消费者按权限读取文件
消息应该尽量包含:
- 事件 ID;
- 业务 ID;
- 事件类型;
- 版本;
- 必要参数;
- 数据来源;
- 时间戳。
15. 如何测试基础设施故障
至少测试:
- MySQL 连接超时;
- MySQL 事务回滚;
- MySQL 死锁重试;
- Redis 连接失败;
- Redis 缓存过期;
- Redis 锁超时;
- RabbitMQ 不可用;
- 消息重复投递;
- 消费者处理异常;
- 消息进入死信;
- Outbox 发布中断;
- 应用重启后继续发布。
六、进阶思考
1. 基础设施不是简单依赖,而是故障边界
一个服务依赖 MySQL、Redis 和消息队列,相当于引入了三个不同故障域:
text
MySQL 故障:
核心数据不可读写
Redis 故障:
缓存失效或临时状态不可用
消息队列故障:
异步任务延迟或事件积压
应该对每个故障域分别设计:
- 超时;
- 降级;
- 重试;
- 告警;
- 恢复;
- 数据补偿。
不能所有异常都统一返回 500。
2. 连接池与并发模型
FastAPI 使用多个 worker 时,每个 worker 通常都有自己的数据库连接池和 Redis 客户端:
text
4 个 Uvicorn worker
× 每个 MySQL pool_size=10
= 最多约 40 个数据库连接
如果配置时只看单个进程,实际连接数可能超出数据库上限。消息消费者也可能单独占用连接,因此需要计算全局资源预算。
3. Outbox 的发布状态机
Outbox 不应只有一个布尔字段。可以使用:
text
PENDING:等待发布
PUBLISHING:已抢占,正在发布
PUBLISHED:发布成功
RETRYING:等待重试
DEAD:超过重试次数
CANCELED:业务取消
发布器需要处理:
- 分布式抢占;
- 长时间 PUBLISHING 恢复;
- 发布成功但状态更新失败;
- 重复发布;
- 死信和人工重放;
- 事件版本兼容。
4. 事件结构和版本
事件应该包含稳定的信封:
json
{
"event_id": "evt-001",
"event_type": "ORDER_CREATED",
"event_version": 1,
"aggregate_type": "order",
"aggregate_id": "ORD-1001",
"occurred_at": "2026-09-13T10:00:00+08:00",
"producer": "order-service",
"payload": {
"user_id": 1,
"amount": "299.00"
}
}
事件版本可以帮助消费者兼容字段变化。不要直接把数据库 ORM 对象序列化成消息,因为数据库结构和事件契约的生命周期不同。
5. 数据库与消息的一致性策略
可以按业务重要性选择:
| 场景 | 方案 |
|---|---|
| 非关键日志 | 直接异步发送,允许少量丢失 |
| 订单事件 | Outbox 或事务消息 |
| 通知任务 | Outbox + 消费幂等 |
| 资金流水 | 本地事务、事件和对账 |
| 搜索索引更新 | 最终一致和重建 |
| 跨服务状态变更 | 状态机、补偿和对账 |
越接近核心业务,越需要显式记录状态和补偿路径。
6. Redis 分布式锁的边界
Redis 锁适合:
- 防止热点缓存同时重建;
- 控制短时间任务并发;
- 防止重复执行非核心任务。
不适合直接替代:
- 数据库事务;
- 资金扣款一致性;
- 长时间业务锁;
- 跨系统强一致协议。
锁必须设置过期时间、唯一 Token 和安全释放。对于核心资源,最终状态仍然要由数据库约束和业务状态机保证。
7. 任务与消息的可观测性
每个消息事件建议携带:
text
event_id
trace_id
task_id
aggregate_id
producer
occurred_at
schema_version
日志中记录:
text
消息发布耗时
发布确认结果
队列积压
消费开始和结束
消费重试次数
处理结果
死信数量
这样可以追踪:
text
HTTP 请求
-> 数据库事务
-> Outbox 记录
-> 消息发布
-> 消费处理
-> 外部通知
8. 健康检查不能只检查进程存活
健康检查可以分为:
Liveness
进程是否还能运行。
Readiness
服务是否具备接收流量的条件,包括:
- 数据库连接;
- Redis 连接;
- 消息连接;
- 必要配置;
- 关键依赖状态。
如果 Redis 只是可选缓存,Redis 故障不一定应该让整个服务变为不可用;如果消息队列是核心写入链路,则应该在无法发布关键事件时停止接收相关请求。
9. 数据库读写分离
读多写少的服务可以考虑:
text
写请求 -> MySQL 主库
读请求 -> MySQL 只读副本
但要注意复制延迟:
text
刚刚创建订单
-> 主库已经成功
-> 从库还没有同步
-> 查询不到订单
需要根据业务选择:
- 创建后短时间读主库;
- 使用 sticky session;
- 使用版本或时间戳;
- 对关键查询强制主库;
- 接受最终一致。
10. Redis Cluster 和 Key 设计
分布式 Redis 需要关注 Key 设计:
- Key 不能无限增长;
- Key 应包含业务边界;
- 需要设置过期时间;
- 避免热点 Key;
- 使用 Hash Tag 时确认分片影响;
- 批量操作要考虑跨槽问题。
示例:
text
tenant:{tenant-a}:order:ORD-1001
user:{user-001}:session
rate:{user-001}:20260913
Key 结构应稳定、可观察和可批量清理。
11. 消息队列与任务队列的区别
消息队列传递业务事件:
text
ORDER_CREATED
PAYMENT_SUCCEEDED
任务队列传递待执行动作:
text
发送邮件
生成报表
刷新索引
两者可以使用同一套中间件,但语义不同:
- 事件描述已经发生的事实;
- 任务描述需要执行的动作;
- 事件通常允许多个消费者;
- 任务通常由一个消费者组处理;
- 事件强调契约和版本;
- 任务强调重试和执行状态。
12. 使用容器启动本地环境
开发环境可以使用 Docker Compose:
yaml
services:
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: order_db
MYSQL_USER: app
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: root
ports:
- "3306:3306"
redis:
image: redis:7
ports:
- "6379:6379"
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
本地环境只用于开发和测试,生产环境需要使用持久卷、账号隔离、网络限制、监控和备份。
13. 事务边界的设计原则
一个服务方法中的事务应尽量短:
text
校验请求
-> 开启数据库事务
-> 写入核心数据
-> 写入 Outbox
-> 提交
-> 事务结束
-> 异步处理外部副作用
不要将以下操作放进数据库事务:
- 等待用户;
- 调用慢外部 API;
- 等待消息消费;
- 大文件处理;
- 长时间计算。
14. 基础设施测试替身
测试服务层时,可以替换:
text
MySQL:
使用测试数据库或事务回滚
Redis:
使用临时实例或 FakeRedis
RabbitMQ:
使用测试队列或消息记录器
外部通知:
使用 Mock
但关键路径仍需进行真实基础设施集成测试,不能只依赖 Mock,因为连接池、序列化、事务和消息确认问题很难完全通过 Mock 发现。
结论
Python 后端连接 MySQL、Redis 和消息队列时,最重要的不是记住几个客户端 API,而是理解它们各自的职责和故障边界。
MySQL 是核心数据和事务的事实来源,应重点管理异步连接池、Session 生命周期、事务长度和数据库迁移。Redis 适合缓存、计数和短期状态,应重点处理 TTL、缓存一致性、穿透、击穿、雪崩以及故障降级。消息队列适合异步解耦和事件传播,应重点处理持久化、生产确认、消费确认、幂等、重试、死信和事件版本。
一个较稳妥的订单服务链路是:
text
FastAPI 接收请求
-> 认证和参数校验
-> MySQL 本地事务
写入订单
写入 Outbox 事件
-> 提交事务
-> 删除或失效 Redis 缓存
-> Relay 发布消息
-> 消费者执行通知等异步任务
-> 记录消费结果和失败状态
落地时需要持续关注:
- 连接池总量;
- 每层超时;
- 事务边界;
- 缓存失效;
- 消息重复;
- Outbox 状态;
- 死信和补偿;
- 监控和告警;
- 多租户数据隔离;
- 敏感信息保护。
当数据库、缓存和消息系统被组织成独立的基础设施层,并且每条链路都有明确的重试、降级和恢复策略后,FastAPI 服务才具备支撑真实业务的基础。
下一篇可以继续学习《同步与异步:理解 Python asyncio 的运行机制》,深入理解事件循环、协程、任务、Future、阻塞 IO 和异步并发控制。