echo

任生命穿梭 时间的角落

0%

JPA中@Transactional怎么用?

准备

首先新建一个 SpringBoot 项目,修改 pom.xml,增加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
</dependencies>

然后新建配置文件 application.yml,我们需要在本地有个 test 数据库。

1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/test?useSSL=true&characterEncoding=utf-8&serverTimezone=Hongkong
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true

新建实体类 User:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import javax.persistence.*;

@Entity
@Table(name = "usr")
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Long money;

public User() {
}

public User(Long money){
this.money = money;
}
}

新建启动类 DemoMain:

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement //开启事务管理
@SpringBootApplication
public class DemoMain {

public static void main(String[] args) {
SpringApplication.run(DemoMain.class, args);
}
}

启动程序,打开Navicat,可以看到 test 数据库中有一张 usr 表。

在Navicat 中插入两个User:

1
2
insert into usr(money) values(10000);
insert into usr(money) values(0);

image-20200911211455133

到这里,一切准备就绪。

实现

新建 UserRepository :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

@Transactional
@Modifying
@Query(value = "update usr set money = money - 1 where id = ?1", nativeQuery = true)
void sub(Long userId);

@Transactional
@Modifying
@Query(value = "update usr set money = money + 1 where id = ?1", nativeQuery = true)
void add(Long userId);
}

sub()方法使用户 1 账户减少一块钱,add()方法使用户 2 账户增加一块钱。

实现 UserService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

//@Transactional //重点
public void transfer() {

//用户 1 向用户 2 转一千块(每次一块)
for(int i = 0; i < 1000; i++){
userRepository.sub(1L);
}

//用户 2 每次收到 1 块
for (int i = 0; i < 1000; i++) {
userRepository.add(2L);
}
}
}

transfer()方法模拟,用户 1 向用户 2 转账 1000 次,每次 1000 块,但是用户 2 需要等待用户 1 的所有转出都完成才开始转入。

新建 UserController:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/transfer")
public String transfer(){
userService.transfer();
return "done";
}
}

验证

现在启动DemoMain,访问 http://localhost:8080/transfer ,然后立即关闭这个程序。

现在我们来看 usr 表

image-20200911212851901

发现它们账户总和资金并不是 10000 ,这是非常危险的,转账丢钱了!查看控制栏我们也能看到 Hibernate 执行的 sql 信息。

我们再试一次,先启动 DemoMain,访问 http://localhost:8080/transfer ,然后立即关闭 MySQL(模拟一次数据库故障),然后再启动 MySQL。

再看 usr 表

image-20200911213334624

钱又变少了!!

看到这里,你可能已经猜到了 @Transactional注解是用来干啥的,它就是用来保证转账的金额总和不变。

现在我们将transfer()方法上的@Transactional恢复,然后将两个账户恢复至初始状态(账户 1 中 10000块,账户 2 中 0 块,PS:这两句 SQL 会写吧。。)

我们再尝试一次服务故障,发现控制栏中依然有 Hibernate 执行的 sql 信息,打开 usr 表,账户 1 中依然有 10000块,账户 1 的所有转账都失败了!

再试一次数据库故障,账户 1 中依然有 10000 块,转账依然失败。

这种情况是符合我们的预期,宁可失败,也不能少钱。

总结

通常来说,repository 实例的 CRUD 方法是事务的,如果自己定义 SQL,需要在除 SELECT 语句的方法(INSERT、UPDATE 等)上加上@Transactional(保证事务性)和@Modifying

另一种方法就是在 service 的方法上添加@Transactional注解,现在 repository 上的@Transactional注解被忽略,永远使用的是最外层的 @Transactional 注解。注意:必须要在启动类上添加 @EnableTransactionManagement开启事务管理。

如果这个事务在中途失败了,Spring 会将该事务回滚。

由于个人水平有限,如有错误和不足请轻喷