乐趣区

Spring-Boot-2x基础教程事务管理入门

什么是事务?

咱们在开发企业应用时,通常业务人员的一个操作实际上是对数据库读写的多步操作的联合。因为数据操作在程序执行的过程中,任何一步操作都有可能产生异样,异样会导致后续操作无奈实现,此时因为业务逻辑并未正确的实现,之前胜利操作的数据并不牢靠,如果要让这个业务正确的执行上来,通常有实现形式:

  1. 记录失败的地位,问题修复之后,从上一次执行失败的地位开始继续执行前面要做的业务逻辑
  2. 在执行失败的时候,回退本次执行的所有过程,让操作复原到原始状态,带问题修复之后,从新执行原来的业务逻辑

事务就是针对上述形式 2 的实现。事务,个别是指要做的或所做的事件,就是下面所说的业务人员的一个操作(比方电商零碎中,一个创立订单的操作蕴含了创立订单、商品库存的扣减两个基本操作。如果创立订单胜利,库存扣减失败,那么就会呈现商品超卖的问题,所以最根本的最发就是须要为这两个操作用事务包含起来,保障这两个操作要么都胜利,要么都失败)。

这样的场景在理论开发过程中十分多,所以明天就来一起学习一下 Spring Boot 中的事务管理如何应用!

疾速入门

在 Spring Boot 中,当咱们应用了 spring-boot-starter-jdbcspring-boot-starter-data-jpa依赖的时候,框架会主动默认别离注入 DataSourceTransactionManager 或 JpaTransactionManager。所以咱们不须要任何额定配置就能够用 @Transactional 注解进行事务的应用。

咱们以之前实现的《应用 Spring Data JPA 拜访 MySQL》的示例作为根底工程进行事务的应用学习。在该样例工程中(若对该数据拜访形式不理解,可先浏览该前文),咱们引入了 spring-data-jpa,并创立了 User 实体以及对 User 的数据拜访对象 UserRepository,在单元测试类中实现了应用 UserRepository 进行数据读写的单元测试用例,如下:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void test() throws Exception {

        // 创立 10 条记录
        userRepository.save(new User("AAA", 10));
        userRepository.save(new User("BBB", 20));
        userRepository.save(new User("CCC", 30));
        userRepository.save(new User("DDD", 40));
        userRepository.save(new User("EEE", 50));
        userRepository.save(new User("FFF", 60));
        userRepository.save(new User("GGG", 70));
        userRepository.save(new User("HHH", 80));
        userRepository.save(new User("III", 90));
        userRepository.save(new User("JJJ", 100));

        // 省略后续的一些验证操作
    }

}

能够看到,在这个单元测试用例中,应用 UserRepository 对象间断创立了 10 个 User 实体到数据库中,上面咱们人为的来制作一些异样,看看会产生什么状况。

通过 @Max(50) 来为 User 的 age 设置最大值为 50,这样通过创立时 User 实体的 age 属性超过 50 的时候就能够触发异样产生。

@Entity
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @Max(50)
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

}

执行测试用例,能够看到控制台中抛出了如下异样,对于 age 字段的谬误:

2020-07-09 11:55:29.581 ERROR 24424 --- [main] o.h.i.ExceptionMapperStandardImpl        : HHH000346: Error during managed flush [Validation failed for classes [com.didispace.chapter310.User] during persist time for groups [javax.validation.groups.Default,]
List of constraint violations:[ConstraintViolationImpl{interpolatedMessage='最大不能超过 50', propertyPath=age, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Max.message}'}
]]

此时查数据库中的 User 表:

能够看到,测试用例执行到一半之后因为异常中断了,前 5 条数据正确插入而后 5 条数据没有胜利插入,如果这 10 条数据须要全副胜利或者全副失败,那么这时候就能够应用事务来实现,做法非常简单,咱们只须要在 test 函数上增加 @Transactional 注解即可。

@Test
@Transactional
public void test() throws Exception {// 省略测试内容}

再来执行该测试用例,能够看到控制台中输入了回滚日志(Rolled back transaction for test context),

2020-07-09 12:48:23.831  INFO 24889 --- [main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@f6efaab testClass = Chapter310ApplicationTests, testInstance = com.didispace.chapter310.Chapter310ApplicationTests@60816371, testMethod = test@Chapter310ApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@3c19aaa5 testClass = Chapter310ApplicationTests, locations = '{}', classes = '{class com.didispace.chapter310.Chapter310Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@34cd072c, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@528931cf, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2353b3e6, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@7ce6a65d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@4b85edeb]; rollback [true]
2020-07-09 12:48:24.011  INFO 24889 --- [main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@f6efaab testClass = Chapter310ApplicationTests, testInstance = com.didispace.chapter310.Chapter310ApplicationTests@60816371, testMethod = test@Chapter310ApplicationTests, testException = javax.validation.ConstraintViolationException: Validation failed for classes [com.didispace.chapter310.User] during persist time for groups [javax.validation.groups.Default,]
List of constraint violations:[ConstraintViolationImpl{interpolatedMessage='最大不能超过 50', propertyPath=age, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Max.message}'}
], mergedContextConfiguration = [WebMergedContextConfiguration@3c19aaa5 testClass = Chapter310ApplicationTests, locations = '{}', classes = '{class com.didispace.chapter310.Chapter310Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@34cd072c, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@528931cf, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2353b3e6, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@7ce6a65d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]

再看数据库中,User 表就没有 AAA 到 EEE 的用户数据了,胜利实现了主动回滚。

这里次要通过单元测试演示了如何应用 @Transactional 注解来申明一个函数须要被事务管理,通常咱们单元测试为了保障每个测试之间的数据独立,会应用 @Rollback 注解让每个单元测试都能在完结时回滚。而真正在开发业务逻辑时,咱们通常在 service 层接口中应用 @Transactional 来对各个业务逻辑进行事务管理的配置,例如:

public interface UserService {
    
    @Transactional
    User update(String name, String password);
    
}

事务详解

下面的例子中咱们应用了默认的事务配置,能够满足一些根本的事务需要,然而当咱们我的项目较大较简单时(比方,有多个数据源等),这时候须要在申明事务时,指定不同的事务管理器。对于不同数据源的事务管理配置能够见《Spring Data JPA 的多数据源配置》中的设置。在申明事务时,只须要通过 value 属性指定配置的事务管理器名即可,例如:@Transactional(value="transactionManagerPrimary")

除了指定不同的事务管理器之后,还能对事务进行隔离级别和流传行为的管制,上面别离具体解释:

隔离级别

隔离级别是指若干个并发的事务之间的隔离水平,与咱们开发时候次要相干的场景包含:脏读取、反复读、幻读。

咱们能够看 org.springframework.transaction.annotation.Isolation 枚举类中定义了五个示意隔离级别的值:

public enum Isolation {DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}
  • DEFAULT:这是默认值,示意应用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是:READ_COMMITTED
  • READ_UNCOMMITTED:该隔离级别示意一个事务能够读取另一个事务批改但还没有提交的数据。该级别不能避免脏读和不可反复读,因而很少应用该隔离级别。
  • READ_COMMITTED:该隔离级别示意一个事务只能读取另一个事务曾经提交的数据。该级别能够避免脏读,这也是大多数状况下的推荐值。
  • REPEATABLE_READ:该隔离级别示意一个事务在整个过程中能够多次重复执行某个查问,并且每次返回的记录都雷同。即便在屡次查问之间有新增的数据满足该查问,这些新增的记录也会被疏忽。该级别能够避免脏读和不可反复读。
  • SERIALIZABLE:所有的事务顺次一一执行,这样事务之间就齐全不可能产生烦扰,也就是说,该级别能够避免脏读、不可反复读以及幻读。然而这将重大影响程序的性能。通常状况下也不会用到该级别。

指定办法:通过应用 isolation 属性设置,例如:

@Transactional(isolation = Isolation.DEFAULT)

流传行为

所谓事务的流传行为是指,如果在开始以后事务之前,一个事务上下文曾经存在,此时有若干选项能够指定一个事务性办法的执行行为。

咱们能够看 org.springframework.transaction.annotation.Propagation 枚举类中定义了 6 个示意流传行为的枚举值:

public enum Propagation {REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}
  • REQUIRED:如果以后存在事务,则退出该事务;如果以后没有事务,则创立一个新的事务。
  • SUPPORTS:如果以后存在事务,则退出该事务;如果以后没有事务,则以非事务的形式持续运行。
  • MANDATORY:如果以后存在事务,则退出该事务;如果以后没有事务,则抛出异样。
  • REQUIRES_NEW:创立一个新的事务,如果以后存在事务,则把以后事务挂起。
  • NOT_SUPPORTED:以非事务形式运行,如果以后存在事务,则把以后事务挂起。
  • NEVER:以非事务形式运行,如果以后存在事务,则抛出异样。
  • NESTED:如果以后存在事务,则创立一个事务作为以后事务的嵌套事务来运行;如果以后没有事务,则该取值等价于REQUIRED

指定办法:通过应用 propagation 属性设置,例如:

@Transactional(propagation = Propagation.REQUIRED)

代码示例

本文的相干例子能够查看上面仓库中的 chapter3-10 目录:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/
  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

如果您感觉本文不错,欢送 Star 反对,您的关注是我保持的能源!

本文首发:Spring Boot 2.x 基础教程:事务管理入门,转载请注明出处。
欢送关注我的公众号:程序猿 DD,取得独家整顿的学习资源和日常干货推送。
如果您对我的其余专题内容感兴趣,中转我的集体博客:didispace.com。

退出移动版