我们的旗舰应用曾在流量高峰时崩溃,导致大量用户无法访问。查询延迟飙升到 2.5 秒,订单处理失败,错误日志中充斥着死锁报错。
"改用 NoSQL 会破坏数据完整性"、"优化 SQL 查询就行了"、"NoSQL 就是个营销噱头"。
这些声音我都听过。但我选择无视。
因为当所有人都在鼓吹 SQL 优化时,我们的应用正在垂死挣扎。
我们面临两个选择:再次纵向扩展 SQL 数据库,或者彻底重构数据架构。批评者说 NoSQL 是玩具,"它处理不了事务",他们警告说。
三个月后,我们的应用处理能力提升 5 倍,查询速度加快 10 倍,宕机时间?零。
以下是我们的完整实践路径,也许值得你参考。
监控面板揭示残酷真相:
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 次/分钟
我们首先尝试查询优化。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);
CREATE 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;
WITH 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 毫秒。仍不达标。
我们实施激进缓存策略:
const getOrderDetails = async (orderId) => { const cacheKey = `order:${orderId}:details`;
let orderDetails = await redis.get(cacheKey); if (orderDetails) { return JSON.parse(orderDetails); }
orderDetails = await db.query(ORDER_DETAILS_QUERY, [orderId]); await redis.setex(cacheKey, 300, JSON.stringify(orderDetails));
return orderDetails;};
const updateOrder = async (orderId, data) => { await db.query(UPDATE_ORDER_QUERY, [data, orderId]); await redis.del(`order:${orderId}:details`);};
甚至增加缓存预热:
const 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)) );};
cron.schedule('*/5 * * * *', warmOrderCache);
结果: 有所改善,但高流量时缓存失效成为新噩梦。
扩展到 5 个只读副本并实施负载均衡:
const 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 })};
const getReadConnection = () => { const replicaIndex = Math.floor(Math.random() * 5); return pool.read.connect(replicaIndex);};
const 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(); }};
结果: 高峰时段复制延迟难以忍受。
我们从最棘手的订单处理服务开始迁移,文档模型设计如下:
// 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 毫秒。
我们通过模式验证保证数据完整性:
db.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 });
经过三个月的精心迁移:
如果我们要重新开始这段旅程:
我们会从一个较小的服务入手,而不是从订单处理系统开始。虽然最终也成功了,但这就好比在深水区学习游泳。
我们会在前期更多地投资于团队培训。第一个月非常艰难,因为每个人都需要适应新的范式。
我们会更早地构建更好的监控工具。我们早期的一些性能问题本可以更早被发现。
转向NoSQL对我们来说是正确的选择吗?绝对正确。
我会推荐每个人使用吗?不会。
事实是,数据库选择就像架构风格一样——没有所谓的“最佳”选择。
SQL并没有消亡,NoSQL也不是魔法。
但对我们来说,在我们的规模下,针对我们特定的问题,它带来了变革。
六个月前,我在凌晨3点42分盯着手机,怀疑自己是否即将犯下职业生涯中最大的错误。
今天,我从一个更加平静的地方写下这篇文章,拥有一个能够自主运行的系统和一个对未来充满期待的团队。 有时候,最冒险的选择反而是最安全的。