
我们的旗舰应用曾在流量高峰时崩溃,导致大量用户无法访问。查询延迟飙升到 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)SELECTo.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 次/分钟
我们首先尝试查询优化。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 ASSELECTo.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 (SELECTo.id, o.status, o.created_at,c.name, c.emailFROM orders oJOIN customers c ON o.customer_id = c.idWHERE o.status = 'processing'AND o.created_at > NOW() - INTERVAL '24 HOURS')SELECTod.*,os.items_count,os.total_amountFROM order_data odJOIN order_summaries os ON od.id = os.id;
结果: 查询时间优化至 800 毫秒。仍不达标。
我们实施激进缓存策略:
// Redis caching layerconst getOrderDetails = async (orderId) => {const cacheKey = `order:${orderId}:details`;// Try cache firstlet orderDetails = await redis.get(cacheKey);if (orderDetails) {return JSON.parse(orderDetails);}// Cache miss - query databaseorderDetails = await db.query(ORDER_DETAILS_QUERY, [orderId]);// Cache for 5 minutesawait redis.setex(cacheKey, 300, JSON.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 ordersWHERE 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);
结果: 有所改善,但高流量时缓存失效成为新噩梦。
扩展到 5 个只读副本并实施负载均衡:
// Database connection pool with read-write splitconst pool = {write: new Pool({host: 'master.database.aws',max: 20,min: 5}),read: new Pool({hosts: ['replica1.database.aws','replica2.database.aws','replica3.database.aws','replica4.database.aws','replica5.database.aws'],max: 50,min: 10})};// 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();}};
结果: 高峰时段复制延迟难以忍受。
每月因宕机损失 11 万美元收入
工程师团队 38% 时间耗费在数据库问题上
客户满意度持续下降
功能开发停滞
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 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 transactionconst session = await mongoose.startSession();session.startTransaction();// Write to MongoDBconst mongoOrder = await this.createMongoOrder(orderData, session);// Write to PostgreSQLconst pgOrder = await this.createPostgresOrder(orderData);// Verify consistencyif (!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 DataDogconst metrics = {operation_type: change.operationType,execution_time: change.clusterTime.getTime() - change.operationTime.getTime(),collection: 'orders'};await datadog.gauge('mongodb.operation', metrics);// Alert on specific conditionsif (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}};
经过三个月的精心迁移:

在黑色星期五期间零停机时间(处理了超3倍的正常流量)
开发速度提高了57%
客户满意度评分提升了42%
消除了每月11万美元的收入损失,并新增了7.5万美元的收入来源
工程团队的士气大幅提升
如果我们要重新开始这段旅程:
我们会从一个较小的服务入手,而不是从订单处理系统开始。虽然最终也成功了,但这就好比在深水区学习游泳。
我们会在前期更多地投资于团队培训。第一个月非常艰难,因为每个人都需要适应新的范式。
我们会更早地构建更好的监控工具。我们早期的一些性能问题本可以更早被发现。
转向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
汇集2025年讨论度最高的数据库议题,XCOPS智能运维管理人年会将于5月16日在广州举办。大会精选金融核心系统数据库切换、多模态数据库设计、存算分离架构搭建,以及云原生数据库、数仓及数据湖的创新实践等干货案例,就等你扫码一起来探讨↓
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721