事务使用

引入依赖:

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

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
</dependencies>

包结构如下:

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

创建数据库:

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

配置文件:

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

实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class User implements Serializable {

private Integer id;
private String username;
private Integer age;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}

创建持久接口:

1
2
3
4
5
6
7
8
@Mapper
@Repository
public interface UserMapper {

@Insert("insert into user(username,age) values(#{username},#{age})")
void save(User user);

}

创建业务接口:

1
2
3
4
5
public interface UserService {

void saveUser(User user);

}

创建业务实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;

public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}

@Transactional
@Override
public void saveUser(User user) {
userMapper.save(user);
if (!user.getUsername().contains("K")) {
throw new RuntimeException("用户名不含有K");
}
}
}

创建启动类:

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

创建测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootTest
class KHighnessApplicationTest {

@Resource
private UserService userService;

private final User user1 = new User();
private final User user2 = new User();

@BeforeEach
public void before() {
user1.setUsername("KHighness");
user2.setUsername("Flower");
}

@Test
public void test1() {
userService.saveUser(user1);
userService.saveUser(user2);
}
}

测试结果如下:


说明user1插入,user2回滚。

事务原理

@EnableTransactionManagement

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

boolean proxyTargetClass() default false;

AdviceMode mode() default AdviceMode.PROXY;

int order() default Ordered.LOWEST_PRECEDENCE;

}

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


AutoRegistrar

查看AutoProxyRegistrar的源码:


查看AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry)源码:


查看InfrastructureAdvisorAutoProxyCreator的层级关系图:


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

ProxyTransactionManagementConfiguration


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

查看AnnotationTransactionAttributeSource源码:


查看SpringTransactionAnnotationParser源码:


  1. 注册TransactionInterceptor事务拦截器

查看TransactionInterceptor源码,其实现了MethodInterceptor方法拦截器接口,目标方法执行的时候,对应拦截器的invoke方法会被执行,所以重点关注TransactionInterceptor实现的invoke方法:


查看invokeWithinTransaction方法源码:


查看completeTransactionAfterThrowing方法源码:


这里,如果没有在@Transaction注解上指定回滚的异常类型的话,默认只对RuntimeExceptionError类型的异常进行回滚:


再看commitTransactionAfterReturning方法源码:


DEBUG验证

在测试代码上打上断点:


以DEBUG方式运行test2:


可以看到目标对象已经被JDK代理(因为UserServiceImpl实现了接口,所以采用JDK动态代理)。
在断点处执行Step Into,程序跳转到JdkDynamicAopProxyinvoke方法:


invocation.proceed()处继续Step Into,查看内部调用过程:


点击Step Into,程序跳转到TransactionInterceptorinvoke方法:


继续Step Into,程序跳转到TransactionAspectSupportinvokeWithinTransaction方法:


不生效场景

场景一

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

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

解决

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

场景二

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;

public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}

@Transactional
@Override
public void saveUser(User user) {
userMapper.save(user);
if (!user.getUsername().contains("K")) {
throw new RuntimeException("用户名不含有K");
}
}

@Override
public void saveUser2(User user) {
this.saveUser(user);
}

}

测试:

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

执行结果:


发现插入成功,说明事务不生效。

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

1
2
3
4
@log 
public void A() {}

public void B() { A(); }

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class UserServiceImpl implements UserService, ApplicationContextAware {
private final UserMapper userMapper;
private ApplicationContext applicationContext;

public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

@Transactional
@Override
public void saveUser(User user) {
userMapper.save(user);
if (!user.getUsername().contains("K")) {
throw new RuntimeException("用户名不含有K");
}
}

@Override
public void saveUser2(User user) {
UserService userService = applicationContext.getBean(UserService.class);
userService.saveUser(user);
}
}