最近因为下面一段代码和同事引发了一些讨论:
js
// count 是一个异步函数,用于统计数据库中某个集合的数量
// find 是一个异步函数,用于查询数据库中某个集合的数据
await Promise.all([
count(),
find(),
]);
同事 :这段代码意图并行执行两个查询操作,但实际上起不到作用。因为两个查询共用一个数据库连接,所以两个查询是串行执行的。 我 :不对,两个查询操作是并行的。虽然共用一个连接,但是
Node
进程会在短时间内不等待 的发送两个查询请求到数据库服务,数据库会并行的执行两个查询操作,最后将结果返回,整体耗时减少了。 同事:我还是不理解。
上面是一个非常经典且重要的性能优化点。让我们一起来详细解释原因以及需要注意的事项。
1. 为什么并行执行更快
想象一下你在瑞幸小程序点单(瑞幸打钱!!!):
-
串行执行 :
- 你点了杯冰美式。
- 你一直在等,直到冰美式做好。
- 冰美式订单完成后,你再点一杯生椰拿铁。
- 再等生椰拿铁做好。
- 总耗时 ≈ 冰美式制作时间 + 生椰拿铁制作时间
-
并行执行 :
- 你同时下单了冰美式和生椰拿铁。
- 咖啡店的店员同时制作这两杯子饮品。
- 你只要等待制作时间最长的那一杯咖啡完成(一般生椰拿铁制作时间会更长)。
- 总耗时 ≈ 生椰拿铁制作时间
在上述的场景中,Node
服务就是你,而数据库就是"瑞幸"。
- 串行调用 :
Node
发送第一个count
查询,然后等待 数据库返回结果。收到结果后,再发送第二个find
查询,再等待结果返回。总耗时是两个查询执行时间的之和。 - 并行调用 :
Node
同时发送两个查询到数据库。数据库收到请求后,会根据内部调度机制(多进程、多线程)同时处理这两个查询。Node
等待两个查询都完成后返回结果(忽略异常情况)。总耗时是两个查询执行时间的最大值。
2. 代码示例
串行执行(稍慢)
js
// 伪代码,只用于示例
const db = new DB();
async function getUsers() {
const count = await db.count();
const users = await db.find().limit(20).offset(0);
return {
count,
users,
};
}
并行执行(更快)
js
// 伪代码,只用于示例
const db = new DB();
async function getUsers() {
const [count, users] = await Promise.all([
db.count(),
db.find().limit(20).offset(0),
]);
return {
count,
users,
};
}
3. 注意事项
虽然并行执行是一种优化性能的方式,但也需要注意以下事项:
- 数据库连接数:数据库通常会限制每个连接的最大并发查询数。如果并行执行的查询数量超过了数据库的连接数,就会导致连接池满,后续的查询会被阻塞。
- 查询依赖:如果并行执行的查询之间存在依赖关系,比如第二个查询依赖第一个查询的结果,那么就不能并行执行。
- 错误处理 :如果使用
Promise.all
发起并行调用,在其中任何一个异步调用发生错误时,会导致整个Promise.all
失败,且不会等待其他异步操作完成。 - 事务处理:如果并行执行的查询涉及到数据库事务,需要注意事务的隔离级别和并发控制机制,避免出现数据不一致的情况。
这里推荐一个 处理
Promise
的库,bluebird
,它不仅兼容原生Promise
,也提供了丰富的Promise
相关方法。如Promise.map
方法可以并行执行多个异步操作,且可以控制并发数。
4. 特殊情况
假设数据库也是单进程的,这个时候并行和串行在总耗时上就没有区别了。但是一般数据库或服务都会采用线程池的机制,来处理多个并发请求。