为后台线程创建JPA会话



我们通过JPA和Spring使用Hibernate来管理web应用程序中的对象持久性。我们使用open-session-in-view模式为响应http请求的线程创建会话。我们还使用了一些不生成视图的线程——它们只是不时地唤醒来完成它们的工作。这会产生问题因为默认情况下没有打开会话所以会产生异常,比如

org.hibernate.SessionException: Session is closed! 

 could not initialize proxy - no Session

我们发现,如果每个后台线程都在一个带有@Transactional注释的方法中调用它的逻辑,那么就没有这种异常,因为@Transactional确保线程在事务内部时有会话。

它解决了一段时间的问题,但我不认为这是一个很好的解决方案-使长时间运行的方法事务性会导致问题,因为其他线程无法看到在数据库中所做的更改,直到事务提交。

我创建了一个java伪代码示例来更好地说明我的问题:

public class FirstThread {
    ...
    @Transactional
    public void processQueue() {
        for(element : queue){
            if(elementCanBeProcessed(element)){
                elementDao.saveIntoDatabase(element);
                secondThread.addToQueue(element.getId());
            }
        }
    }
    private boolean elementCanBeProcessed(element){
        //code that gets a few objects from database and processes them
    }
}

如果我用@Transactional注释整个processQueue方法,

中所做的更改
elementDao.saveIntoDatabase(element);
在事务提交之前(也就是在整个队列被处理之前),secondThread中不会看到

。如果我不这样做,那么线程将不会在elementCanBeProcessed内部有会话,它将无法访问数据库。我也不能注释elementCanBeProcessed,因为它是这个类中的私有方法,我必须将其移动到另一个类中,以便Spring代理可以工作。

是否可以将会话绑定到线程而不使整个方法事务性?我应该如何管理会话和事务在后台线程像一个吗?

这是我在阅读了Amir Moghimi的回答后编写的代码。这看起来有点"hacky",因为文档说EntityManagerHolder和TransactionSynchronizationManager都不应该被典型的应用程序代码直接使用。

@Service
public class DatabaseSessionManager {
    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;
    public void bindSession() {
        if (!TransactionSynchronizationManager.hasResource(entityManagerFactory)) {
            EntityManager entityManager = entityManagerFactory.createEntityManager();
            TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(entityManager));
        }
    }
    public void unbindSession() {
        EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager
                .unbindResource(entityManagerFactory);
        EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
    }
}

它似乎正在工作-会话在bindSession()unbindSession()调用之间绑定到我的线程,我不必创建一个事务来实现它。

我不知道有什么spring现成的解决方案。我认为你需要实现一个类似于OpenEntityManagerInViewInterceptor类。

基本上,你需要使用TransactionSynchronizationManager来bindResource()一个实例的EntityManagerHolder当你的线程启动和unbindResource()当你的线程完成。

OpenEntityManagerInViewInterceptor的核心部分是:

    if (TransactionSynchronizationManager.hasResource(getEntityManagerFactory())) {
        ...
    }
    else {
        logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
        try {
            EntityManager em = createEntityManager();
            EntityManagerHolder emHolder = new EntityManagerHolder(em);
            TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), emHolder);
            ...
        }
        catch (PersistenceException ex) {
            throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
        }
    }

如果你实现了,请把代码贴在这里作为答案。

如果您希望每个元素在自己的事务中处理,则需要:

  1. processQueue中删除@Transactional
  2. elementCanBeProcessed逻辑移动到单独的ElementProcessor @Component类中,并使用@Transactional注释elementCanBeProcessed
  3. 呼叫elementProcessor.elementCanBeProcessed(element);

因为elementProcessor是一个独立的bean,它被Spring代理,因此调用将被TransactionInterceptor截获。

避免跨任务长时间运行的事务,使事务较小,并且不太可能发生冲突/回滚。以下是我在没有JTA和@Transactional的情况下依赖JPA所做的:

注入实体管理器或从Spring上下文中获取或作为引用传递:

@PersistenceContext
private EntityManager entityManager;    

然后创建一个新的实体管理器,以避免使用共享的:

EntityManager em = entityManager.getEntityManagerFactory().createEntityManager();

现在你可以启动事务并使用Spring DAO, Repository, JPA等

private void save(EntityManager em) {
    try
    {         
        em.getTransaction().begin();                
        <your database changes>
        em.getTransaction().commit();                        
    }
    catch(Throwable th) {
        em.getTransaction().rollback();
        throw th;
    }        
}

您还需要在数据库@Configuration中提供JPA bean,但很可能您已经有了。

基于Amir和Pawel的回答以及JpaTemplate的想法,我创建了一个组件来完成模板角色:

@Component
@Slf4j
public class EntityManagerProvider {
    private final EntityManagerFactory entityManagerFactory;
    public EntityManagerProvider(EntityManagerFactory entityManagerFactory) {
        this.entityManagerFactory = entityManagerFactory;
    }
    /**
     * Attaches a JPA {@link EntityManager} to the current thread (if none is already present).
     * For use within Springboot background threads (such as ones associated with a scheduler),
     * when we don't want to enclose all work within a single transaction.
     *
     * @param operation lambda that needs to run with a JPA session available.
     */
    public void withEntityManager(Operation operation) {
        if (isEntityManagerBoundToThread()) {
            operation.get();
        } else {
            withNewEntityManager(operation);
        }
    }
    @FunctionalInterface
    public interface Operation {
        void get();
    }
    private boolean isEntityManagerBoundToThread() {
        return TransactionSynchronizationManager.hasResource(entityManagerFactory);
    }
    private void withNewEntityManager(Operation operation) {
        try {
            bindEntityManager();
            operation.get();
        } finally {
            unbindEntityManager();
        }
    }
    private void bindEntityManager() {
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(entityManager));
    }
    private void unbindEntityManager() {
        try {
            EntityManagerHolder holder =
                    (EntityManagerHolder) TransactionSynchronizationManager.unbindResource(entityManagerFactory);
            EntityManagerFactoryUtils.closeEntityManager(holder.getEntityManager());
        } catch (Throwable t) {
            log.error("Failed to unbind EntityManager", t);
        }
    }
}

然后像这样从调用代码中使用:

entityManagerProvider.withEntityManager(() -> {
    ... code using JPA operations ...
});

最新更新