前言

最近遇到了一个这样的情况 

在某系统中 大部分通过 rest 接口的 新增接口都是没有问题的 

但是 通过 task 跑的任务的 添加 操作存在问题, 操作都是使用的 jpa 

呵呵 这个当时 令我非常奇怪, 然后 因为当时 项目中的 RestartClassLoader 影响到了一些调试, 所以 当天是没有找出原因 

当然 后面回来回溯一下, 这个调试过程 还是蛮长的  

 

本文调试, 代码基于 spring-boot-* 2.0.0.RELEASE

 

 

测试用例

测试用例如下, 对比差异的话, 只需要吧 addUser 部分更新为同步即可 

其中同步情况下 是可以正常入库的, 但是异步处理之后 就入库不成功了, 并且 从大体上面看整个流程是灭有任何异常的 

在最开始的时候, 我是灭有注意到 TransactionManager 是 Neo4jTransactionManager 的, 在后面调试的过程中虽然看到了这一点, 但是 也没有过分的去关注其中的实现, 导致 调试的过程还是走了一些弯路 

/**
 * RestartClassloaderController
 *
 * @author Jerry.X.He 
 * @version 1.0
 * @date 2021-09-11 09:46
 */
@RestController
@RequestMapping("/RestartClassloaderController")
public class RestartClassloaderController {

    // executors
    public static ExecutorService EXECUTORS = Executors.newFixedThreadPool(5, (r) -> {
        return new Thread(r, "MyExecutor");
    });

    @Resource
    private UserService userService;

    @PostMapping("/addUser")
    public JSONObject addUser(String username) {
        JSONObject result = new JSONObject();
        result.put("username", username);

        EXECUTORS.execute(() -> {
            ClassLoader oldClassloder = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(RestartClassloaderController.class.getClassLoader());
            userService.addUser(result);
            Thread.currentThread().setContextClassLoader(oldClassloder);
        });
        return result;
    }

}

 

UserService 如下, 其中 UserRepository 是一个基于 jpa 的接口, addUser 上面包了一层 @Transactional 

/**
 * UserService
 *
 * @author Jerry.X.He 
 * @version 1.0
 * @date 2021-09-11 09:49
 */
@Service
public class UserService {

    @Resource
    private UserMapper userMapper;
    @Resource
    private UserRepository userRepository;

    /**
     * addUser
     *
     * @param params params
     * @return com.alibaba.fastjson.JSONObject
     * @author Jerry.X.He
     * @date 2021-09-11 09:50
     */
    @Transactional
    public User addUser(JSONObject params) {
        User user = userRepository.findById("jerry").orElse(null);
        User newUser = new User();
        newUser.setName("jjerry");
        newUser.setAge(111);
        userRepository.save(newUser);
        params.put("xx", "xx");
//        throw new RuntimeException("xx");
        return newUser;
    }


}

 

 

问题的调试

首先 回溯一下 我的调试流程, 我们这里事务有两层, 内层是 UserRepository.addUser, 外层是 UserService.addUser, 内层事务没有做太多的事情, 我们这里核心关注 UserService.addUser 的事务 

首先调试的是 出现问题的场景 

来到这里 triggerBeforeCompletion, 可以看到这里根据 返回的 id 可以查询到 User 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_User

 

单步调试之后, 发现从 UserRepository 根据返回的 id 已经查询不出 User 了 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_java_02

 

然后 我们具体跟一下 造成这个问题的原因, 根据调试可以发现是在 TransactionSynchronizationManager.getSynchronizations() 里面 unbind 了 EntityManagerHolder 了之后, UserRepository 拿不到数据了, 当然 这个影响的可能只是 UserRepository 的查询而已[这个过程中没有回滚操作], 那么之前 UserRepository.findById 查询到的数据又在哪里去了呢? 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_数据_03

 

 

呵呵 那么这里 应该是有一些问题, 然后 我们参照 正确的能够入库的场景对比一下 

二者的主要差距是在, TransactionSynchronizationManager.getSynchronizations() 有两个 相比多的一个是 ExtendedEntityManagerSynchronization 一个, 并且 org.springframework.orm.jpa.EntityManagerFactoryUtils$TransactionalEntityManagerSynchronization 实例的 newEntityManager 为 false, 到后面 unbind 的判断没有进去 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_jpa_04

 

然后这个  ExtendedEntityManagerSynchronization 主要是在 EntityManagerUtils.doGetTransactionalEntityManager 里面创建并且添加到 TransactionSynchronizationManager.synchronizations 里面的, 并且这个 ExtendedEntityManagerSynchronization 在这个场景中有着比较重要的作用, 我们待会儿 再说 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_User_05

接着我们在反向对比一下 异常情况下 我们这里 EntityManagerUtils.doGetTransactionalEntityManager 的情况 

可以看到, 我们这里根据 emf 获取 entityManagerHolder 没有获取到, 然后走了的是 else 后面的东西 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_数据_06

 

 然后得到了一个  newEntityManager 为 true 的 TransactionalEntityManagerSynchronization 的实例, 这个导致的是我们上面的 unbind 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_User_07

 

 

能够正常入库的情况下 TransactionSynchronizationManager.resources.emf 是多久注册的?

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_transaction_08

 

来到 EntityManagerUtils.doGetTransactionalEntityManager, 可以确认 emf -> emHolder 是在前面的 OpenSessionInViewInterceptor 里面放进去的 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_User_09

 

 

Neo4jTransactionManager 管理事务 jpa事务 是怎么入库的?

在我们常规的 doCommit 之后去数据库根据这个 id 查询, 是查询不到这个 user 的 

这里可以点 doCommit 进去调试, 可以发现里面是处理 neo4j 的事务的相关业务代码, 因此 和我们的 jpa事务 是没有太大的关系的 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_jpa_10

 

然后在 triggerAfterCommit 之后去数据库根据这个 id 查询, 就可以查询到这个 user 了, 这个能够说明的是 这种情况下 jpa事务 是在 triggerAfterCommit 里面提交的 

而这里之所以能够提交事务, 全部是因为上面多出的这一个 ExtendedEntityManagerSynchronization 这里面获取了 entityManager 关联的 Tranaction 并进行提交[更下面是获取 driver 的Connection, 进行真实的提交] 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_User_11

 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_transaction_12

 

 

Neo4jTransactionManager 管理事务 jpa事务 没有入库的场景?

通过上面的分析, 我们可以知道 事务的 begin 和 commit 分别是来自于 entityManager.joinTransaction 和 ExtendedEntityManagerSynchronization.afterCommit 

那么异步的场景下面 实际上是不存在 mysql 的事务的?, 那么 save 的对象为什么没有入库?

save 了之后这个数据对象又是放在那里的? 是怎么查询到的? 

并且我们在 TransactionAspectSupport.commitTransactionAfterReturning 的时候是能够通过 UserRepository.findById 是能够查询到 user 的, 在 unbind 了 entityManager 之后就查询不到了 是怎么回事呢 ?

 

先看 save 的时候, 做了两件事情 添加了 insert 到 session.actionQueue, 第二件事就是将 id -> $entity 添加到 session.persistenceContext 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_数据_13

 

根据 id 来查询的时候, 可以从 persistenceContext 中查询到实体, 然后返回 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_User_14

我们可以顺便看一下 外部的查询的相关结构 

首先是从 session 的 cache 获取数据, 然后尝试从 secondary cache 获取数据, 最后尝试从 database 获取数据  

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_User_15

 

注意我们这里 save 之后 是还没有入库, 甚至于对于当前事务来说, 也还未执行 insert 操作 

真实的 insert 操作的执行是在 doCommit 的时候 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_jpa_16

 

 driver 层面的 commit 也是在 doCommit 里面

注意 事务的 begin 是在 invoke 外层事务的时候, 创建事务的时候触发的 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_jpa_17

 

 

再回到另外一个问题, 为什么 unbind 了 emf 之后  UserRepositroy.findById 查询不到数据了 ?

因为我们 unbind 了 emf 之后, 这上面 EntityManagerFactoryUtils.doGetTransactionalEntityManager 找不到 emf 对应的 entityManager, 然后重新创建了 entityManager[session对象]

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_数据_18

 

 

解决方式1?  

呵呵 当然 无疑, 这是一种比较牵强的解决方式 

/**
 * RestartClassloaderController
 *
 * @author Jerry.X.He
 * @version 1.0
 * @date 2021-09-11 09:46
 */
@RestController
@RequestMapping("/RestartClassloaderController")
public class RestartClassloaderController {

    // executors
    public static ExecutorService EXECUTORS = Executors.newFixedThreadPool(5, (r) -> {
        return new Thread(r, "MyExecutor");
    });

    @Resource
    private UserService userService;
    @Resource
    private EntityManagerFactory entityManagerFactory;

    @PostMapping("/addUser")
    public JSONObject addUser(String username) {
        JSONObject result = new JSONObject();
        result.put("username", username);

        EXECUTORS.execute(() -> {
            EntityManager em = entityManagerFactory.createEntityManager();
            EntityManagerHolder emHolder = new EntityManagerHolder(em);
            TransactionSynchronizationManager.bindResource(entityManagerFactory, emHolder);

            ClassLoader oldClassloder = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(RestartClassloaderController.class.getClassLoader());
            userService.addUser(result);
            Thread.currentThread().setContextClassLoader(oldClassloder);
        });
        return result;
    }

}

 

 

解决方式2

使用 JpaTransactionManager 来管理 jpa 的事务 

 

 

关于 JpaTransactionManager 

我们在 JpaTransactionManager 里面打上断点 

这里的 beginTransaction 开启了 jpa 事务, 以及后面 bind 了 dataSource 和 emf -> emHolder, 并且设置为已经同步事务 

然后 正常的提交事务, 是在 doCommit 里面 

111 基于 NeoTransactionManager 的 jpa事务 在 rest接口 处理的情况下有效, 异步处理的情况下无效_transaction_19

 

完