如何正确处理 Spring 声明式事务
共 5906字,需浏览 12分钟
·
2023-07-14 13:43
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);
}
}
测试接口可以发现,程序报异常了,但是数据库已经却成功的插入了记录,事务并未生效!!!
其实在上面已经看出来了,idea会在当你使用@Transactional 标记 private 修饰的方法时报红。 @Transactional生效的原则之一就是,只有定义在public方法上的 @Transactional 注解才能生效。这是因为Spring 默认使用动态代理实现AOP,对目标方法进行增强,private 修饰的方法是无法被代理到的。 那如果说,我把上面的 createUserPrivate 方法改为 public 修饰,那么事务是否会生效呢?
答案是否定的,事务是依然不会生效的。要使 @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();
}
可以看到此时 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属性