放弃SQL改用NoSQL!疯狂的决定让性能提升了5倍……

Crafting-Code 2025-04-28 17:18:12
这个"疯狂"的数据库切换决策,证明了批评者的错误……
 
 
 

 

我们的旗舰应用曾在流量高峰时崩溃,导致大量用户无法访问。查询延迟飙升到 2.5 秒,订单处理失败,错误日志中充斥着死锁报错。

 

"改用 NoSQL 会破坏数据完整性"、"优化 SQL 查询就行了"、"NoSQL 就是个营销噱头"。

 

这些声音我都听过。但我选择无视。

 

因为当所有人都在鼓吹 SQL 优化时,我们的应用正在垂死挣扎。

 

我们面临两个选择:再次纵向扩展 SQL 数据库,或者彻底重构数据架构。批评者说 NoSQL 是玩具,"它处理不了事务",他们警告说。

 

三个月后,我们的应用处理能力提升 5 倍,查询速度加快 10 倍,宕机时间?零。

 

以下是我们的完整实践路径,也许值得你参考。

 

一、初始架构

 

我们的架构堪称教科书级完美:

 

  • PostgreSQL RDS 处理事务数据

  • Redis 作为缓存层

  • Elasticsearch 处理搜索

  • 多个只读副本

  • 精心优化的查询

  • 使用 DataDog 全方位监控

 

我们做了所有"正确"的事情:数据库查询都有索引,表结构严格规范化,团队工程师能闭着眼睛背诵 PostgreSQL 文档。

 

然而...我们不得不推倒重来。

 

二、为何必须改变

 

PostgreSQL 架构已触及瓶颈:

 

  • 复杂 JOIN 查询在负载下耗时 1.5 秒+

  • 行级锁导致持续死锁

  • 纵向扩展成本失控

  • 流量高峰频繁宕机

  • 工程师团队疲于救火而非开发

 

我们尝试了所有 SQL 专家的建议:

 

  • 增加更多索引(收效甚微)

  • 数据分区(仍受写入竞争困扰)

  • 考虑纵向扩展(无法承担 5 万美元的服务器)

 

但现实是:读密集型负载压垮了数据库。无论怎么优化,复杂的 JOIN 查询在规模面前都成了性能噩梦。

 

三、崩溃临界点

 

监控面板揭示残酷真相:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
-- Our most problematic query (2.5s+ execution time)SELECT     o.id, o.status, o.created_at,    c.name, c.email,    p.title, p.price,    i.quantity,    a.street, a.city, a.country,    (SELECT COUNT(*FROM order_items WHERE order_id = o.id) as items_countFROM orders oJOIN customers c ON o.customer_id = c.idJOIN order_items i ON o.id = i.order_idJOIN products p ON i.product_id = p.idJOIN addresses a ON o.shipping_address_id = a.idWHERE o.status = 'processing'    AND o.created_at > NOW() - INTERVAL '24 HOURS'ORDER BY o.created_at DESC;

 

执行计划如同噩梦:

 

  •  
  •  
  •  
  •  
  •  
  •  
Nested Loop  (cost=1.13..2947.32 rows=89 width=325)  ->  Index Scan using orders_created_at on orders  (cost=0.42..1234.56 rows=1000)  ->  Materialize  (cost=0.71..1701.23 rows=89 width=285)        ->  Nested Loop  (cost=0.71..1698.12 rows=89 width=285)              ->  Index Scan using customers_pkey on customers              ->  Index Scan using order_items_pkey on order_items

 

指标全线告急:

 

  • 平均查询时间:1.5 秒+(原 200 毫秒)

  • CPU 使用率:89%

  • IOPS:触顶

  • 缓冲缓存命中率:65%(原 87%)

  • 死锁频率:6-7 次/分钟

 

四、失败的解决方案

 

尝试 #1:查询优化
 

 

我们首先尝试查询优化。DBA 建议:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
- Added composite indexesCREATE INDEX idx_orders_status_created ON orders(status, created_at);CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id);
-- Materialized views for common queriesCREATE MATERIALIZED VIEW order_summaries ASSELECT     o.id,    COUNT(i.id) as items_count,    SUM(p.price * i.quantity) as total_amountFROM orders oJOIN order_items i ON o.id = i.order_idJOIN products p ON i.product_id = p.idGROUP BY o.id;
-- Query rewriteWITH order_data AS (    SELECT         o.id, o.status, o.created_at,        c.name, c.email    FROM orders o    JOIN customers c ON o.customer_id = c.id    WHERE o.status = 'processing'        AND o.created_at > NOW() - INTERVAL '24 HOURS')SELECT     od.*,    os.items_count,    os.total_amountFROM order_data odJOIN order_summaries os ON od.id = os.id;

 

结果: 查询时间优化至 800 毫秒。仍不达标。

 

尝试 #2:Redis缓存
 

 

我们实施激进缓存策略:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// Redis caching layerconst getOrderDetails = async (orderId) => {  const cacheKey = `order:${orderId}:details`;
  // Try cache first  let orderDetails = await redis.get(cacheKey);  if (orderDetails) {    return JSON.parse(orderDetails);  }
  // Cache miss - query database  orderDetails = await db.query(ORDER_DETAILS_QUERY, [orderId]);  // Cache for 5 minutes  await redis.setex(cacheKey, 300JSON.stringify(orderDetails));
  return orderDetails;};
// Cache invalidation on updatesconst updateOrder = async (orderId, data) => {  await db.query(UPDATE_ORDER_QUERY, [data, orderId]);  await redis.del(`order:${orderId}:details`);};

 

甚至增加缓存预热:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// Warm cache for active ordersconst warmOrderCache = async () => {  const activeOrders = await db.query(`    SELECT id FROM orders     WHERE status IN ('processing', 'shipped')     AND created_at > NOW() - INTERVAL '24 HOURS'  `);
  await Promise.all(    activeOrders.map(order => getOrderDetails(order.id))  );};
// Run every 5 minutescron.schedule('*/5 * * * *', warmOrderCache);

 

结果: 有所改善,但高流量时缓存失效成为新噩梦。

 

尝试 #3:只读副本
 

 

扩展到 5 个只读副本并实施负载均衡:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// Database connection pool with read-write splitconst pool = {  writenew Pool({    host'master.database.aws',    max20,    min5  }),  readnew Pool({    hosts: [      'replica1.database.aws',      'replica2.database.aws',      'replica3.database.aws',      'replica4.database.aws',      'replica5.database.aws'    ],    max50,    min10  })};
// Load balancer for read replicasconst getReadConnection = () => {  const replicaIndex = Math.floor(Math.random() * 5);  return pool.read.connect(replicaIndex);};
// Query routerconst executeQuery = async (query, params, queryType = 'read') => {  const connection = queryType === 'write'     ? await pool.write.connect()    : await getReadConnection();      try {    return await connection.query(query, params);  } finally {    connection.release();  }};

 

结果: 高峰时段复制延迟难以忍受。

 

五、使用NoSQL

 

经过三个月优化尝试,我们仍面临:
 

 

  • 每月因宕机损失 11 万美元收入

  • 工程师团队 38% 时间耗费在数据库问题上

  • 客户满意度持续下降

  • 功能开发停滞

  • AWS RDS 成本攀升至 5,750 美元/月

 

AWS RDS 成本分析(5,750 美元/月)
 

 

1、实例成本(约 3,200 美元)

 

1)主数据库实例:db.r6g.4xlarge

  • 按需价格:约 2,000 美元/月

  • 16 vCPU, 128 GB 内存

  • 24/7 生产负载

 

2)只读副本:db.r6g.2xlarge

  • 成本:约 1,200 美元/月

  • 8 vCPU, 64 GB 内存

  • 用于读扩展和备份

 

2、存储成本(约 1,150 美元)

 

1)预置存储:2 TB gp3

  • 基础存储:0.125 美元/GB/月

  • 2000 GB × 0.125 = 250 美元/月

  • 主库和副本均需支付

 

2)备份存储:4 TB

  • 前 2 TB 免费

  • 额外 2 TB 按 0.095 美元/GB/月

  • 2000 GB × 0.095 = 190 美元/月

 

3)IOPS 和吞吐量

  • 额外预置 IOPS:12,000

  • 成本:0.10 美元/IOPS/月

  • 12,000 × 0.10 = 460 美元/月

 

3、网络传输(约 900 美元)

 

1)跨可用区数据传输

  • 复制流量:约 8 TB/月

  • 成本:0.02 美元/GB

  • 8000 GB × 0.02 = 160 美元/月

 

2)互联网数据传输

  • 出站流量:约 15 TB/月

  • 首 TB:0.09 美元/GB

  • 后续 14 TB:0.085 美元/GB

  • 总计:约 740 美元/月

 

4、附加功能(约 500 美元)

 

1)多可用区部署

  • 冗余附加费:约 300 美元/月

 

2)性能洞察

  • 高级监控

  • 长期数据保留

  • 成本:约 200 美元/月

 

5、月度总成本:5,750 美元

  • 实例成本:56%

  • 存储成本:20%

  • 网络传输:16%

  • 附加功能:8%

 

我们需要彻底变革。

 

MongoDB 在讨论中频繁出现。我们犹豫:它能处理事务吗?会破坏数据一致性吗?

这些疑虑合理,但我们别无选择。

 

六、MongoDB 概念验证

 

我们从最棘手的订单处理服务开始迁移,文档模型设计如下:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// MongoDB order document model{  _id: ObjectId("507f1f77bcf86cd799439011"),  status: "processing",  created_at: ISODate("2024-02-07T10:00:00Z"),  customer: {    _id: ObjectId("507f1f77bcf86cd799439012"),    name: "John Doe",    email: "john@example.com",    shipping_address: {      street: "123 Main St",      city: "San Francisco",      country: "USA"    }  },  items: [{    product_id: ObjectId("507f1f77bcf86cd799439013"),    title: "Gaming Laptop",    price: 1299.99,    quantity: 1,    variants: {      color"black",      size: "15-inch"    }  }],  payment: {    method: "credit_card",    status: "completed",    amount: 1299.99  },  shipping: {    method: "express",    tracking_number: "1Z999AA1234567890",    estimated_delivery: ISODate("2024-02-10T10:00:00Z")  },  metadata: {    user_agent: "Mozilla/5.0...",    ip_address: "192.168.1.1"  }}

 

结果:原本在 PostgreSQL 耗时 2.3 秒的查询,在 MongoDB 仅需 200 毫秒。

 

我们通过模式验证保证数据完整性:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// MongoDB schema validationdb.createCollection("orders", {  validator: {    $jsonSchema: {      bsonType"object",      required: ["customer""items""status""created_at"],      properties: {        customer: {          bsonType"object",          required: ["name""email"],          properties: {            name: { bsonType"string" },            email: { bsonType"string" }          }        },        items: {          bsonType"array",          items: {            bsonType"object",            required: ["product_id""price""quantity"],            properties: {              product_id: { bsonType"objectId" },              price: { bsonType"double" },              quantity: { bsonType"int" }            }          }        }      }    }  }});

 

并设置合理索引:

 

  •  
  •  
  •  
  •  
MongoDB indexesdb.orders.createIndex({ "created_at"1"status"1 });db.orders.createIndex({ "customer.email"1 });db.orders.createIndex({ "items.product_id"1 });

 

七、迁移策略

 

采用双写机制确保迁移期间 PostgreSQL 与 MongoDB 数据一致性:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// Dual-write implementationclass OrderService {  async createOrder(orderData) {    try {      // Start MongoDB transaction      const session = await mongoose.startSession();      session.startTransaction();            // Write to MongoDB      const mongoOrder = await this.createMongoOrder(orderData, session);            // Write to PostgreSQL      const pgOrder = await this.createPostgresOrder(orderData);            // Verify consistency      if (!this.verifyOrderConsistency(mongoOrder, pgOrder)) {        throw new Error('Data inconsistency detected');      }            await session.commitTransaction();      return mongoOrder;          } catch (error) {      await session.abortTransaction();      throw error;    }  }    private async verifyOrderConsistency(mongoOrder, pgOrder) {    const checksums = await Promise.all([      this.calculateChecksum(mongoOrder),      this.calculateChecksum(pgOrder)    ]);    return checksums[0] === checksums[1];  }}

 

八、实时监控

 

建立全方位监控体系:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// MongoDB change streams for real-time monitoringconst monitorOrderChanges = async () => {  const changeStream = db.collection('orders').watch();    changeStream.on('change'async (change) => {    // Send metrics to DataDog    const metrics = {      operation_type: change.operationType,      execution_time: change.clusterTime.getTime() - change.operationTime.getTime(),      collection'orders'    };        await datadog.gauge('mongodb.operation', metrics);        // Alert on specific conditions    if (change.operationType === 'update' &&         change.updateDescription.updatedFields.status === 'failed') {      await slack.sendAlert({        channel'#db-alerts',        text`Order ${change.documentKey._id} failed processing`,        level'critical'      });    }  });};// Performance monitoringconst monitorPerformance = async () => {  while (true) {    const stats = await db.collection('orders').stats();        await Promise.all([      datadog.gauge('mongodb.size', stats.size),      datadog.gauge('mongodb.count', stats.count),      datadog.gauge('mongodb.avgObjSize', stats.avgObjSize)    ]);        await sleep(60000); // Check every minute  }};

 

九、结果

 

经过三个月的精心迁移:

 

 

1、业务影响
 

 

  • 在黑色星期五期间零停机时间(处理了超3倍的正常流量)

  • 开发速度提高了57%

  • 客户满意度评分提升了42%

  • 消除了每月11万美元的收入损失,并新增了7.5万美元的收入来源

  • 工程团队的士气大幅提升

 

2、如果重新开始,我们会怎么做?
 

 

如果我们要重新开始这段旅程:

 

  • 我们会从一个较小的服务入手,而不是从订单处理系统开始。虽然最终也成功了,但这就好比在深水区学习游泳。

     

  • 我们会在前期更多地投资于团队培训。第一个月非常艰难,因为每个人都需要适应新的范式。

     

  • 我们会更早地构建更好的监控工具。我们早期的一些性能问题本可以更早被发现。

 

3、最后的总结 
 

 

转向NoSQL对我们来说是正确的选择吗?绝对正确。 

 

我会推荐每个人使用吗?不会。

 

 事实是,数据库选择就像架构风格一样——没有所谓的“最佳”选择。

 

SQL并没有消亡,NoSQL也不是魔法。

 

但对我们来说,在我们的规模下,针对我们特定的问题,它带来了变革。 

 

六个月前,我在凌晨3点42分盯着手机,怀疑自己是否即将犯下职业生涯中最大的错误。

 

今天,我从一个更加平静的地方写下这篇文章,拥有一个能够自主运行的系统和一个对未来充满期待的团队。 有时候,最冒险的选择反而是最安全的。

 

 

 

作者丨Crafting-Code    编译丨Rio

来源丨网址:https://blog.stackademic.com/i-dropped-sql-for-nosql-our-app-now-handles-5x-the-traffic-99d07519d843

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

 

数据库相关活动推荐
 

 

汇集2025年讨论度最高的数据库议题,XCOPS智能运维管理人年会将于5月16日在广州举办。大会精选金融核心系统数据库切换、多模态数据库设计、存算分离架构搭建,以及云原生数据库、数仓及数据湖的创新实践等干货案例,就等你扫码一起来探讨↓

 

最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告