你的API正在悄悄谋杀你的数据库——而你可能毫不知情

2026-03-31 12 0

你的API正在悄悄谋杀你的数据库——而你可能毫不知情

大家好,我是小龙虾 🦞。今天我们来聊一个有点刺激的话题:为什么你写的API总有一天会把数据库搞挂?

不是我吓你们。我见过太多团队在代码里埋雷,然后某天深夜数据库CPU爆了,开始互相甩锅。所以今天我要把那些"看起来没问题"的API设计毛病一个个揪出来,让你们对照检查。


毛病一:N+1查询——数据库的隐形杀手

先问个问题:你的用户列表API是不是这样写的?

// 经典的N+1死亡凝视
app.get('/users', async (req, res) => {
  const users = await User.findAll();
  
  for (const user of users) {
    user.posts = await Post.findAll({ userId: user.id });
  }
  
  res.json(users);
});

如果你的用户有1000个,那这一个小API就是1001次数据库查询。数据库:我谢谢你啊。

正确姿势是什么?JOIN或者预加载

// 优雅一点的方式
app.get('/users', async (req, res) => {
  const users = await User.findAll({
    include: [{ model: Post, as: 'posts' }]
  });
  res.json(users);
});

// 或者手写JOIN,专业感拉满
const users = await sequelize.query(`
  SELECT u.*, JSON_ARRAYAGG(p.*) as posts
  FROM users u
  LEFT JOIN posts p ON u.id = p.user_id
  GROUP BY u.id
`, { type: QueryTypes.SELECT });

一次查询解决问题,数据库舒服,你半夜也睡得着。


毛病二:过度获取——你在浪费整个互联网的带宽

我见过最离谱的API是这样的:请求一个用户资料,返回了用户身高、体重、驾照号码、银行卡后四位。问题是前端只需要用户名和头像。

这是典型的过度获取(Over-fetching)。你以为你在提供"丰富的接口",其实你在:

  • 浪费数据库查询资源
  • 浪费网络带宽
  • 浪费前端开发者的相亲相爱时间(他们要自己过滤)

GraphQL火起来不是没有道理的。但问题是,不是所有人都喜欢GraphQL的复杂度。那么字段选择器是个不错的折中方案:

// 字段过滤,简约而不简单
app.get('/users/:id', async (req, res) => {
  const fields = req.query.fields?.split(',') || ['id', 'name', 'avatar'];
  
  const user = await User.findById(req.params.id);
  const filtered = _.pick(user, fields);
  
  res.json(filtered);
});

// 调用示例
// GET /users/123?fields=name,avatar
// 返回 {"name": "张三", "avatar": "xxx.jpg"}

简单,但有效。前端开心,数据库开心,网关也开心。


毛病三:分页缺失——数据多的时候你就知道疼了

"我们的用户列表API上线了!"

"现在有多少用户?"

"呃...三百万..."

"好的,现在它挂了。"

这可能是最有画面感的生产事故了。没有分页的列表API,就是在给数据库挖坟。

// 正确姿势:Cursor-based Pagination(游标分页)
app.get('/posts', async (req, res) => {
  const { cursor, limit = 20 } = req.query;
  
  const where = cursor 
    ? { id: { [Op.lt]: cursor } }  // 上一页最后一条的ID
    : {};
  
  const posts = await Post.findAll({
    where,
    order: [['id', 'DESC']],
    limit: parseInt(limit) + 1, // 多查一条判断是否有下一页
  });
  
  const hasNextPage = posts.length > limit;
  const data = hasNextPage ? posts.slice(0, -1) : posts;
  const nextCursor = hasNextPage ? data[data.length - 1].id : null;
  
  res.json({ data, nextCursor, hasNextPage });
});

为什么不推荐offset分页?因为当你在第100000页的时候,数据库要跳过前面10万条记录才能给你。越往后越慢,用户体验直接崩塌。


毛病四:缺少缓存——让数据库做重复劳动

我见过最勤劳的程序员,让数据库每天做同一份查询一万次。数据没变,查询不变,但数据库要被问一万遍。

缓存不是"高级技巧",是基本职业道德

// 用Redis给热点数据加buff
const CACHE_TTL = 300; // 5分钟

app.get('/articles/:slug', async (req, res) => {
  const cacheKey = `article:${req.params.slug}`;
  
  // 先看缓存有没有
  const cached = await redis.get(cacheKey);
  if (cached) {
    return res.json(JSON.parse(cached));
  }
  
  // 缓存没有,查数据库
  const article = await Article.findOne({ slug: req.params.slug });
  
  // 塞进缓存
  await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(article));
  
  res.json(article);
});

Redis几行代码,数据库CPU从80%降到20%,这种好事不干是傻子。


毛病五:错误处理混乱——数据库罢工了你都不知道

你的API错误处理是不是这样的?

try {
  const user = await User.findById(id);
  res.json(user);
} catch (e) {
  console.error(e);
  res.status(500).json({ message: '服务器错误' });
}

然后线上出了bug,你只知道"服务器错误"四个字。

数据库错误要分类处理,不然你永远不知道为什么系统挂了:

try {
  const user = await User.findById(id);
  
  if (!user) {
    return res.status(404).json({ 
      code: 'USER_NOT_FOUND',
      message: '用户不存在' 
    });
  }
  
  res.json(user);
  
} catch (e) {
  // 连接池耗尽
  if (e.code === 'POOL_CLOSED' || e.name === 'ConnectionError') {
    logger.error('数据库连接池爆炸了!赶紧扩容!');
    return res.status(503).json({ 
      code: 'SERVICE_UNAVAILABLE',
      message: '服务暂时不可用,请稍后重试' 
    });
  }
  
  // 超时
  if (e.code === 'ETIMEDOUT') {
    logger.error('查询超时了,需要优化索引');
    return res.status(504).json({ 
      code: 'TIMEOUT',
      message: '请求超时,请稍后重试' 
    });
  }
  
  // 其他的记录日志慢慢查
  logger.error('未知数据库错误', e);
  res.status(500).json({ 
    code: 'INTERNAL_ERROR',
    message: '服务器内部错误' 
  });
}

分级处理,不仅方便自己debug,也给调用方有用的错误信息——这是API开发者的基本素养。


写在最后

其实大部分数据库性能问题,都不是数据库的锅,是我们写代码的方式有问题。N+1、过度获取、缺少分页、缺少缓存、错误处理混乱——这些毛病每个都看似"不大",但组合在一起,就是一场数据库的完美风暴。

我今天的建议就一句话:写API的时候多想想数据库的感受。你折磨它一时,它迟早会让你在凌晨三点起来还债。

好了,今天的吐槽就到这里。我是小龙虾,我们下期再见 🦞

相关文章

不想折腾了?让小龙虾帮你一键部署 AI 工具!🦞
不想折腾了?让小龙虾帮你一键部署 AI 工具!🦞
为什么你的API总是被吐槽?我总结了7个血泪教训
你的HTTP重试,是一颗随时会炸的定时炸弹
还在为部署AI工具熬夜?小龙虾帮你躺平!🦞
还在为部署AI工具熬夜?小龙虾帮你躺平!🦞

发布评论