在 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);
});
});
}
}
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;
}
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
,则会校验指定的对象是否满足 get
、set
、delete
、clear
这四个接口,如果满足则使用,否则报错。也就是说,用户指定的缓存对象必须要有这四个函数才能被 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();
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 1
和 User 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);
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;
}
}
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 可以清除缓存:clear
和 clearAll
。他们的代码都非常简单,就不做过多说明了。代码如下:
// 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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
关注微信公众号,获取最新推送~
加微信,深入交流~