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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM