Spring 事务管理简介
概述
事务原本是数据库中的概念,用于数据访问层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。
在 Spring 中通常可以通过以下三种方式来实现对事务的管理:
- 使用 Spring 的事务代理工厂管理事务(已过时)
- 使用 Spring 的事务注解管理事务
- 使用 AspectJ 的 AOP 配置管理事务
Spring 事务管理 API
Spring 的事务管理,主要用到两个事务相关的接口。
事务管理器接口
事务管理器是 PlatformTransactionManager
接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。
该接口定义了 3 个事务方法:
void commit(TransactionStatus status)
:事务的提交TransactionStatus getTransaction(TransactionDefinition definition)
:获取事务的状态void rollback(TranscationStatus status)
:事务的回滚
常用的两个实现类
PlatformTransactionManager
接口有两个常用的实现类:
DataSourceTransactionManager
:使用 JDBC 或 MyBatis 进行持久化数据时使用。HibernateTransactionManager
:使用 Hibernate 进行持久化数据时使用。
Spring 的回滚方式
Spring 事务的默认回滚方式是:发生运行时异常回滚 所有不要使用try{}cath(){}捕获 否则事务无效
事务定义接口
事务定义接口 TransactionDefinition
中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作。
事务的四种隔离级别
- DEFAULT:采用 DB 默认的事务隔离级别。MySql 默认为 REPEATABLE_READ;Oracle 默认为:READ_COMMITTED;
- READ_UNCOMMITTED:读未提交。未解决任何并发问题。
- READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
- REPEATABLE_READ:可重复读。解决脏读、不可重复读。存在幻读。
- SERIALIZABLE:串行化。不存在并发问题。
事务的七种传播行为
所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 a() 调用 B 事务中的方法 b(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。
- REQUIRED:指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
- SUPPORTS:指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
- MANDATORY:指定的方法必须在当前事务内执行,若当前没有事务,则直接抛出异常。
- REQUIRES_NEW:总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
- NOT_SUPPORTED:指定的方法不能在事务环境中执行,若当前存在事务,就将当前事务挂起。
- NEVER:指定的方法不能在事务环境下执行,若当前存在事务,就直接抛出异常。
- NESTED:指定的方法必须在事务内执行。若当前存在事务,则在嵌套事务内执行;若当前没有事务,则创建一个新事务。
Propagation.NESTED 修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务。
注意:这7种传播行为有个前提,他们的事务管理器是同一个的时候,才会有上面描述中的表现行为。
REQUIRED,REQUIRES_NEW,NESTED这几个算是比较特殊的,比较常用的,对比
1、REQUIRED和NESTED修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是REQUIRED是加入外围方法事务,所以和外围事务同属于一个事务,一旦REQUIRED事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED是外围方法的子事务,有单独的保存点,所以NESTED方法抛出异常被回滚,不会影响到外围方法的事务。
2、REQUIRES_NEW和NESTED都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。
针对事务传播类型,我们要弄明白的是 4 个点:
- 子事务与父事务的关系,是否会启动一个新的事务?
- 子事务异常时,父事务是否会回滚?
- 父事务异常时,子事务是否会回滚?
- 父事务捕捉异常后,父事务是否还会回滚?
准备
表结构
tb_order
tb_user
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
配置类
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/study_db?characterEncoding=UTF-8&serverTimezone=GMT%2B8");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
实体类
@Data
@Builder
public class Order {
private Long id;
private Long userId;
private String name;
private BigDecimal price;
private Integer num;
}
@Data
@Builder
public class User {
private Long id;
private String username;
private String address;
}
mapper
@Repository
public class OrderMapper {
@Autowired
private JdbcTemplate jdbcTemplate;
public void saveOrder(Order order) {
jdbcTemplate.update("insert into tb_order(user_id, name,price,num) VALUES (?,?,?,?)",
order.getUserId(),
order.getName(),
order.getPrice(),
order.getNum());
}
}
@Repository
public class UserMapper {
@Autowired
private JdbcTemplate jdbcTemplate;
public void saveUser(User user) {
jdbcTemplate.update("insert into tb_user(username, address) VALUES (?,?)", user.getUsername(), user.getAddress());
}
}
Service
1、UserService
public interface UserService {
void saveUser();
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
throw new RuntimeException();
}
}
2、OrderService
public interface OrderService {
void saveOrder();
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserService userService;
@Override
public void saveOrder() {
// 保存订单信息
save();
// int i = 1/0;
// 保存用户信息
userService.saveUser();
}
public void save() {
orderMapper.saveOrder(Order.builder().userId(100L)
.price(BigDecimal.valueOf(100)).name("Iphone14 pro max").num(2).build());
}
}
Controller
@RestController
@RequestMapping("/api")
public class TxController {
@Autowired
OrderService orderService;
@GetMapping("/test")
public String test1() {
orderService.saveOrder();
return "success";
}
}
运行项目。浏览器访问地址:localhost:8080/api/tx,正常的话应该是接口请求成功。
tb_order、tb_user添加一条数据,测试 Spring 事务传播行为的 Demo 就准备完毕了
。
快速入门
无事务控制: OrderServiceImpl 中添加除0异常, 访问接口localhost:8080/api/tx
,tb_order插入成功,tb_user插入失败。这种在业务中是不允许出现的。
Spring声明式事务控制: 方法上添加 @Transactional
注解控制,tb_order、tb_user都没有插入数据。这说明事务起作用了
REQUIRED
REQUIRED 是 Spring 默认的事务传播类型,该传播类型的特点是:当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。
为了验证 REQUIRED 事务传播类型的特点,我们来做几个测试。
还是上面 saveOrder和 saveUser的例子。当 saveOrder不开启事务,saveUser开启事务,这时候 saveUser就是独立的事务,而 saveOrder并不在事务之中。因此当 saveUser发生异常回滚时,saveOrder 中的内容就不会被回滚。用如下的代码就可以验证我们所说的。
public void saveOrder() {
// 保存订单信息
save();
// 保存用户信息
userService.saveUser();
}
@Override
@Transactional
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
throw new RuntimeException();
}
最终的结果是:tb_order插入了数据,tb_user没有插入数据,符合了我们的猜想。
当 saveOrder开启事务,saveUser也开启事务。按照我们的结论,此时 saveUser会加入 saveOrder的事务。此时,我们验证当父子事务分别回滚时,另外一个事务是否会回滚。
我们先验证第一个:当父方法事务回滚时,子方法事务是否会回滚?
@Transactional
public void saveOrder() {
// 保存订单信息
save();
// 保存用户信息
userService.saveUser();
throw new RuntimeException();
}
@Transactional
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
}
结果是:tb_order和 tb_user都没有插入数据,即:父事务回滚时,子事务也回滚了。
我们继续验证第二个:当子方法事务回滚时,父方法事务是否会回滚?
@Transactional
public void saveOrder() {
// 保存订单信息
save();
// 保存用户信息
userService.saveUser();
}
@Transactional
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
throw new RuntimeException();
}
结果是:tb_order和 tb_user都没有插入数据,即:子事务回滚时,父事务也回滚了。
我们继续验证第三个:当子方法事务回滚时,父方法捕捉了异常,父方法事务是否会回滚?
@Transactional
public void saveOrder() {
// 保存订单信息
save();
try {
// 保存用户信息
userService.saveUser();
} catch (Exception e) {
System.out.println("saveUser发生异常exp....");
}
System.out.println("执行后续代码.......");
}
@Transactional
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
throw new RuntimeException();
}
结果是:tb_order和 tb_user都没有插入数据,即:子事务回滚时,父事务也回滚了。所以说,这也进一步验证了我们之前所说的:REQUIRED 传播类型,它是父子方法共用同一个事务的。
原因: 这种错一般发生在嵌套事务中,即内层事务saveUser()
出错,但是由于是否提交事务的操作由外层事务saveOrder()
触发,于是乎内层事务只能做个标记,来设置当前事务只能回滚。紧接着它想抛出错误,但是由于被 try catch 了,于是乎正常执行后续的逻辑,等执行到最后,外层要提交事务了,发现当前事务已经被打了回滚的标记,所以提交失败,报了上面的错。
REQUIRES_NEW
REQUIRES_NEW 也是常用的一个传播类型,该传播类型的特点是:无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。
为了验证 REQUIRES_NEW 事务传播类型的特点,我们来做几个测试。
首先,我们来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?
@Transactional
public void saveOrder() {
// 保存订单信息
save();
// 保存用户信息
userService.saveUser();
throw new RuntimeException();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
}
结果是:tb_order没有插入数据,tb_user插入了数据,即:父方法事务回滚了,但子方法事务没回滚。这可以证明父子方法的事务是独立的,不相互影响。
下面,我们来看看:当子方法事务发生异常时,父方法事务是否会回滚?
@Transactional
public void saveOrder() {
// 保存订单信息
save();
// 保存用户信息
userService.saveUser();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
throw new RuntimeException();
}
结果是:tb_order没有插入了数据,tb_user没有插入数据。
从这个结果来看,貌似是子方法事务回滚,导致父方法事务也回滚了。但我们不是说父子事务都是独立的,不会相互影响么?怎么结果与此相反呢?
其实是因为子方法抛出了异常,而父方法并没有做异常捕捉,此时父方法同时也抛出异常了,于是 Spring 就会将父方法事务也回滚了。如果我们在父方法中捕捉异常,那么父方法的事务就不会回滚了,修改之后的代码如下所示。
@Transactional
public void saveOrder() {
// 保存订单信息
save();
// 捕捉异常
try {
// 保存用户信息
userService.saveUser();
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
throw new RuntimeException();
}
结果是:tb_order插入了数据,tb_user没有插入数据。这正符合我们刚刚所说的:父子事务是独立的,并不会相互影响。
这其实就是我们上面所说的:父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。因为如果执行过程中发生 RuntimeException 异常和 Error 的话,那么 Spring 事务是会自动回滚的。
NESTED
NESTED 也是常用的一个传播类型,该方法的特性与 REQUIRED 非常相似,其特性是:当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。
可以看到 NESTED 与 REQUIRED 的区别在于:父方法与子方法对于共用事务的描述是不一样的,REQUIRED 说的是共用同一个事务,而 NESTED 说的是在嵌套事务执行。这一个区别的具体体现是:在子方法事务发生异常回滚时,父方法有着不同的反应动作。
对于 REQUIRED 来说,无论父子方法哪个发生异常,全都会回滚。而 REQUIRED 则是:父方法发生异常回滚时,子方法事务会回滚。而子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。
为了验证 NESTED 事务传播类型的特点,我们来做几个测试。
首先,我们来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?
@Transactional
public void saveOrder() {
// 保存订单信息
save();
// 保存用户信息
userService.saveUser();
throw new RuntimeException();
}
@Transactional(propagation = Propagation.NESTED)
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
}
结果是:tb_order和 tb_user都没有插入数据,即:父子方法事务都回滚了。这说明父方法发送异常时,子方法事务会回滚。
接着,我们继续验证一下:当子方法事务发生异常时,如果父方法没有捕捉异常,父方法事务是否会回滚?
@Transactional
public void saveOrder() {
// 保存订单信息
save();
// 保存用户信息
userService.saveUser();
}
@Transactional(propagation = Propagation.NESTED)
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
throw new RuntimeException();
}
结果是:tb_order和 tb_user都没有插入数据,即:父子方法事务都回滚了。这说明子方法发送异常回滚时,如果父方法没有捕捉异常,那么父方法事务也会回滚。
最后,我们验证一下:当子方法事务发生异常时,如果父方法捕捉了异常,父方法事务是否会回滚?
@Transactional
public void saveOrder() {
// 保存订单信息
save();
// 捕捉异常
try {
// 保存用户信息
userService.saveUser();
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.NESTED)
public void saveUser() {
userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
throw new RuntimeException();
}
结果是:tb_order插入了数据,tb_user没有插入数据,即:父方法事务没有回滚,子方法事务回滚了。这说明子方法发送异常回滚时,如果父方法捕捉了异常,那么父方法事务就不会回滚。
看到这里,相信大家已经对 REQUIRED、REQUIRES_NEW 和 NESTED 这三个传播类型有了深入的理解了。最后,让我们来总结一下:
事务传播类型 | 特性 |
---|---|
REQUIRED | 当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。 |
REQUIRES_NEW | 无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。 |
NESTED | 当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。 |
使用方法论
看到这里,你应该也明白:使用事务,不再是简单地使用 @Transaction
注解就可以,还需要根据业务场景,选择合适的传播类型。那么我们再升华一下使用 Spring 事务的方法论。一般来说,使用 Spring 事务的步骤为:
1、根据业务场景,分析要达成的事务效果,确定使用的事务传播类型。
2、在 Service 层使用 @Transaction 注解,配置对应的 propogation 属性。