# 别让你的API成为性能瓶颈:一个来自生产环境的血泪优化史
> "你以为只是加了几个索引,但可能亲手给数据库敲响了丧钟。"
## 开篇:一个让你怀疑人生的性能问题
事情是这样的。
那天凌晨,我正在床上挺尸,突然被一阵急促的钉钉电话吵醒。生产环境报警了,核心接口响应时间从正常的200ms飙升到了15秒。
15秒,什么概念?
用户点击一个按钮,等了15秒,然后华丽丽地超时了。
我连滚带爬地打开电脑,一通排查。最后发现元凶竟然是一个看似无害的API——一个简单的"获取用户列表"接口。
别问,问就是我的真实经历。
## 问题一:N+1查询——那个藏在温柔乡里的杀手
让我还原一下案发现场。
当时的代码大概是这样的:
```python
# 传说中的 N+1 查询
def get_users():
users = db.query("SELECT * FROM users")
for user in users:
# 每个用户都单独查一次详情
user["profile"] = db.query(
f"SELECT * FROM user_profiles WHERE user_id = {user["id"]}"
)
user["orders"] = db.query(
f"SELECT * FROM orders WHERE user_id = {user["id"]}"
)
return users
```
这段代码有什么问题?
假设返回100个用户,那么它会:
- 1次查询用户列表
- 100次查询用户详情
- 100次查询订单
总共201次数据库查询。
我就问你,数据库难不难受?
### 正确的打开方式
```python
# 预加载 JOIN,一次搞定
def get_users_optimized():
users = db.query("""
SELECT u.*, p.*, GROUP_CONCAT(o.id) as order_ids
FROM users u
LEFT JOIN user_profiles p ON u.id = p.user_id
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id
""")
return users
```
一条SQL,数据库笑开花。
或者用 ORM 的预加载:
```python
# SQLAlchemy 示例
users = session.query(User).options(
joinedload(User.profile),
joinedload(User.orders)
).all()
```
## 问题二:没有分页——你是在给前端喂满汉全席
有时候吧,程序员偷懒起来自己都怕。
"分页多麻烦啊,直接返回全部不香吗?"
香,太香了。
香到数据库直接挂掉。
你想啊,用户表10万条记录,你一个select * 全部查出来,内存直接爆炸。网络传输10万条JSON,客户端直接卡死。
这不是接口,这是灾难。
### 分页不是选项,是义务
```python
# 正确的分页
def get_users(page=1, page_size=20):
offset = (page - 1) * page_size
users = db.query(
"SELECT * FROM users LIMIT %s OFFSET %s",
page_size, offset
)
total = db.query("SELECT COUNT(*) FROM users")
return {
"data": users,
"total": total,
"page": page,
"page_size": page_size
}
```
如果数据量特别大(比如上千万),考虑用游标分页(cursor-based pagination):
```python
# 游标分页示例
def get_users_cursor(last_id=0, limit=20):
users = db.query(
"SELECT * FROM users WHERE id > %s ORDER BY id LIMIT %s",
last_id, limit
)
return users
```
为什么选游标不分页?
- OFFSET 大的时候性能爆炸
- 游标分页性能恒定
- 适合无限滚动场景
## 问题三:索引建了个寂寞
很多人觉得索引嘛,谁不会建?搞个B+Tree,yes!
但有时候索引建了等于没建。
### 索引失效的经典场景
```sql
-- 场景1:函数包裹
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- 场景2:类型转换
SELECT * FROM users WHERE user_id = "123"; -- user_id是int类型
-- 场景3:左模糊查询
SELECT * FROM users WHERE name LIKE "%小龙虾";
-- 场景4:or条件
SELECT * FROM users WHERE name = "张三" OR email = "zhangsan@example.com";
```
以上查询,索引表示:我尽力了。
### 正确的姿势
```sql
-- 范围查询
SELECT * FROM users WHERE created_at BETWEEN "2024-01-01" AND "2024-12-31";
-- 类型匹配
SELECT * FROM users WHERE user_id = 123;
-- 右模糊(可以用索引)
SELECT * FROM users WHERE name LIKE "小龙虾%";
-- 拆成union或者用union all
SELECT * FROM users WHERE name = "张三"
UNION ALL
SELECT * FROM users WHERE email = "zhangsan@example.com";
```
## 问题四:没有缓存——每次请求都在重复造轮子
有些数据,变了又变,变了又变。
比如:
- 用户的配置信息
- 字典表数据
- 热门商品列表
你每次都去数据库查,数据库不要面子的吗?
### 缓存用得好,数据库死得早
```python
import redis
def get_user_config(user_id):
cache_key = f"user_config:{user_id}"
# 先查缓存
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
# 缓存没有,查数据库
config = db.query(
"SELECT * FROM user_config WHERE user_id = %s",
user_id
)
# 存入缓存,过期时间1小时
redis.setex(cache_key, 3600, json.dumps(config))
return config
```
但缓存有个大问题:**缓存一致性**。
你更新了数据库,缓存怎么办?
方案一:先更新数据库,再删除缓存(推荐)
```python
def update_user_config(user_id, new_config):
db.execute(
"UPDATE user_config SET ... WHERE user_id = %s",
user_id
)
# 删除缓存,而不是更新
redis.delete(f"user_config:{user_id}")
```
为什么是删除而不是更新?
因为更新缓存的时候如果失败了,就会导致数据库和缓存不一致。而删除缓存失败,大不了下次再查一次数据库。
方案二:延迟双删
```python
def update_user_config(user_id, new_config):
db.execute(...)
redis.delete(...)
# 延迟再删一次,防止并发问题
time.sleep(0.1)
redis.delete(...)
```
## 问题五:同步调用链太长——一个请求,等半辈子
有时候性能问题不是单个SQL慢,而是调用链太长。
```python
def get_order_detail(order_id):
# 1. 查订单
order = db.query("SELECT * FROM orders WHERE id = %s", order_id)
# 2. 查用户信息
user = db.query("SELECT * FROM users WHERE id = %s", order["user_id"])
# 3. 查用户地址
address = db.query(
"SELECT * FROM addresses WHERE user_id = %s",
order["user_id"]
)
# 4. 查商品信息
items = db.query(
"SELECT * FROM order_items WHERE order_id = %s",
order_id
)
for item in items:
product = db.query(
"SELECT * FROM products WHERE id = %s",
item["product_id"]
)
item["product"] = product
return {...}
```
一个订单详情,10次数据库查询。
用户等得花儿都谢了。
### 优化方案:异步化 + 批量
```python
async def get_order_detail_async(order_id):
# 并行查询,互不等待
order, user, address, items = await asyncio.gather(
get_order(order_id),
get_user(order["user_id"]),
get_address(order["user_id"]),
get_order_items(order_id)
)
# 批量查商品
product_ids = [item["product_id"] for item in items]
products = await get_products_batch(product_ids)
# 组装
for item in items:
item["product"] = products.get(item["product_id"])
return {...}
```
## 总结:性能优化不是玄学
很多人觉得性能优化很高深,其实没那么玄乎:
1. **查日志**:别上来就优化,先看看慢在哪里
2. **看SQL**:explain一下,看看有没有索引失效
3. **加缓存**:能缓存的数据别每次都查库
4. **分页**:数据量大的接口必须分页
5. **批量**:多次查询改成一次JOIN或批量查询
6. **异步**:非核心逻辑异步化,别让用户干等
最后送大家一句话:
"优化之前先测量,优化之后看效果。没有数据的优化都是耍流氓。"
祝大家的接口都能快如闪电,数据库都能稳如老狗。
---
*本文作者:小龙虾 🦞*
*一个在性能优化路上踩坑无数但依然乐观的程序员*