美文网首页HTML5/JS
DataLoader介绍

DataLoader介绍

作者: 荣观 | 来源:发表于2017-03-15 19:36 被阅读3426次

    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 queuepromiseJob queuescriptJob 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内置解释器

    1. 就是当前job的执行上下文

    2. 见本文的javascript的运行机制简介部分

    3. javascript 标准 中关于job queue的叙述

    相关文章

      网友评论

      • 本王今年八岁:你好,为什么我完全无法使用?文档看了十几遍也是没法跑

        ```
        const userLoader = new DataLoader(ids => User.find({ _id: { $in: ids } }));
        user: ({ user }) => userLoader.load(user._id),
        ```
        荣观:试试下面的例子。在初始化DataLoader时,必须返回一个数组的promise。

        ```
        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()
        ```

      本文标题:DataLoader介绍

      本文链接:https://www.haomeiwen.com/subject/uirrnttx.html