GraphQL Java - Batching

使用DataLoader

使用GraphQL的过程中,可能需要在一个图数据上做多次查询。使用原始的数据加载方式,很容易产生性能问题。

通过使用java-dataloader,可以结合缓存(Cache)和批处理(Batching)的方式,在图形数据上发起批量请求。如果dataloader已经获取过相关的数据,那么它会缓存数据的值,然后直接返回给调用方(无需重复发起请求)。

假设我们有一个StarWars的执行语句如下:它允许我们找到一个hero,他的朋友的名字以及朋友的朋友的名字。显然会有一部分朋友数据,会在这个查询中被多次请求到。

        {
            hero {
                name
                friends {
                    name
                    friends {
                       name
                    }
                }
            }
        }

其查询结果如下所示:

        {
          "hero": {
            "name": "R2-D2",
            "friends": [
              {
                "name": "Luke Skywalker",
                "friends": [
                  {"name": "Han Solo"},
                  {"name": "Leia Organa"},
                  {"name": "C-3PO"},
                  {"name": "R2-D2"}
                ]
              },
              {
                "name": "Han Solo",
                "friends": [
                  {"name": "Luke Skywalker"},
                  {"name": "Leia Organa"},
                  {"name": "R2-D2"}
                ]
              },
              {
                "name": "Leia Organa",
                "friends": [
                  {"name": "Luke Skywalker"},
                  {"name": "Han Solo"},
                  {"name": "C-3PO"},
                  {"name": "R2-D2"}
                ]
              }
            ]
          }
        }

比较原始的实现方案是,每次query的时候都调用一次DataFetcher来获取一个person对象。

在这种场景下,将会发起15次调用,并且其中有很多数据被多次、重复请求。结合dataLoader,可以使数据的请求效率更高。

针对Query语句的层级,GraphQL会逐层次下降依次查询。(例如:首先处理hero字段,然后处理friends,然后处理每个friend的friends)。data loader是一种契约,使用它可以获得查询的对象,但它将延迟发起对象数据的请求。在每一个层级上,dataloader.dispatch()方法会批量触发这一层级上的所有请求。在开启了缓存的条件下,任何之前已请求到的数据都会直接返回,而不会再次发起请求调用。

上述的实例中,只有五个唯一的person对象。通过使用缓存+批处理的获取方式,实际上只发起了三次网络调用就实现了数据的请求。

相比于原始的15次请求方式,效率大大提升。

如果使用了java.util.concurrent.CompletableFuture.supplyAsync(),还可以通过开启异步执行的方式,进一步提升执行效率,减少响应时间。

示例代码如下:

        //
        // a batch loader function that will be called with N or more keys for batch loading
        // This can be a singleton object since it's stateless
        //
        BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() {
            @Override
            public CompletionStage<List<Object>> load(List<String> keys) {
                //
                // we use supplyAsync() of values here for maximum parellisation
                //
                return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys));
            }
        };


        //
        // use this data loader in the data fetchers associated with characters and put them into
        // the graphql schema (not shown)
        //
        DataFetcher heroDataFetcher = new DataFetcher() {
            @Override
            public Object get(DataFetchingEnvironment environment) {
                DataLoader<String, Object> dataLoader = environment.getDataLoader("character");
                return dataLoader.load("2001"); // R2D2
            }
        };

        DataFetcher friendsDataFetcher = new DataFetcher() {
            @Override
            public Object get(DataFetchingEnvironment environment) {
                StarWarsCharacter starWarsCharacter = environment.getSource();
                List<String> friendIds = starWarsCharacter.getFriendIds();
                DataLoader<String, Object> dataLoader = environment.getDataLoader("character");
                return dataLoader.loadMany(friendIds);
            }
        };


        //
        // this instrumentation implementation will dispatch all the data loaders
        // as each level of the graphql query is executed and hence make batched objects
        // available to the query and the associated DataFetchers
        //
        // In this case we use options to make it keep statistics on the batching efficiency
        //
        DataLoaderDispatcherInstrumentationOptions options = DataLoaderDispatcherInstrumentationOptions
                .newOptions().includeStatistics(true);

        DataLoaderDispatcherInstrumentation dispatcherInstrumentation
                = new DataLoaderDispatcherInstrumentation(options);

        //
        // now build your graphql object and execute queries on it.
        // the data loader will be invoked via the data fetchers on the
        // schema fields
        //
        GraphQL graphQL = GraphQL.newGraphQL(buildSchema())
                .instrumentation(dispatcherInstrumentation)
                .build();

        //
        // a data loader for characters that points to the character batch loader
        //
        // Since data loaders are stateful, they are created per execution request.
        //
        DataLoader<String, Object> characterDataLoader = DataLoader.newDataLoader(characterBatchLoader);

        //
        // DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
        // in this case there is 1 but you can have many.
        //
        // Also note that the data loaders are created per execution request
        //
        DataLoaderRegistry registry = new DataLoaderRegistry();
        registry.register("character", characterDataLoader);

        ExecutionInput executionInput = newExecutionInput()
                .query(getQuery())
                .dataLoaderRegistry(registry)
                .build();

        ExecutionResult executionResult = graphQL.execute(executionInput);

如上,我们添加了DataLoaderDispatcherInstrument实例。因为我们想要调整它的初始化选项(Options)。如果不去显式指定的话,它默认会自动添加进来。

使用AsyncExecutionStrategy策略的Data Loader

graphql.execution.AsyncExecutionStrategy是dataLoader的唯一执行策略。这个执行策略可以自行确定dispatch的最佳时间,它通过追踪还有多少字段未完成,以及它们是否为列表值等来实现此目的。

其他的执行策略,例如:ExecutorServiceExecutionStrategy策略无法实现该功能。当data loader检测到并未使用AsyncExecutionStrategy策略时,它会在遇到每个field时都调用data loader的dispatch方法。虽然可以通过缓存值的方式减少请求次数,但无法使用批量请求策略。

request特定的Data Loader

如果正在发起Web请求,那么数据可以特定于请求它的用户。 如果有特定于用户的数据,且不希望缓存用于用户A的数据,然后在后续请求中将其提供给用户B。

DataLoader实例的作用域很重要。为每个web请求创建dataLoader实例,并确保数据仅仅缓存在该web请求中,而对于其他web请求无效。它也确保了调用仅仅影响本次graphql的执行,而不影响其他的graphql请求执行。

默认情况下,DataLoaders充当缓存。 如果访问到之前请求过的key的值,那么它们会自动返回它以便提高效率。

如果数据需要在多个web请求当中共享,那么需要修改data loader的缓存实现,以使不同的请求之间,其data loader可以通过一些中间层(如redis缓存或memcached)共享数据。

在使用的过程中,仍然为每次请求都创建一个data loaders,通过缓存层在不同的data loader之间开启数据共享。

        CacheMap<String, Object> crossRequestCacheMap = new CacheMap<String, Object>() {
            @Override
            public boolean containsKey(String key) {
                return redisIntegration.containsKey(key);
            }

            @Override
            public Object get(String key) {
                return redisIntegration.getValue(key);
            }

            @Override
            public CacheMap<String, Object> set(String key, Object value) {
                redisIntegration.setValue(key, value);
                return this;
            }

            @Override
            public CacheMap<String, Object> delete(String key) {
                redisIntegration.clearKey(key);
                return this;
            }

            @Override
            public CacheMap<String, Object> clear() {
                redisIntegration.clearAll();
                return this;
            }
        };

        DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(crossRequestCacheMap);

        DataLoader<String, Object> dataLoader = DataLoader.newDataLoader(batchLoader, options);

异步调用batch loader功能

采用data loader的编码模式,通过将所有未完成的data loader请求合并为一个批量加载的请求,提高了请求的效率。

GraphQL - Java会追踪那些尚未完成的data loader请求,并在最合适的时间调用dispatch方法,触发数据的批量请求。

TODO

posted on 2019-09-16 18:37  PKU_荐辕  阅读(1283)  评论(2编辑  收藏  举报

导航