使用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)。如果不去显式指定的话,它默认会自动添加进来。
graphql.execution.AsyncExecutionStrategy是dataLoader的唯一执行策略。这个执行策略可以自行确定dispatch的最佳时间,它通过追踪还有多少字段未完成,以及它们是否为列表值等来实现此目的。
其他的执行策略,例如:ExecutorServiceExecutionStrategy策略无法实现该功能。当data loader检测到并未使用AsyncExecutionStrategy策略时,它会在遇到每个field时都调用data loader的dispatch方法。虽然可以通过缓存值的方式减少请求次数,但无法使用批量请求策略。
如果正在发起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);
采用data loader的编码模式,通过将所有未完成的data loader请求合并为一个批量加载的请求,提高了请求的效率。
GraphQL - Java会追踪那些尚未完成的data loader请求,并在最合适的时间调用dispatch方法,触发数据的批量请求。