我们的旗舰应用曾在流量高峰时崩溃,导致大量用户无法访问。查询延迟飙升到 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_count
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
JOIN addresses a ON o.shipping_address_id = a.id
WHERE 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 indexes
CREATE 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 AS
SELECT
o.id,
COUNT(i.id) as items_count,
SUM(p.price * i.quantity) as total_amount
FROM orders o
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
GROUP 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_amount
FROM order_data od
JOIN 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 indexes
db.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分盯着手机,怀疑自己是否即将犯下职业生涯中最大的错误。
今天,我从一个更加平静的地方写下这篇文章,拥有一个能够自主运行的系统和一个对未来充满期待的团队。 有时候,最冒险的选择反而是最安全的。