准备
首先新建一个 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/> </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> <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);
|

到这里,一切准备就绪。
实现
新建 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;
public void transfer() {
for(int i = 0; i < 1000; i++){ userRepository.sub(1L); }
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 表

发现它们账户总和资金并不是 10000 ,这是非常危险的,转账丢钱了!查看控制栏我们也能看到 Hibernate 执行的 sql 信息。
我们再试一次,先启动 DemoMain,访问 http://localhost:8080/transfer ,然后立即关闭 MySQL(模拟一次数据库故障),然后再启动 MySQL。
再看 usr 表

钱又变少了!!
看到这里,你可能已经猜到了 @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 会将该事务回滚。
由于个人水平有限,如有错误和不足请轻喷。