最近在撰写论文,参考了大量文献,也在阅读博文的过程中对架构有了新的认识,发现原文章Spring 事务管理因局限于 Hibernate 框架,未对 NESTED 级别的事务做详述,特写本文进行补充。
正常的逻辑:
造成了需要编写许多关于事务的冗余代码,为了解决此问题, Spring 采用声明式事务。
Spring Boot 的核心配置中已经默认启用了事务,使用 Transactional 注解即为方法添加事务:
Spring 事务注解配置如下,比较主要的就是 isolation 和 propagation 了。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
isolation 为事务的隔离级别,讲了好多遍了,不做赘述。
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
propagation 为事务传播级别, Spring 共配置了 7 种传播类别,原文章已对前六种做过详述,本文一起来学习 Hibernate 不支持的 NESTED 传播级别。
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
因 Hibernate 不支持,故本文启用 MyBatis 进行本传播级别事务的研究。
POM 中依赖 MyBatis 、 MySQL ;为了演示方便,选用了自动化工具 mapper-spring-boot-starter 。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
实体层:
public class Cat {
private Long id;
private String name;
}
public class Dog {
private Long id;
private String name;
}
数据访问层:
public interface CoreMapper<T> extends Mapper<T>, MySqlMapper<T> {
}
@Mapper
public interface CatMapper extends CoreMapper<Cat> {
}
@Mapper
public interface DogMapper extends CoreMapper<Dog> {
}
类似于 JPA ,对于简单的数据库操作,通过继承 Mapper 和 MySqlMapper 接口,不需要写一行 SQL ,同时开启驼峰映射, XML 也不用写。
服务层两个保存方法:
@Transactional(propagation = Propagation.NESTED)
@Override
public void save(Cat cat) {
catMapper.insertUseGeneratedKeys(cat);
}
@Transactional(propagation = Propagation.NESTED)
@Override
public void save(Dog dog) {
dogMapper.insertUseGeneratedKeys(dog);
}
写个方法测试一下:
public void test() {
catService.save(new Cat("Hello Kitty"));
dogService.save(new Dog("史努比"));
}
数据保存成功,数据访问层配置没有问题。
如果当前存在事务,则在当前事务的一个嵌套事务中运行。
test 方法开始事务,调用 cat 和 dog 的保存方法, dog 的保存方法中抛出了 RuntimeException 异常。
@Transactional
public void test() {
catService.save(new Cat("Hello Kitty"));
dogService.saveAndThrowException(new Dog("史努比"));
}
执行 test 方法,两张表的数据都没有存上。不应该是两个子事务吗? dog 事务回滚,为什么 cat 也存不上呢?
原因如下, test 方法开启了事务, CatService 与 DogService 在 NESTED 的传播级别下分别建立了子事务,嵌套运行, DogService 抛出了异常,子事务回滚,不影响父事务。
但是父事务没有捕获 RuntimeException ,父事务回滚,父事务的回滚会使子事务回滚,所以 CatService 的子事务也回滚了,造成了两张表的数据都没存上。
父事务的提交和回滚会使其子事务提交或回滚。
这个层面并不是 NESTED 的全部,因为全部设置成 REQUIRED 三个方法共享一个事务也能实现相同的功能。
对上述方法加以修改,添加一个简易的异常处理,再运行。
@Transactional
public void test() {
catService.save(new Cat("Hello Kitty"));
try {
dogService.saveAndThrowException(new Dog("史努比"));
} catch (RuntimeException e) {
e.printStackTrace();
}
}
cat 存上了, dog 没存上。
子事务的提交或回滚不影响父事务的提交或回滚,这里 DogService 的子事务回滚,向上抛出的异常被处理,父事务不回滚,事务提交。
学习完特性可能还每碰到过应用场景,我有幸碰到过一次,举例如下:
将事务全部修改为默认的 REQUIRED 级别重新运行上述代码:
@Transactional
public void test() {
catService.save(new Cat("Hello Kitty"));
try {
dogService.saveAndThrowException(new Dog("史努比"));
} catch (RuntimeException e) {
e.printStackTrace();
}
}
@Transactional
@Override
public void save(Cat cat) {
catMapper.insertUseGeneratedKeys(cat);
}
@Transactional
@Override
public void saveAndThrowException(Dog dog) {
this.save(dog);
throw new RuntimeException();
}
如下图所示,两张表都没存上数据:
且控制台报错:
Transaction rolled back because it has been marked as rollback-only.
DogService 抛出了异常,将事务标记为回滚,虽然 test 方法中处理了该异常,但是事务已被标记,导致数据存储失败。
两相对比之下, NESTED 适合允许失败的场景,我遇到的就是软删除场景:
try {
hardDelete();
} catch(Exception e) {
softDelete();
}
如果配置为 REQUIRED ,事务被标记,即使处理异常,仍然回滚,数据软删除失败。此处,可以将 hardDelete 和 softDelete 设置为 NESTED ,作为子事务运行,让调用方决定是否回滚。
项目中采用 Hibernate ,不支持 NESTED ,为了规避该问题,将传播级别设置为 REQUIRES_NEW ,挂起当前事务,新建事务进行回滚,不影响调用方的事务。虽然能实现功能,但理论上,还是 NESTED 更符合逻辑。
虽然有这么多隔离级别,但是 REQUIRED 和 SUPPORTS 已经能满足大多数的开发需求了。
数据库写 INSERT/UPDATE/DELETE 使用 REQUIRED ,读 SELECT 使用 SUPPORTS ,遇到异常,再分析使用其他事务传播级别。
任何理论都不如现实具体。——沈从文