2 min read

Nodejs项目性能优化技巧(数据结构和并发请求等)

一次性能优化技巧总结

Table of Contents

背景

接口的响应时间非常长,直到超时。排查原因,一种是sql串行查询,一种是sql查询返回的数据量大之后,在内存操作过程很慢。

优化思路

单机优化方向:避免同步阻塞代码,缓存热点数据,并发执行异步任务等。

分布式优化方向:微服务架构,增加机器(容器编排)

sql请求并发优化

该接口涉及sql串行查询,适用于Promise.all(). Promise.all()方法接收一个数组,数组中的每个元素都是Promise对象,Promise.all()方法返回一个Promise对象,只有当数组中的所有Promise对象都成功时,才会成功,只要有一个失败,就会失败。然后所有请求是并发执行的。最慢的那个请求时长就等于Promise.all的时长。

避免同步阻塞代码

接口拿到数据后,需要在内存中经过一步合并操作,方法代码是:

const slowMergeBy = (_id, arrays) => {
  const merged = [];
  arrays.forEach(array => {
    array.forEach(i => {
      const hit = merged.findIndex(m => m[_id] === i[_id]);
      if (hit >= 0) {
        merged[hit] = Object.assign({}, merged[hit], i);
      } else {
        merged.push(i);
      }
    });
  });
  return merged;
};

分析复杂度后发现,方法的时间复杂度是O(n^2),n为二维数组展平后的长度。这是因为,findIndex()的复杂度是O(n),而数组的forEach()方法复杂度是O(n),所以整个方法复杂度是O(n^2)。所以突破口是查找这一步骤。ES6提供了Map数据结构,它可以保证查找的复杂度是O(1),所以可以替换findIndex()方法。改进后的方法代码是:

const fastMergeBy = (_id, arrays) => {
  const map = new Map();
  for (const array of arrays) {
    for (const item of array) {
      const key = item[_id];
      if (!map.has(key)) {
        map.set(key, { ...item });
      } else {
        Object.assign(map.get(key), item);
      }
    }
  }
  return Array.from(map.values());
};

benchmark测试结果显示:fastMergeBy()方法比slowMergeBy()方法快了100倍左右。

附上JavaScript 各种内建数据结构在查找、插入、删除操作上的时间复杂度如下所示(理论时间复杂度 + 常用场景说明):

数据结构查找插入删除说明
ObjectO(1)O(1)O(1)键为字符串或 symbol;性能略受原型链影响
MapO(1)O(1)O(1)更现代、支持任意键类型;性能更稳定
SetO(1)O(1)O(1)值唯一的集合结构,底层也是哈希表实现
ArrayO(n)O(1)*O(n)查找需遍历;末尾插入是 O(1),中间插入是 O(n)
TypedArrayO(1)O(1)O(n)固定类型数组(如 Int32Array);可直接索引
WeakMapO(1)O(1)O(1)键只能是对象,自动垃圾回收
WeakSetO(1)O(1)O(1)值只能是对象,自动垃圾回收

总结

串行的异步任务,且相互独立的,可以走并发来减少阻塞时间。

涉及在内存中操作大量数据的方法,要分析方法的时间复杂度,选择合适的数据结构(及其背后的算法)。


Jason Lee
Hi, I'm Jason Lee

I hope I can still be curious about the world when I turn 60 years old.

Enjoy!

Contact me:

Email | GitHub


Content licenced under CC BY-NC-ND 4.0