最近在撰写论文,参考了大量文献,也在阅读博文的过程当中对架构有了新的认识,发现原文章Spring 事务管理因局限于Hibernate
框架,未对NESTED
级别的事务作详述,特写本文进行补充。java
正常的逻辑:mysql
形成了须要编写许多关于事务的冗余代码,为了解决此问题,Spring
采用声明式事务。spring
Spring Boot
的核心配置中已经默认启用了事务,使用Transactional
注解即为方法添加事务:sql
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
为事务的隔离级别,讲了好多遍了,不作赘述。segmentfault
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
传播级别。mybatis
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
。app
<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
设置为NESTED
,由于该场景下容许hardDelete
失败,hardDelete
做为子事务,让调用方决定是否回滚。
项目中采用Hibernate
,不支持NESTED
,为了规避该问题,将传播级别设置为REQUIRES_NEW
,挂起当前事务,新建事务进行回滚,不影响调用方的事务。虽然能实现功能,但理论上,仍是NESTED
更符合逻辑。
虽然有这么多隔离级别,可是REQUIRED
和SUPPORTS
已经能知足大多数的开发需求了。
数据库写INSERT/UPDATE/DELETE
使用REQUIRED
,读SELECT
使用SUPPORTS
,遇到异常,再分析使用其余事务传播级别。
任何理论都不如现实具体。——沈从文