积跬步至千里

如何正确的处理spring的声明式事务

1. 前言

Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,我们大多数做业务开发的时候,通常就在业务方法上使用声明式注解 @Transactional 来开启事务,大多数我们就没有去关注事务是否会生效,出错后事务是否能正确回滚,所以这里是有“坑”的。
事务没有正确处理,对于我们来说通常是不易发现的,当压力越来越大数据越来越多的时候,极有可能带来大量的数据不一致脏数据的问题,所以处理好事务极为重要。

2. Spring事务没有生效

首先来看一下,Spring 在什么情况下事务是不生效的,在这里为了方便我直接就采用了Sping JPA 作为数据库访问,首先定义一个实体类

@Entity
@Data
@NoArgsConstructor
public class SysUser {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;

    public SysUser(String name) {
        this.name = name;
    }
}

实现一个repository 接口,里面有一个根据名字查SysUser的方法

@Repository
public interface SysUserRepository extends JpaRepository<SysUser, Long> {
    List<SysUser> findByName(String name);
}

实现一个SysUserService,其中使用一个公有方法调用标记了 @Transactional 注解的私有方法

@Service
@Slf4j
public class SysUserService {
    @Resource
    private SysUserRepository sysUserRepository;

    /**
     * 公有方法调用标记了 @Transactional 注解的私有方法
     * @param name
     * @return
     */
    public int createUserWrong(String name) {
        this.createUserPrivate(new SysUser(name));
        return this.sysUserRepository.findByName(name).size();
    }

    @Transactional
    private void createUserPrivate(SysUser sysUser) {
        this.sysUserRepository.save(sysUser);
        if (sysUser.getName().contains("test")) {
            throw new RuntimeException("invalid name");
        }
    }
}

实现一个Controller如下

@RestController
@RequestMapping("/proxyfailed")
@RequiredArgsConstructor
public class ProxyFailedController {

    private final SysUserService sysUserService;

    @GetMapping("/wrong1")
    public int wrong1(@RequestParam String name) {
        return this.sysUserService.createUserWrong1(name);
    }
}

测试接口可以发现,程序报异常了,但是数据库已经却成功的插入了记录,事务并未生效!!!

image
image

其实在上面已经看出来了,idea会在当你使用@Transactional 标记 private 修饰的方法时报红。
@Transactional生效的原则之一就是,只有定义在public方法上的 @Transactional 注解才能生效。这是因为Spring 默认使用动态代理实现AOP,对目标方法进行增强,private 修饰的方法是无法被代理到的。
那如果说,我把上面的 createUserPrivate 方法改为 public 修饰,那么事务是否会生效呢?

image

答案是否定的,事务是依然不会生效的。要使 @Transactional生效的原则之二就是,必须通过代理类从外部调用目标方法才能生效。在这里,使用this调用目标方法,this指向的并不是代理类,而是当前目标类实例。
在这里可以在目标Service类注入自己的Bean 实例,如下:

  @Resource
    private SysUserService sysUserService;
    public int createUserRight1(String name) {
        this.sysUserService.createUserPublic(new SysUser(name));
        return this.sysUserRepository.findByName(name).size();
    }

image

可以看到此时 this.sysUserService是通过cglib增强过的代理类实例,所以此时 @Transactional 注解是生效的。但是自己注入自己是一件很怪的事情,最好还是在controller中直接调用被 @Transactional标记的 public 方法,使事务生效,这里可以看到this.sysUserService同样是被增强后的代理类

那接下来我们看看下面这种情况, @Transactional有没有生效,也就是标记了 @Transactional的public方法调用private修饰的方法,且在private方法中进行了数据库操作

 @GetMapping("/right3")
    public int right3(@RequestParam String name) {
        return this.sysUserService.createUserRight3(name);
    }
 @Transactional
    public int createUserRight3(String name) {
        this.createUserPrivate1(new SysUser(name));
        return this.sysUserRepository.findByName(name).size();
    }

    private void createUserPrivate1(SysUser sysUser) {
        this.sysUserRepository.save(sysUser);
        if (sysUser.getName().contains("test")) {
            throw new RuntimeException("invalid name");
        }
    }

答案是事务是生效的,因为在controller层调用createUserRight3方法,是通过代理对象调用的,在这时已经开启了事务,接下来在createUserRight3方法中的createUserPrivate1方法的调用只不过对应着线程中栈帧的压栈,事务已经在前面开启了。对应之前事务不生效的几种情况是它们的事务就根本没开启。

3. Spring事务没有回滚

上面讲了 Spring 声明式事务未生效的几种情况,下面来谈一谈事务没有回滚的几种情况,也就是说事务生效了,但是事务(对应于数据库数据)并没有回滚的情况。

@RestController
@RequestMapping("/rollbackfailed")
@RequiredArgsConstructor
public class RollbackFailedController {

    private final SysUserService sysUserService;

    @GetMapping("/wrong1")
    public void wrong1(@RequestParam String name) {
        this.sysUserService.createUserWrong1(name);
    }
}
@Service
@Slf4j
public class SysUserService {
    @Resource
    private SysUserRepository sysUserRepository;

    @Transactional
    public void createUserWrong1(String name) {
        try {
            sysUserRepository.save(new SysUser(name));
            throw new RuntimeException("error");
        } catch (Exception e) {
            log.error("create user failed", e);
        }
    }
}

我们可以看到在createUserWrong1方法中捕获了异常,但是在 catch 块处理中只是打印了错误日志,在这里事务并不会回滚。默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。
也就是说对于上面的情况,你需要在catch 块中手动的去回滚事务,或者你干脆不捕获异常

   @Transactional
    public void createUserWrong1(String name) {
        try {
            sysUserRepository.save(new SysUser(name));
            throw new RuntimeException("error");
        } catch (Exception e) {
            log.error("create user failed", e);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

上面说了,默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。下面的情况是检查时异常,这种情况下,是不会回滚事务的。

 @Transactional
    public void createUserWrong2(String name) throws IOException {
        sysUserRepository.save(new SysUser(name));
        readFile();
    }

    /**
     * 检查时异常
     * @throws IOException
     */
    private void readFile() throws IOException {
        Files.readAllLines(Paths.get("file-that-not-exist"));
    }

想要遇到所有的 Exception 都回滚事务,需要在 @Transactional注解中添加属性

 @Transactional(rollbackFor = Exception.class)
    public void createUserWrong2(String name) throws IOException {
        sysUserRepository.save(new SysUser(name));
        readFile();
    }

4. 总结

针对 Spring 声明式事务生效,需要保证:

  • @Transactional生效的原则之一就是,只有定义在public方法上的 @Transactional 注解才能生效。
  • 要使 @Transactional生效的原则之二就是,必须通过代理类从外部调用目标方法才能生效。

针对 Spring 声明式事务回滚,需要注意:

  • 默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。
  • 要使检查时异常也回滚,考虑设置@Transactional属性
posted @ 2023-05-21 22:09  大阿张  阅读(81)  评论(0编辑  收藏  举报