2017-8-30,大幅修改本文,加了图,简化了例子。
简介
Dataloader
(官方网址)是由facebook推出,能大幅降低数据库的访问频次,经常在Graphql场景中使用。
Dataloader机制
主要通过2个机制来降低数据库的访问频次:批处理
和缓存
。
批处理
自动批处理在一个Nodejs任务执行单元中[1],如果多次调用同一类数据,为避免数据库访问,Dataloader可以使用传入的批量处理函数一次访问后台服务,具体例子如下:
import Sequelize from 'sequelize'
import DataLoader from 'dataloader'
// 定义表结构
const sequelize = new Sequelize('test', null, null, {
dialect: 'sqlite',
})
const UserModel = sequelize.define('user', {
name: Sequelize.STRING
})
await sequelize.sync({force: true})
//插入测试数据
await [
UserModel.create({name: 'ron'}),
UserModel.create({name: 'john'}),
]
// 初始化DataLoader,传入一个批处理函数
const userLoader = new DataLoader(keys => UserModel.findAll({where: {name: {$in: keys}}}))
// 以下2个Load语句会被自动批处理,合并成一次数据库的操作
await [
userLoader.load('ron'),
userLoader.load('john')
]
以上代码就仅会产生以下一条数据库查询语句:
Executing (default): SELECT id, name, createdAt, updatedAt FROM users AS user WHERE user.name IN ('ron', 'john’);
缓存
load一次,DataLoader就会把数据缓存在内存,下一次再load时,就不会再去访问后台。
DataLoader缓存的是promise,而不是具体数据。则意味着:
let ron1, ron2
await ron1 = userLoader.load('ron')
await ron2 = userLoader.load('ron')
assert(ron1 !== ron2) // true,这个容易理解
assert(userLoader.load('ron') === userLoader.load('ron')) // 还是true,因为是缓存promise
DataLoader不能取代redis
等应用级别的缓存,DataLoader只是一台服务器级别的内存缓存,是redis
的补充。DataLoader缓存的典型应用是per-request
范围的缓存,如下例子:
const app = express()
const loaderMiddleware = (req, res, next) => {
req.loader = {
users: new DataLoader(ids => UserModel.findAll({id: {$in: ids}}))
}
}
app.get('/users/:id', loaderMiddleware (req, res) => {
req.loaer.users.load(req.params.id)
//...
})
app.listen()
缓存机制默认是打开的,当然也可以在初始化DataLoader的时候配置缓存关闭,如下:
new DataLoader(ids => UserModel.findAll({id: {$in: ids}}), {cache: false})
当修改了数据后,我们可能需要清除缓存,以便于下一次获得更新过的数据,这时候我们可以这样做:userLoader.clear('ron')
实现原理
dataloader的实现原理是:把每一次load都推迟到Next Tick
[2]后集中批处理运行。
具体的说就是:DataLoader把个Nodejs任务执行单元中[1]的多个load操作集中放到promiseJob queue
中,并使用DataLoader初始化时传入的批量操作完成。
dataloader的代码非常少,总共是有300行,而且有大半的注释。
关于使用
dataloader并不像我先前想的是一个超级工具,其实它的使用场景有限,基本上只能优化loadById
。
dataloader的最佳使用场景是解决Graphql中的N+1查询问题,如下:
# 定义
type User {
name: String,
friends: [User]
}
#查询
{
users {
name
friends {
name
friends {
name
}
}
}
}
graphql
支持嵌套查询,如果没有dataloader
,就会出现严重的N+1
查询性能问题,而通过使用dataloader
,数据库的访问频次指数级别下降。我参与的最近一个项目,其中一个复杂页面,通过使用dataloader
,数据库的访问次数从74降低到了10次。
javascript的运行机制简介
javascript运行机制中主要依靠:1个堆栈:call stack
; 2个job queue
:promiseJob queue
和scriptJob queue
[3]
-
job
:我的理解就是一个javascript函数 -
call stack
:一个javascript函数在运行时会调用其它函数,这就引入了call stack
,函数调用函数时就会推入堆栈,这也是我们经常说的同步调用。javascript会不间断的运行完所有call stack
的内容; -
promiseJob queue
:当运行到promise时,javascript会把then中的函数放入该队列,当当前job运行完毕后(call stack
中的代码运行完毕后),就会从该queue中取最早加入的job运行;Next Tick
,也是把一个函数放到task queue
中去,意思是到下一个Event Loop
的时候运行 -
scriptJob queue
:我的理解是javascript内置解释器
-
见本文的
javascript的运行机制简介
部分 ↩ -
见 javascript 标准 中关于job queue的叙述 ↩
网友评论
```
const userLoader = new DataLoader(ids => User.find({ _id: { $in: ids } }));
user: ({ user }) => userLoader.load(user._id),
```
```
const DataLoader = require('dataLoader')
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
const main =async () => {
const MyModel = mongoose.model('testtest', { name: String, _id: Number });
mongoose.set('debug', true);
// prepare data
await MyModel.remove({_id: {$in: [1,2,3]}})
await MyModel.create({_id: 1, name: 'ron'})
await MyModel.create({_id: 2, name: 'jack'})
await MyModel.create({_id: 3, name: 'tom'})
const find = ids => MyModel.find({_id: {$in: ids}})
.then(records => ids.map(x=>records.find(y=>y =>y._id === x)))
const loader = new DataLoader(find)
await loader.load(1)
.then(x => console.log(`found: ${x}`))
.catch(x => console.error(`error: ${x}`))
mongoose.disconnect()
}
main()
```