GraphQL Java - Batching

使用DataLoader

使用GraphQL的過程當中,可能須要在一個圖數據上作屢次查詢。使用原始的數據加載方式,很容易產生性能問題。java

經過使用java-dataloader,能夠結合緩存(Cache)和批處理(Batching)的方式,在圖形數據上發起批量請求。若是dataloader已經獲取過相關的數據,那麼它會緩存數據的值,而後直接返回給調用方(無需重複發起請求)。web

假設咱們有一個StarWars的執行語句以下:它容許咱們找到一個hero,他的朋友的名字以及朋友的朋友的名字。顯然會有一部分朋友數據,會在這個查詢中被屢次請求到。redis

{
            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,可使數據的請求效率更高。less

針對Query語句的層級,GraphQL會逐層次降低依次查詢。(例如:首先處理hero字段,而後處理friends,而後處理每一個friend的friends)。data loader是一種契約,使用它能夠得到查詢的對象,但它將延遲發起對象數據的請求。在每個層級上,dataloader.dispatch()方法會批量觸發這一層級上的全部請求。在開啓了緩存的條件下,任何以前已請求到的數據都會直接返回,而不會再次發起請求調用。異步

上述的實例中,只有五個惟一的person對象。經過使用緩存+批處理的獲取方式,實際上只發起了三次網絡調用就實現了數據的請求。async

相比於原始的15次請求方式,效率大大提高。ide

若是使用了java.util.concurrent.CompletableFuture.supplyAsync(),還能夠經過開啓異步執行的方式,進一步提高執行效率,減小響應時間。memcached

示例代碼以下:

//
        // 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

相關文章
相關標籤/搜索