事务使用

引入依赖:

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>2.4.0</version>
  5. </parent>
  6. <dependencies>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter</artifactId>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter-test</artifactId>
  14. </dependency>
  15. <dependency>
  16. <groupId>mysql</groupId>
  17. <artifactId>mysql-connector-java</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.mybatis.spring.boot</groupId>
  21. <artifactId>mybatis-spring-boot-starter</artifactId>
  22. <version>2.1.2</version>
  23. </dependency>
  24. </dependencies>

包结构如下:

  1. top
  2. └─parak
  3. ├─entity
  4. └─User
  5. ├─mapper
  6. └─UserMapper
  7. ├─service
  8. └─UserService
  9. └─impl
  10. └─UserServiceImpl
  11. └─KHighnessApplication

创建数据库:

  1. CREATE TABLE `user` (
  2. `id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  3. `username` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户名',
  4. `age` int DEFAULT NULL COMMENT '用户年龄',
  5. PRIMARY KEY (`id`) USING BTREE
  6. ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

配置文件:

  1. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  2. spring.datasource.username=root
  3. spring.datasource.password=KAG1823
  4. spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8
  5. logging.level.top.parak.mapper=debug

实体类:

  1. public class User implements Serializable {
  2. private Integer id;
  3. private String username;
  4. private Integer age;
  5. public Integer getId() {
  6. return id;
  7. }
  8. public void setId(Integer id) {
  9. this.id = id;
  10. }
  11. public String getUsername() {
  12. return username;
  13. }
  14. public void setUsername(String username) {
  15. this.username = username;
  16. }
  17. public Integer getAge() {
  18. return age;
  19. }
  20. public void setAge(Integer age) {
  21. this.age = age;
  22. }
  23. }

创建持久接口:

  1. @Mapper
  2. @Repository
  3. public interface UserMapper {
  4. @Insert("insert into user(username,age) values(#{username},#{age})")
  5. void save(User user);
  6. }

创建业务接口:

  1. public interface UserService {
  2. void saveUser(User user);
  3. }

创建业务实现类:

  1. @Service
  2. public class UserServiceImpl implements UserService {
  3. private final UserMapper userMapper;
  4. public UserServiceImpl(UserMapper userMapper) {
  5. this.userMapper = userMapper;
  6. }
  7. @Transactional
  8. @Override
  9. public void saveUser(User user) {
  10. userMapper.save(user);
  11. if (!user.getUsername().contains("K")) {
  12. throw new RuntimeException("用户名不含有K");
  13. }
  14. }
  15. }

创建启动类:

  1. @SpringBootApplication
  2. @EnableTransactionManagement
  3. public class KHighnessApplication {
  4. public static void main(String[] args) {
  5. new SpringApplicationBuilder(KHighnessApplication.class)
  6. .web(WebApplicationType.SERVLET).run(args);
  7. }
  8. }

创建测试:

  1. @SpringBootTest
  2. class KHighnessApplicationTest {
  3. @Resource
  4. private UserService userService;
  5. private final User user1 = new User();
  6. private final User user2 = new User();
  7. @BeforeEach
  8. public void before() {
  9. user1.setUsername("KHighness");
  10. user2.setUsername("Flower");
  11. }
  12. @Test
  13. public void test1() {
  14. userService.saveUser(user1);
  15. userService.saveUser(user2);
  16. }
  17. }

测试结果如下:
image.png
说明user1插入,user2回滚。

事务原理

@EnableTransactionManagement

直接查看模块驱动注解@EnableTransactionManagement 的源码:

  1. @Target(ElementType.TYPE)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. @Import(TransactionManagementConfigurationSelector.class)
  5. public @interface EnableTransactionManagement {
  6. boolean proxyTargetClass() default false;
  7. AdviceMode mode() default AdviceMode.PROXY;
  8. int order() default Ordered.LOWEST_PRECEDENCE;
  9. }

可以看到这个注解通过@Import向容器中导入了一个TransactionManagementConfigurationSelector组件,查看其源码:
image.png

AutoProxyRegistrar

查看AutoProxyRegistrar的源码:
image.png
查看AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry)源码:
image.png
查看InfrastructureAdvisorAutoProxyCreator的层级关系图:

image.png
这个和AOP中的AnnotationAwareAspectJAutoProxyCreator的层级关系图一致,所以我们可以推断出InfrastructureAdvisorAutoProxyCreator的作用为:为目标Service创建代理对象,增强目标Service方法,用于事务控制。

ProxyTransactionManagementConfiguration

image.png

  1. 注册BeanFactoryTransactionAttributeSourceAdvisor增强器,该增强器需要如下两个Bean
    • TransactionAttributeSource
    • TransactionInterceptor
  2. 注册TransactionAttributeSource

查看AnnotationTransactionAttributeSource源码:
image.png
查看SpringTransactionAnnotationParser源码:
image.png

  1. 注册TransactionInterceptor事务拦截器

查看TransactionInterceptor源码,其实现了MethodInterceptor方法拦截器接口,目标方法执行的时候,对应拦截器的invoke方法会被执行,所以重点关注TransactionInterceptor实现的invoke方法:
image.png
查看invokeWithinTransaction方法源码:
image (1).png
查看completeTransactionAfterThrowing方法源码:
image.png
这里,如果没有在@Transaction注解上指定回滚的异常类型的话,默认只对RuntimeExceptionError类型的异常进行回滚:
image.png
再看commitTransactionAfterReturning方法源码:
image.png

DEBUG验证

在测试代码上打上断点:
image.png
以DEBUG方式运行test2:
image.png
可以看到目标对象已经被JDK代理(因为UserServiceImpl实现了接口,所以采用JDK动态代理)。
在断点处执行Step Into,程序跳转到JdkDynamicAopProxyinvoke方法:
image.png
invocation.proceed()处继续Step Into,查看内部调用过程:
image.png
点击Step Into,程序跳转到TransactionInterceptorinvoke方法:
image.png
继续Step Into,程序跳转到TransactionAspectSupportinvokeWithinTransaction方法:
image.png

不生效场景

场景一

Service方法抛出的异常不是RuntimeException或者Error类型,并且@Transactional注解上没有指定回滚异常类型。

原因
默认情况下,Spring事务只对RuntimeException或者Error类型异常进行回滚,检查异常(通常为业务类异常)不会导致事务回滚。

解决

  1. 手动在@Transactional注解上声明回滚的异常类型rollbackFor(方法抛出该异常及其所有子类型异常都能出发事务回滚):
  2. 将方法内抛出的异常继承RuntimeException。

场景二

非事务方法直接通过this调用本类事务方法。
比如,修改UserServiceImpl如下:

  1. @Service
  2. public class UserServiceImpl implements UserService {
  3. private final UserMapper userMapper;
  4. public UserServiceImpl(UserMapper userMapper) {
  5. this.userMapper = userMapper;
  6. }
  7. @Transactional
  8. @Override
  9. public void saveUser(User user) {
  10. userMapper.save(user);
  11. if (!user.getUsername().contains("K")) {
  12. throw new RuntimeException("用户名不含有K");
  13. }
  14. }
  15. @Override
  16. public void saveUser2(User user) {
  17. this.saveUser(user);
  18. }
  19. }

测试:

  1. @Test
  2. public void test2() {
  3. userService.saveUser2(user2);
  4. }

执行结果:
image.png
发现插入成功,说明事务不生效。

原因
这让我想起面试字节时一个AOP的面试题:

  1. @log
  2. public void A() {}
  3. public void B() { A(); }

问直接通过this.B()调用A()方法能否打印日志。答案当然是不能的。
AOP的实现是通过目标类的JDK动态代理类或者CGlib动态代理类来实现的,直接调用方法调用的是目标类的方法而不是代理类的增强后方法,因此不能打印出日志。
Spring事务控制使用AOP代理实现,通过对目标对象的代理来增强目标方法,直接通过this调用本类的方法的时候,this的指向并非代理类,而是目标类本身。

解决
从IOC容器中获取UserService,然后调用saveUser方法:

  1. @Service
  2. public class UserServiceImpl implements UserService, ApplicationContextAware {
  3. private final UserMapper userMapper;
  4. private ApplicationContext applicationContext;
  5. public UserServiceImpl(UserMapper userMapper) {
  6. this.userMapper = userMapper;
  7. }
  8. @Override
  9. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  10. this.applicationContext = applicationContext;
  11. }
  12. @Transactional
  13. @Override
  14. public void saveUser(User user) {
  15. userMapper.save(user);
  16. if (!user.getUsername().contains("K")) {
  17. throw new RuntimeException("用户名不含有K");
  18. }
  19. }
  20. @Override
  21. public void saveUser2(User user) {
  22. UserService userService = applicationContext.getBean(UserService.class);
  23. userService.saveUser(user);
  24. }
  25. }

最终总结

事务原理.png

模块驱动注解@EnableTransactionManagement通过@Import导入了一个@TransactionManagementConfigurationSelector组件,它通过selectImports方法批量注册两个类:

  1. 注册器:AutoProxyRegistrar
  2. 配置类:ProxyTransactionManagementConfiguration

(1)AutoProxyRegistrar
通过AopConfigUtils向容器中注册InfrastructureAdvisorAutoProxyCreator,作用是为目标Service创建代理对象,增强Service方法,用于事务控制。

(2)ProxyTransactionManagementConfiguration
主要向容器中注册两个bean:
BeanFactoryTransactionAttributeSourceAdvisor
TransactionInterceptor
①创建SpringTransactionAnnotationParser,解析@Transaction注解上的各个属性值,包装为Transaction对象
② 实现了MethodInterceptor方法拦截器,在invoke方法中先处理和Kotlin相关内容,再执行invokeWithTransaction方法。
这个方法是事务核心,先获取目标方法的@Transaction注解的属性,再获取事务管理器JdbcTransactionManager,在一个try-catch块中通过反射执行目标方法proceedWithInvocation

  • 出现异常则执行completeTransactionThrowing方法:需要判断方法抛出的异常是否在指定的范围内,是则通过事务管理器回滚事务,否则提交事务。
  • 成功则执行commitTransactionAfterReturning方法:通过事务管理器提交事务。