多事务处理下JPA的GenericJDBCException问题,本质是JPA中同一个Bean无法同时进行多事务的写入操作。

场景

单点系统中,员工转正需要更新是否转正状态,并触发一次年假重新计算。
最开始为了提高代码的复用性,把计算年假的操作也放入更新方法中,出现以下情况:
更新操作为@Transactional事务级别,而因为重新计算年假需要再更新之后,所以采用事务传播的方式,但是在子事务中调用到老事务中的xxxDao.save()方法,故而出现以下报错:
GenericJDBCException: could not execute statement

问题代码

@Transactional
public SsoUser update(SsoUserVo ssoUserVo) {
    ...
    // ssoUserDao处理的各种相关业务save()之类
    ssoUser = applicationContext.getBean(SsoUserService.class).saveSsoUser(ssoUser);
    // 依赖于saveUser()方法处理完。如果为转正变更,则执行年假补偿策略
    if (isOfficial) {
        officialDispose(ssoUser.getLoginname());
    }
    return ssoUser;
}

/**
 * 事务优先于update()方法提交,为转正年假补偿做前提
 */
@Transactional(propagation = Propagation.REQUIRES_NEW)
public SsoUser saveSsoUser(SsoUser ssoUser){
    return ssoUserDao.save(ssoUser);
}

/**
 * 执行转正年假补偿策略
 */
public void officialDispose(SsoUserVo ssoUserVo) {
    long id = ssoUserVo.getId();
    SsoUser ssoUser = ssoUserDao.findOne(id);
    // 员工转正
    if(ssoUserVo.getOfficial() == 1 && ssoUserDao.getOfficial() == 0){
        //soa请求处理
    }
}

在执行到ssoUserDao.save(ssoUser);出现GenericJDBCException: could not execute statement,导致数据库抛出Statement cancelled due to timeout or client request异常。
相当于,为方式传播失效时注入自身的SsoUserService在新事务中调用了旧事务SsoUserService的ssoUserDao.save(ssoUser)导致stament构建失效。
如果执行的是其它(ssoUserDao之外)的任何数据库操作,或者是ssoUserDao的读操作,都是没问题的。
有点类似设置了只读(MySQL中是不会默认开启的)。

顺带一提,@Transactional(readOnly = true)开启只读事务(只能进行查询的操作,不能写入),如果只读事务中有增删改操作,会抛出如下异常:
could not execute statement; nested exception is org.hibernate.exception.GenericJDBCException: could not execute statement.

解决思路

放弃多事务传播处理的方式,将操作拆分成两个单独的业务,先更新,后计算年假。虽然有一定的耦合性,但业务更好维护,同时用简单易懂的业务方法也会让团队的维护代价更小。

注意点:因为先执行更新完毕后,所以转正状态已经变更。需要在前端表单提交中额外加一个隐藏标签传入标志参数来判断是否为转正更新(更新方法包含大量字段的更新)。这样做还减少了一次查库。

/**
 * 执行转正年假补偿策略
 */
public void officialDispose(SsoUserVo ssoUserVo) {
    // 员工转正
    if(ssoUserVo.getOfficial() == 1 && ssoUserVo.getOfficialFlag() == 0){
        //soa请求处理
    }
}
Last modification:July 18, 2021
喵ฅฅ