我有一个正在运行的 Web 应用程序,它有一个用于搜索项目的端点。中间层的
search
方法看起来像这样:
private ListingScoringFunction scoringFunc;
@Transactional(readOnly = true)
public Page<Listing> search(ListingSearchParameters params) {
Page<Long> pageOfListingIds = listingSearchRepo.searchForListingIds(params);
return pageOfListingIds.map(eachListingId -> {
var eachEntity = listingRepo.getOne(eachListingId).get();
var score = scoringFunc.score(eachEntity);
// Map the Hibernate entity to a serializable DTO
return ModelMappings.toListing(eachEntity, score);
});
}
当我调用
ModelMappings.toListing()
时,我访问了一堆延迟加载的字段。目前,这是可行的,因为所有操作都封装在活动会话/事务中。
由于延迟加载的属性,我多次访问数据库。由于系统中的其他限制,我不想将这些字段更改为急切加载。我想通过多线程提高吞吐量:当一个线程正在等待 DB I/O 时,将执行转移到准备运行评分函数的线程。
我创建了一个带有固定线程池的
ExecutorService
,将其连接到我的服务中,并将代码修改为如下所示:
private ListingScoringFunction scoringFunc;
private ExecutorService scoringExecutor;
@Transactional(readOnly = true)
public Page<Listing> search(ListingSearchParameters params) {
Page<Long> pageOfListingIds = listingSearchRepo.searchForListingIds(params);
List<Listing> scoredListings = scoreListingsById(pageOfListingIds);
return new PageImpl<>(scoredListings);
}
private List<Listing> scoreListingsById(Page<Long> listingIds) {
List<Listing> scoredListings;
List<CompletableFuture<Listing>> resultList = listingIds.stream()
.map(eachListingId -> {
Supplier<Listing> scoreTask = () -> this.convertToScoredListing(eachListingId);
return scoreTask;
})
.map(each -> CompletableFuture.supplyAsync(each, scoringExecutor))
.toList();
CompletableFuture.allOf(resultList.toArray(new CompletableFuture[0])).join();
scoredListings = resultList.stream()
.map(eachFuture -> eachFuture.getNow(null))
.filter(Objects::nonNull)
.toList();
return scoredListings;
}
@Transactional
public Listing convertToScoredListing(Long eachListingId) {
var eachListing = listingRepo.findById(eachListingId).get();
var score = scoringFunc.score(eachListing);
Listing scoredListing = ModelMappings.toListing(eachListing, score);
return scoredListing;
}
当我运行这个新代码并尝试从线程池中访问延迟加载字段时,我收到了可怕的“无会话”错误:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of ___,
could not initialize proxy - no Session
据我了解,这通常意味着我调用代码的方式使得 Spring/Hibernate 无法判断我已经跨越了事务边界(由
@Transactional
注释定义)。奇怪的是, findById()
调用成功,但随后的 ListingEntity.getLazyLoadedField()
调用失败并抛出异常。我打开了一些 Hibernate 日志记录,发现它为 listingRepo.findById()
调用启动了一个事务,但随后立即提交它,这样当我访问惰性字段时,事务已提交并且会话已关闭。我将此解释为,当我调用 @Repository
实例时,Hibernate 可以知道这是一个已跨越的事务边界。我不明白的是,在我提交给Supplier
的ExecutorService
中,我调用convertToScoredListing()
,用@Transactional
注释。我不明白为什么这不足以为整个方法的执行提供一个会话。
我想避免的可能的混乱解决方案:
convertToScoredListing()
移至另一个类,以使边界已被跨越的情况更加明显。当方法与类紧密耦合时,将方法从类中强制取出似乎很混乱。将数据库访问代码卸载到线程的正确配置是什么?如何让 Hibernate 在会话中运行我的线程?
嗯,我认为你的第一种方法有
n+1
问题,因为你在循环内查询getOne
或findById
。如果您打开 spring.jpa.show-sql=true
并计算查询数量,它将是 pageSize + 1
(counting page
又为 1)。这就是问题的根本原因。
有多种方法可以解决这个问题,但我在你的帖子中看到的最简单的是,不要使用方法
getOne
或 findById
在 Repository
中创建此方法
List<ListingEntity> findByIdIn(Collection<Long> ids);
然后应用到服务中
@Transactional(readOnly = true)
public Page<Listing> search(ListingSearchParameters params) {
Page<Long> pageOfListingIds = listingSearchRepo.searchForListingIds(params);
List<ListingEntity> listingEntities = repo.findByIdIn(pageOfListingIds.getContent());
List<Listing> listings = convertToScoredListings(listingEntities);
return new PageImpl<>(listings, pageable, pageOfListingIds.getTotalElements());
}
@Transactional
public Listing convertToScoredListings(List<ListingEntity> listingEntities) {
return listingEntities.stream()
.map(entity -> {
var score = scoringFunc.score(eachListing);
return ModelMappings.toListing(eachListing, score);
})
.collect(toList());
}
现在你可以检查一下,Hibernate 只生成了 3 个查询(选择 ids、选择计数、按 ids 选择列表实体)
帖子的第二部分很有趣,多线程提高性能。是的,可能是这样,但是当我们申请写入数据而不是像这样读取数据时(只有 3 个查询),这会很有帮助。