dbaplus社群 前天 20:17
放弃SQL改用NoSQL!疯狂的决定让性能提升了5倍……
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文分享了电商应用从传统SQL数据库迁移至NoSQL数据库的实践经验。面对SQL数据库的性能瓶颈和高昂成本,团队果断拥抱NoSQL,通过细致的架构设计、双写机制和实时监控,成功解决了高并发下的性能问题,提升了应用稳定性,并最终实现了业务的快速发展。文章总结了迁移过程中的经验教训,为面临类似挑战的团队提供了宝贵的参考。

🚀 **SQL数据库的困境:** 随着业务增长,原有的PostgreSQL数据库架构暴露出性能瓶颈,复杂的JOIN查询导致查询延迟高,频繁的行级锁和死锁问题,以及高昂的纵向扩展成本,最终导致了应用在高峰时段的崩溃和宕机。

💡 **NoSQL的尝试与实践:** 经过多次SQL优化尝试(如查询优化、缓存策略、只读副本扩展)均未根本解决问题后,团队决定采用NoSQL数据库MongoDB。通过文档模型设计、模式验证和合理的索引,解决了性能问题,原本耗时2.3秒的查询在MongoDB仅需200毫秒。

🔄 **迁移策略与监控:** 为了确保迁移期间的数据一致性,采用了双写机制,将数据同时写入PostgreSQL和MongoDB。此外,还建立了全方位的实时监控体系,利用MongoDB的Change Streams监控数据库操作,并结合Datadog进行性能监控和告警,确保了系统的稳定运行。

原创 Crafting-Code 2025-04-28 07:15 广东

有时候,最冒险的选择反而是最安全的。



这个"疯狂"的数据库切换决策,证明了批评者的错误……





我们的旗舰应用曾在流量高峰时崩溃,导致大量用户无法访问。查询延迟飙升到 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日在广州举办。大会精选金融核心系统数据库切换、多模态数据库设计、存算分离架构搭建,以及云原生数据库、数仓及数据湖的创新实践等干货案例,就等你扫码一起来探讨↓


                        阅读原文

                        跳转微信打开

                        Fish AI Reader

                        Fish AI Reader

                        AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

                        FishAI

                        FishAI

                        鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

                        联系邮箱 441953276@qq.com

                        相关标签

                        NoSQL 数据库迁移 MongoDB 性能优化
                        相关文章