Facebook Dataloader

簡介

DataLoader 是facebook推出的一款通用工具,可為傳統(tǒng)應(yīng)用層與持久層之間提供一款緩存批處理的操作。JS、Java、Ruby、Go等主流語言都有開源三方庫支持。尤其在Graphql興起后,DataLoader被廣泛地應(yīng)用于解決N+1查詢問題。

機(jī)制

DataLoader的實現(xiàn)原理很簡單:就是把每一次load推遲到nextTick中集中處理。在現(xiàn)實開發(fā)中其主要有兩點應(yīng)用:

  1. 批處理操作
batch.png
  1. 內(nèi)存級別的緩存
cache.png

案例

以下以nodejs中調(diào)用dynamoose api為例具體介紹一下DataLoader在dynamodb查詢時的一些使用方法。

批處理 (Batching)

先看一下傳統(tǒng)DAO設(shè)計實現(xiàn)中的模版方法。我們通過name來獲取user的信息。

// #UserDao.ts

import {ModelConstructor} from 'dynamoose';
import {DataSchema, userModel} from './User.schema'; 

export default class UserDao {

  private readonly model: ModelConstructor<DataSchema, string>;

  constructor() {
    this.model = userModel; //Dynamoose user schema model
  }

  public getUser(name: string) {
    console.log('Get user:', name);
    return this.model.get(name);
  }
}

當(dāng)我們調(diào)用getUser方法時:

import UserDao from './UserDao';

async function run(user) {
  const users = await Promise.all([
    user.getUser('Garlic'),
    user.getUser('Onion'),
    ]);

  console.log('return:', users);
}

run(new UserDao());

打印結(jié)果是:

Get user: Garlic
Get user: Onion
return: [ Model-user { name: 'Garlic' }, Model-user { name: 'Onion' } ]

顯然,dynamodb被訪問了兩次。再看一下使用DataLoader后的情況。

// #UserLoader.ts

import Dataloader = require('dataloader');
import {DataSchema, userModel} from './User.schema';

const BatchLoadFn = (names: [string]) => {
  console.log('Get Keys:', names);
  return userModel.batchGet(names);
};

export default class UserLoader {

  private readonly loader: Dataloader<string, DataSchema>;

  constructor() {
    this.loader = new Dataloader<string, DataSchema>(BatchLoadFn);
  }

  public getUser(name: string) {
    return this.loader.load(name);
  }
}

DataLoader初始化時必須傳入一個BatchLoadFn,在dataloader/index.d.ts里找到如下定義:

type BatchLoadFn<K, V> = (keys: K[]) => Promise<Array<V | Error>>;

BatchLoadFn的參數(shù)是數(shù)組且返回是個包在Promise里的數(shù)組。因此可以直接調(diào)用dynamoose的batchGet方法。

再次調(diào)用上述的run方法

async function run(user) {
  const users = await Promise.all([
    user.getUser('Garlic'),
    user.getUser('Onion'),
    ]);

  console.log('return:', users);
}

run(new UserLoader());

看一下輸出結(jié)果:

Get Keys: [ 'Garlic', 'Onion' ]
return: [ Model-user { name: 'Garlic' }, Model-user { name: 'Onion' } ]

返回一樣,但是兩次get方法被合并成了一次batchGet了。

不過,在使用dynamoose的batchGet的時候,會出現(xiàn)一些奇妙的bug;稍微改動一下getUser的順序,把GarlicOnion換一下。

async function run(user) {
  const users = await Promise.all([
    user.getUser('Onion'),
    user.getUser('Garlic'),
    ]);

  console.log('return:', users);
}

返回變成了:

# run(new UserDao());
Get user: Onion
Get user: Garlic
return: [ Model-user { name: 'Onion' }, Model-user { name: 'Garlic' } ]

---

# run(new UserLoader());
Get Keys: [ 'Onion', 'Garlic' ]
return: [ Model-user { name: 'Garlic' }, Model-user { name: 'Onion' } ]

userLoader返回的內(nèi)容錯了,先返回了Garlic,后返回Onion。這個是很多NoSql數(shù)據(jù)庫搜索算法共通的問題。

改動一下BatchLoadFn,將batchGet的返回結(jié)果按name排序。

const BatchLoadFn: any = (names: [string]) => {
  console.log('Get Keys:', names);
  return userModel.batchGet(names)
    .then((users) => {

      const usersByKey: object = users.reduce(
        (acc, user) => Object.assign(acc, {[user.name]: user}), {});

      return names.map((name) => usersByKey[name]);
    });
};

OK,這下輸出正常了。

Get Keys: [ 'Onion', 'Garlic' ]
return: [ Model-user { name: 'Onion' }, Model-user { name: 'Garlic' } ]

緩存

async function run(user) {
  const users = await Promise.all([
    user.getUser('Onion'),
    user.getUser('Onion'),
    ]);

  console.log('return:', users);
}

設(shè)想一下,如果兩次getUser都是Onoin會怎么樣?

Get Keys: [ 'Onion' ]
return: [ Model-user { name: 'Onion' }, Model-user { name: 'Onion' } ]

結(jié)果是一次getUser后,DataLoader會把數(shù)據(jù)緩存到內(nèi)存里,下一次get相同的User時,就不會再調(diào)用BatchLoadFn了。事實上,DataLoader緩存的是Promise。如下:

assert(user.getUser('Onion') === user.getUser('Onion')) // true

Dataloader在默認(rèn)機(jī)制下是啟動cache的,也可以選擇關(guān)閉cache。

new Dataloader<KeySchema, DataSchema>(BatchLoadFn, {cache: false});
//duplicated keys in batchGet may occur error.

在出錯或是更新時也可調(diào)用clear方法清除cache。

public getUser(name: string) {
  return this.loader.load(name)
    .catch((e) => {
      this.loader.clear(name);
      return e;
    });
}

此外,在初始化Dataloader時可以自定義cache策略:new DataLoader(batchLoadFn [, options])

Option Key Type Default Description
cache Boolean true 設(shè)置為false則停用cache
cacheKeyFn Function key => key cacheKeyFn返回只能是string或number, 如key為object,可設(shè)為key => JSON.stringify(key)
cacheMap Object new Map() 自定義cache算法, 如DataloaderCacheLru

API

DataLoader并不是一個超級工具,代碼也只有300多行,而且相當(dāng)部分是注釋。它只提供了5個API,基本只能完成loadByKey相關(guān)的操作。

  1. load(key: K): Promise<V>;

  2. loadMany(keys: K[]): Promise<V[]>;

  3. clear(key: K): DataLoader<K, V>;

  4. clearAll(): DataLoader<K, V>;

  5. prime(key: K, value: V): DataLoader<K, V>;

Graphql

DataLoader被廣泛應(yīng)用于Graphql的resolver中,

# Define in graphql type def
type User {
  name: String
  friends: [User]
}

# Query in front-end
{
  user(name: "Onion") {
    name
    friends {
      name
      friends {
        name
      }
    }
  }
}
# user.resolver.ts
Query: {
  user: (root, {name}) => {
    return userDao.getUser(name);
  }
}
User: {
  friends: (root) => {
    return Promise.all( root.friends.map( (name) => userDao.getUser(name) ) );
  }
}

Onion朋友的朋友中必然有Onion自己。Graphql支持嵌套查詢,假如直接調(diào)用傳統(tǒng)UserDao的getUser方法, 數(shù)據(jù)庫查詢單個Onion的次數(shù)將會是1+len(friends)。

若將上述代碼中的userDao換成userLoader,Onion的數(shù)據(jù)庫訪問就只有一次了。這就解決了N+1查詢的問題。

Query: {
  user: (root, {name}) => {
    return userLoader.getUser(name);
  }
}
User: {
  friends: (root) => {
    return Promise.all( root.friends.map( (name) => userLoader.getUser(name) ) );
  }
}

小結(jié)

今天大體介紹了一下DataLoader的機(jī)制和使用方法。在現(xiàn)實開發(fā)中我們可以將dataloader專門作為一層架構(gòu),對應(yīng)用層做cache,對數(shù)據(jù)層做batch。甚至有項目將DataLoader與redis集成(redis-dataloader)。
我參與的其中一個項目在使用DataLoader優(yōu)化Graphql查詢后,DB訪問數(shù)減少了3/4。尤其是用到DynamoDB這類按查詢收費的服務(wù)時,DataLoader不僅可以加速前端訪問速度,還可以極大地減少后端運維成本。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 昨晚孩子發(fā)燒,我也一晚上沒睡。白天送去醫(yī)院檢查、打點滴,看看孩子逐漸長大,感覺自己最近陪他太少了。中午染頭發(fā)后去公...
    TA77范麗萍閱讀 188評論 0 0
  • 微商是個全民皆商的平臺!而健康產(chǎn)品將會是未來發(fā)展的一個主導(dǎo)作用!不管是那一個年代,那一個發(fā)展階段,健康體系都應(yīng)該與...
    草根的命運LOVE閱讀 236評論 5 2
  • 一.成功要素雷達(dá)分析圖 二.6個月后達(dá)成目標(biāo) 1.個人健康達(dá)標(biāo)/體重68㎏,現(xiàn)81㎏,每月減重目標(biāo)2㎏!通過晚餐...
    壵_0268閱讀 246評論 0 0
  • 在昨天到小半月之前,感覺自己還有那么一點糾結(jié),大姨媽遇上徒增的工作量(白天處理各種外聯(lián)事件,到了下班開始上...
    Imnice閱讀 197評論 0 0
  • 一個人無助站在馬路中間不知道往哪走了 人來人往 沒方向沒目標(biāo) 有特別想要去的地方只是有 特別想要去的地方 ...
    姝疏閱讀 116評論 0 0

友情鏈接更多精彩內(nèi)容