在 Dataloader 的构造函数中,有三个配置项与缓存有关:

  • cache 是否开启缓存,为 true 表示开启缓存,默认为 true。
  • cacheKeyFn 缓存 key 的生成函数,从 load 函数的 key 生成缓存 key.
  • cacheMap 存储数据的缓存对象,默认为 new Map(),也可以是满足要求的任何对象。

缓存的实现

我们上一篇说过,load 函数每次都会先检查一次缓存,如果缓存直接命中,就直接返回缓存的数据。

// load 函数的部分实现
var cacheMap = this._cacheMap;

if (cacheMap) {
  var cachedPromise = cacheMap.get(cacheKey);
  if (cachedPromise) {
    var cacheHits = batch.cacheHits || (batch.cacheHits = []);
    return new Promise((resolve) => {
      cacheHits.push(() => {
        resolve(cachedPromise);
      });
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

this._cacheMap 是在构造函数中通过 getValidCacheMap 获取的。

function getValidCacheMap<K, V, C>(
  options: ?Options<K, V, C>
): CacheMap<C, Promise<V>> | null {
  var shouldCache = !options || options.cache !== false;
  if (!shouldCache) {
    return null;
  }
  var cacheMap = options && options.cacheMap;
  if (cacheMap === undefined) {
    return new Map();
  }
  if (cacheMap !== null) {
    var cacheFunctions = ['get', 'set', 'delete', 'clear'];
    var missingFunctions = cacheFunctions.filter(
      (fnName) => cacheMap && typeof cacheMap[fnName] !== 'function'
    );
    if (missingFunctions.length !== 0) {
      throw new TypeError(
        'Custom cacheMap missing methods: ' + missingFunctions.join(', ')
      );
    }
  }
  return cacheMap;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

getValidCacheMap 的代码可以看到,如果没有指定 cacheMap,则默认是返回 new Map() 作为缓存对象。

如果指定了 cacheMap,则会校验指定的对象是否满足 getsetdeleteclear 这四个接口,如果满足则使用,否则报错。也就是说,用户指定的缓存对象必须要有这四个函数才能被 Dataloader 使用。

使用请求级别的缓存

load 函数的代码可以看到,每次调用 load(key),都会先尝试从缓存中读取。不同的用户访问同一个资源可能获取的信息不同,比如权限不同等。如果使用全局缓存的话,会导致各种各样的问题。因此,我们需要确保只缓存单次请求级别。

有两种方式可以实现,一种是确保 Dataloader 是请求级别,及在请求的开始实例化 Dataloader。

下面的代码展示了如何在 express 中使用请求级别的 Dataloader.

function createLoaders(authToken) {
  return {
    users: new DataLoader((ids) => genUsers(authToken, ids)),
  };
}

const app = express();

app.get('/', function (req, res) {
  const authToken = authenticateUser(req);
  const loaders = createLoaders(authToken);
  res.send(renderPage(req, loaders));
});

app.listen();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

另一种是借助于 async_hooksopen in new window 模块,全局缓存来管理请求缓存,即每次请求的缓存单独管理,请求结束释放缓存。因为一些限制,无法使用第一种方式,笔者在项目中使用了这个方法,受篇幅限制这里我们不做详细实现了。

命中缓存与批处理

load 函数的源码解读中我们可以看到,即使命中了缓存,函数的调用结果也不是立即返回的,而是等到与 batchLoadFn 的结果一起返回。也就是说命中缓存的调用和需要发起请求的调用会一起返回。为什么这么设计的?这么做是为了 Dataloader 能为后续的数据请求做优化。

举个例子,假如 User 1 一开始是被缓存了的(借助于 prime 函数,后面说)。但是因为 User 1User 2 是一个时间周期内调用的,那么他们的结果也会在同一时间返回。这样的话,后续的 user.bestFriendID 请求也会在下一个时间周期同时请求。那么,总共会想远端服务发送两个请求(查用户一次,查好友一次)。

userLoader.prime(1, { bestFriend: 3 });

async function getBestFriend(userID) {
  const user = await userLoader.load(userID);
  return await userLoader.load(user.bestFriendID);
}

// In one part of your application
getBestFriend(1);

// Elsewhere
getBestFriend(2);
1
2
3
4
5
6
7
8
9
10
11
12

如果命中的缓存立即返回的话,那么查 User 1 好友的请求可能会提前发出,那么就可能会造成总共需要发送三次数据查询请求了。因为每个 user.bestFriendID 的查询时间不一样。

手动缓存数据 prime

prime 函数允许我们手动地向缓存中插入数据。代码如下:

prime(key: K, value: V | Error): this {
    var cacheMap = this._cacheMap;
    if (cacheMap) {
      var cacheKey = this._cacheKeyFn(key);

      // Only add the key if it does not already exist.
      if (cacheMap.get(cacheKey) === undefined) {
        // Cache a rejected promise if the value is an Error, in order to match
        // the behavior of load(key).
        var promise;
        if (value instanceof Error) {
          promise = Promise.reject(value);
          // Since this is a case where an Error is intentionally being primed
          // for a given key, we want to disable unhandled promise rejection.
          promise.catch(() => {});
        } else {
          promise = Promise.resolve(value);
        }
        cacheMap.set(cacheKey, promise);
      }
    }
    return this;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

我们可以看到,如果要插入缓存的 key 已经存在,则直接忽略,不会更新缓存。否则会向缓存中插入一个 Promise,然后等待 batchScheduleFn 调度。

prime 函数还支持缓存错误信息,只需要保证缓存的值是 Error 类的一个实例就可以了。

清除缓存

Dataloader 有两个 API 可以清除缓存:clearclearAll。他们的代码都非常简单,就不做过多说明了。代码如下:

// clear 函数实现
clear(key: K): this {
  var cacheMap = this._cacheMap;
  if (cacheMap) {
    var cacheKey = this._cacheKeyFn(key);
    cacheMap.delete(cacheKey);
  }
  return this;
}

// clearAll 函数实现
clearAll(): this {
  var cacheMap = this._cacheMap;
  if (cacheMap) {
    cacheMap.clear();
  }
  return this;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

关注微信公众号,获取最新推送~

加微信,深入交流~