Spring声明式事务可能出现的问题

Spring声明式事务可能出现的问题

在日常的Spring项目开发中,一般都是在方法上通过使用@Transactional注解来声明事务,从而保证方法内多个对数据库的操作的一致性和原子性。但是使用@Transactional注解时如果不稍加注意就有可能出现事务不生效以及出错后事务不回滚等问题。接下来通过模拟一个简单的业务场景来看看使用@Transactional究竟有哪些坑

业务逻辑

  1. 根据用户名注册用户
  2. 如果用户名为"test"则在注册时抛出异常
  3. 注册抛出异常时需要回滚
  • Entity
@Entity
@Data
@Table(name = "user_tb")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = AUTO)
    private long id;

    private String name;

    public UserEntity() {
    }

    public UserEntity(String name) {
        this.name = name;
    }
}
  • Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {

    List<UserEntity> findByName(String name);
}
  • Controller
    @GetMapping("test")
    public int wrong1(@RequestParam("name") String name) {
        userService.createUser(new UserEntity(name));
        return userService.getUserCount(name);
    }

1. @Transactional注解失效

失败场景1:private方法上的@Transactional事务失效

  • Service

@Slf4j
@Service
public class UserService {

    private Logger logger = LoggerFactory.getLogger(UserService.class);

    @Autowired
    private UserRepository userRepository;

    // 公共方法供Controller调用,内部调用事务性的私有方法
    public int createUser(String name) {
        try {
            this.createUserPrivate(new UserEntity(name));
        } catch (Exception ex) {
            logger.error("create user failed because {}", ex.getMessage());
        }
        return userRepository.findByName(name).size();
    }

    //标记了@Transactional的private方法
    @Transactional
    private void createUserPrivate(UserEntity userEntity) {
        userRepository.save(userEntity);
        if (userEntity.getName().contains("test")) {
            throw new RuntimeException("invalid username!");
        }
    }
}

在这段代码中,同公共的createUser方法调用一个声明了@Transactional注解的createUserPrivate私有方法,期望在保存一个name="test"的用户时,私有方法能够抛出异常,从而使事务回滚,test用户数据无法保存到数据库,然而实际情况是createUserPrivate方法的确抛出了异常,但是test用户数据仍然保存到了数据库之中,也就是说其@Transactional注解并未生效。这是因为Spring默认通过动态代理的方式实现AOP对目标方法进行增强,而private方法无法代理到故Spring无法动态增强事务处理逻辑。
面对这个问题,很容易想到的办法就是将private该为public,然而这样做也是有问题的。

失败场景2:未通过Spring代理过的类从外部调用目标方法导致事务失效

  • 将createUserPrivate方法改为公共方法
public int createUser(String name) {
        try {
            this.createUserPrivate(new UserEntity(name));
        } catch (Exception ex) {
            logger.error("create user failed because {}", ex.getMessage());
        }
        return userRepository.findByName(name).size();
    }
    
@Transactional
    public void createUserPrivate(UserEntity userEntity) {
        userRepository.save(userEntity);
        if (userEntity.getName().contains("test")) {
            throw new RuntimeException("invalid username!");
        }
    }

把createUserPrivate改为public后,注册name='test'的用户,虽然会报异常但是@Transactional注解仍未生效,test用户仍然成功保存到数据库之中,这是因为这里是通过this来调用createUserPrivate()方法,而this指针代表对象自己,此时的对象并不是通过spring注入的,故不能通过Spring的动态代理增强事务功能,为此,可以尝试注入一个UserService类然后再调用该方法。

成功案例:

  • 方式1:注入一个UserService类,再通过该类来调用createUserPrivate()方法
@Autowired
    UserService self;
public int createUser(String name) {
        try {
            self.createUserPrivate(new UserEntity(name));
        } catch (Exception ex) {
            logger.error("create user failed because {}", ex.getMessage());
        }
        return userRepository.findByName(name).size();
    }
    
@Transactional
    public void createUserPrivate(UserEntity userEntity) {
        userRepository.save(userEntity);
        if (userEntity.getName().contains("test")) {
            throw new RuntimeException("invalid username!");
        }
    }

  • 方式2:因为controller层注入了UserService类并调用了createUser()方法,因此将@Transactional注解声明到createUser()方法上,这样就能通过Spring注入的UserService类实现动态增强
@Transactional
public int createUser(String name) {
        try {
           createUserPrivate(new UserEntity(name));
        } catch (Exception ex) {
            logger.error("create user failed because {}", ex.getMessage());
        }
        return userRepository.findByName(name).size();
    }

2. 事务生效但未能回滚

失败场景1:异常未能传播出@Transactional声明的方法导致事务未能回滚

  • 在@Transactional声明的方法内部通过try catch捕获异常,是异常不被抛出
@Transactional
    public int createUser(String name) throws Exception{
        try {
            userRepository.save(new UserEntity(name));
            throw new RuntimeException("error");

        } catch (Exception ex) {
            logger.error("create user failed because {}", ex.getMessage());
        }
        userRepository.save(new UserEntity(name));
    }

由于在方法内catch了抛出的Exception,导致异常无法从方法中传递出去,而在SpringTransactionAspectSupport方法内有个invokeWithinTransaction方法,通过这个方法用来处理事务,这个方法只有在捕获到异常之后才会进行后续的事务处理,如果没有捕获到异常则不会进行回滚。


try {
   // This is an around advice: Invoke the next interceptor in the chain.
   // This will normally result in a target object being invoked.
   retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
   // target invocation exception
   completeTransactionAfterThrowing(txInfo, ex);
   throw ex;
}
finally {
   cleanupTransactionInfo(txInfo);
}

失败场景2:IOException不会导致事务的回滚

  • 方法抛出IOException
    //即使出了受检异常也无法让事务回滚
    @Transactional
    public void createUser(String name) throws IOException {
        userRepository.save(new UserEntity(name));
        otherTask();
    }

    //因为文件不存在,一定会抛出一个IOException
    private void otherTask() throws IOException {
        Files.readAllLines(Paths.get("file-that-not-exist"));
    }
}

由于Spring默认在出现RuntimeException或Error时才会回滚事务,而IOException并不会触发事务的回滚


/**
 * The default behavior is as with EJB: rollback on unchecked exception
 * ({@link RuntimeException}), assuming an unexpected outcome outside of any
 * business rules. Additionally, we also attempt to rollback on {@link Error} which
 * is clearly an unexpected outcome as well. By contrast, a checked exception is
 * considered a business exception and therefore a regular expected outcome of the
 * transactional business method, i.e. a kind of alternative return value which
 * still allows for regular completion of resource operations.
 * <p>This is largely consistent with TransactionTemplate's default behavior,
 * except that TransactionTemplate also rolls back on undeclared checked exceptions
 * (a corner case). For declarative transactions, we expect checked exceptions to be
 * intentionally declared as business exceptions, leading to a commit by default.
 * @see org.springframework.transaction.support.TransactionTemplate#execute
 */
@Override
public boolean rollbackOn(Throwable ex) {
   return (ex instanceof RuntimeException || ex instanceof Error);
}

3.事务传播配置不合理

  • 在原有createUser方法中再添加一个子方法subCreateUser,并且subCreateUser作为一个事务单独回滚,不影响主用户的注册
    @Transactional
    public void createUserWrong(UserEntity userEntity) {
        createMainUser(userEntity);
        try {
            subUserService.createSubUserWithExceptionWrong(userEntity);
        } catch (Exception e) {
            log.error("create sub user error:{}", e.getMessage());
        }
    }
    
    private void createMainUser(UserEntity userEntity) {
        userRepository.save(userEntity);
        logger.info("createMainUser finish");
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createSubUserWithExceptionWrong(UserEntity entity) {
        log.info("createSubUserWithExceptionWrong start");
        userRepository.save(entity);
        throw new RuntimeException("invalid status");
    }

在实际执行时,由于主方法注册用户的逻辑和子方法注册子用户的逻辑是同一个事务,而方法出现了异常,标记了事务需要回滚,而此时主方法的事务无法提交,故在Controller里出现了一个UnexpectedRollbackException异常,指示这个事务回滚了且是静默回滚,导致当前事务无法提交。为解决这个问题可以在子方法的@Transactional注解上添加propagation=Propagation.REQUIRES_NEW来设置REQUIRES_NEW的方式的事务传播策略,也就是在执行这个方法时需要开启新的事务并挂起当前事务。

posted @ 2020-07-07 18:14  三弦音无  阅读(272)  评论(0编辑  收藏  举报