LazyInitializationException:如何将数据库密集型进程卸载到执行器,同时仍然获得 Hibernate 的事务支持?

问题描述 投票:0回答:1

我有什么

我有一个正在运行的 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 在会话中运行我的线程?

java multithreading hibernate spring-data
1个回答
0
投票

嗯,我认为你的第一种方法有

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 个查询),这会很有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.