背景
本篇博文将会讲一讲Spring中使用@Transactional注解会出现的不生效问题。事务的生效与否,一般不是我们冒烟自测的范围,测试也不会去测,但是一旦上线后,事务出现不生效的情况,就可能引发较大的问题,甚至会带来损失。所以,使用好事务注解是非常重要的,尤其是注意哪些场景下会出现事务失效。
事务失效效常见情况分析
- 1、Transactional注解必须修饰在public方法上面,如果不是public方法,则事务不会生效。
原理说明
因为声明式事务是通过SpringAOP代理来实现的,当在类中声明一个事务方法时,Spring会创建一个代理类,以便在调用该类的方法时应用事务管理。
SpringAOP的实现有JDK动态代理和CGLIB代理,对于实现了接口的方法使用JDK动态代理,那些没有实现接口的方法,则使用CGLIB代理。但是不管哪种代理,都必须保证目标方法是public方法;
因为Spring AOP在运行时会创建代理对象来拦截目标方法的调用,并在代理对象中添加事务管理的逻辑;如果不是public,那么代理对象就无法直接调用目标方法。
举例说明
package com.example.demo3.commonpitfalls;
import com.example.demo3.commonpitfalls.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("transactional")
public class TransactionalPit {@Autowiredprivate UserService userService;@GetMapping("wrong")public int wrong1(@RequestParam("name") String name) {return userService.createUserWrong1(name);}
}package com.example.demo3.commonpitfalls.service;import com.example.demo3.commonpitfalls.dto.UserEntity;
import com.example.demo3.commonpitfalls.dto.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.transaction.Transactional;@Service
@Slf4j
public class UserService {@Autowiredprivate UserRepository userRepository;// 一个公共方法供Controller调用,内部调用事务性的私有方法public int createUserWrong1(String name) {try {this.createUserPrivate(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());}return userRepository.findByName(name).size();}//标记了@Transactional的private方法@Transactionalprivate void createUserPrivate(UserEntity entity) {userRepository.save(entity);if (entity.getName().contains("test"))throw new RuntimeException("invalid username!");}
}
package com.example.demo3.commonpitfalls.dto;import lombok.Data;import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;import static javax.persistence.GenerationType.AUTO;
import static javax.persistence.GenerationType.IDENTITY;@Entity
@Data
@Table(name = "user_entity")
public class UserEntity {@Id@GeneratedValue(strategy = IDENTITY)private Long id;private String name;public UserEntity() {}public UserEntity(String name) {this.name = name;}
}
package com.example.demo3.commonpitfalls.dto;import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;import java.util.List;@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {List<UserEntity> findByName(String name);
}
当我们传入一个name ,按照代码中的逻辑,会抛出一个异常,然后会期望触发事务的回滚,但我们的执行结果却是数据库成功保存了这个name。那就说明事务并未生效。
仔细看我们的事务注解,即没有指定回滚异常,而且还用在了private方法上面。Spring AOP代理对象无法访问private的方法,从而导致事务没有生效。
修改为方法修饰符为public
controller增加一个方法@GetMapping("wrong2")public int wrong2(@RequestParam("name") String name) {return userService.createUserWrong2(name);}
service中增加两个方法// 一个公共方法供Controller调用,内部调用事务性的公有方法public int createUserWrong2(String name) {try {this.createUserPublic(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());}return userRepository.findByName(name).size();}//标记了@Transactional的public方法@Transactionalpublic void createUserPublic(UserEntity entity) {userRepository.save(entity);if (entity.getName().contains("test"))throw new RuntimeException("invalid username!");}
从结果发现,这次非法name又保存成功了,说明事务依然没有生效。仔细观察我们的调用方式,使用了this, 在一个方法的内部自调用了另一个带有事务注解的方法。那么,这里给出事务的第二个失效情况。
2、Transactional注解必须通过代理过的类从外部调用目标方法才能生效, 否则会事务失效。
这是因为 Spring 使用代理来实现事务,自调用会绕过代理,导致事务不生效!
修改代码
service
首先注入本类的service@AutowiredUserService userService;
然后,使用userService去调用。
// 一个公共方法供Controller调用,外部调用事务性的公有方法public int createUserWrong2(String name) {try {userService.createUserPublic(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());}return userRepository.findByName(name).size();}
经过调试发现,userService是由SpringCglib增强过的类,故访问的方法也是代理后的方法,具有事务的特性。
再次测试,控制台抛出了异常,数据库已经无法保存这个非法的name了。
说明事务生效了,之前执行过的save, 也在后面有执行了回滚。
这边说的外部调用,也可以是直接从另外一个类调用本来UserService类的方法!
3、事务生效后,如何捕获异常,并保证一定回滚。
有些错误的理解,认为只要有异常,事务一定会回滚,实则不然。
一般,我们写代码的时候,都这样定义事务,即@Transactional(rollbackFor = Exception.class),这个含义指的是只要方法中遇到异常,那么就执行回滚。
因为默认情况下,出现RuntimeException(非受检异常)或Error的时候,Spring才会回滚事务。如果是受检异常,Spring认为受检异常一般是业务异常,或者说
是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而 Error或RuntimeException 代表了非预期的结果,应该回滚。
public class UserService {@Autowiredprivate UserRepository userRepository;@AutowiredUserService userService;// 异常无法传播出方法,导致事务无法回滚@Transactionalpublic void createUserWrong1(String name) {try {userRepository.save(new UserEntity(name));throw new RuntimeException("error");} catch (Exception ex) {log.error("create user failed", ex);}}@Transactionalpublic void createUserWrong2(String name) throws IOException {userRepository.save(new UserEntity(name));otherTask();}//因为文件不存在,一定会抛出一个 IOExceptionprivate void otherTask() throws IOException {Files.readAllLines(Paths.get("file-that-not-exist"));}
以上代码中的两种情况, 事务都不会回滚。第一种情况是异常没有传播出方法,是由于方法内catch了所有异常,所以异常RuntimeException无法从方法传播出去,事务自然无法回滚;第二种情况是属于受检异常,createUserWrong2能将这个受检异常传播出去,但事务看到了这种异常,默认不会进行回滚。
针对第一种情况的解决方式可以是,我们手动在catch中让事务执行回滚。运行后,debug日志中,我们会看到Transactional code has requested rollback, 这个就表示是手动回滚。
针对第二种情况,既然不能回滚受检异常,那么我们就改变这个模式,让事务遇到不再区分受检还是不受检,只要是异常,那么就进行回滚。
@Transactional(rollbackOn = Exception.class)public void createUserWrong2(String name) throws IOException {userRepository.save(new UserEntity(name));otherTask();}
改完以后,会发现可以正常回滚。一般,建议用这个方式。@Transactional(rollbackOn = Exception.class)
总结
这个例子中,我们展现的是一个复杂的业务逻辑,其中有数据库操作、IO 操作,在 IO 操作出现问题时,希望让数据库事务也回滚,以确保逻辑的一致性。在有些业务逻辑中,可能
会包含多次数据库操作,我们不一定希望将两次操作作为一个事务来处理,这时候就需要仔细考虑事务传播的配置了,否则也可能踩坑。
3、事务传播配置是否符合自己的业务逻辑
有时,事务传播级别没有设置正确,业务逻辑和我们预期的不一致,会在排查问题的时候,一下子不容易看出来是什么地方出现的错误。