低调人的gravatar头像
低调人 2017-12-28 16:05:12
Spring Boot之Spring Boot开启声明式事务

什么是事务?它有什么好处特点!

为了完成对数据的操作,企业应用经常要求并发访问在多个构件之间共享的数据。这些应用在下列条件下应该维护数据的完整性(由应用的商务规则来定义): 

 

分布式访问一个单独的数据资源,以及从一个单独的应用构件访问分布式资源。 

在这种情况,可能要求在(分布式)资源上的一组操作被当作一个工作单元(unit)。在一个工作单元中, 操作的所有部分一起成功或失败并恢复。在下面的情况下这个问题更加复杂: 

 

通过一组分布式的、访问多个资源的数据的构件实现一个工作单元,和/或部分操作是被顺序执行的或在要求协调和/或同步的并行线程中。 

 

在所有情况下, 都要求应用维护一个工作单元的成功或失败。在失败的情况下,所有资源要把数据状态返回到以前的状态 

(比如说,工作单元开始前的状态)。 

事务的概念和和事务管理器(或者一个事务处理服务)在一个工作单元中的维护数据完整性,这就简化了这样的企业级别分布式应用的构造。 

 

一个事务是有下列属性的一个工作单元: 

 

原子性(ATOMICITY): 

一个事务要被完全的无二义性的做完或撤消。在任何操作出现一个错误的情况下,构成事务的所有操作的效果必须被撤消,数据应被回滚到以前的状态。 

 

一致性(CONSISTENCY): 

一个事务应该保护所有定义在数据上的不变的属性(例如完整性约束)。在完成了一个成功的事务时,数据应处于一致的状态。换句话说,一个事务应该把系统从一个一致-状态转换到另一个一致状态。举个例子,在关系数据库的情况下, 

一个一致的事务将保护定义在数据上的所有完整性约束。 

 

隔离性(ISOLATION): 

在同一个环境中可能有多个事务并发执行,而每个事务都应表现为独立执行。串行的执行一系列事务的效果应该同于并发的执行它们。这要求两件事: 

 

在一个事务执行过程中,数据的中间的(可能不一致)状态不应该被暴露给所有的其他事务。 

两个并发的事务应该不能操作同一项数据。数据库管理系统通常使用锁来实现这个特征。 

 

持久性(DURABILITY): 

一个被完成的事务的效果应该是持久的。

 

快速入门

在Spring Boot中,当我们使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpa依赖的时候,框架会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager。所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。

首先看pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.xiaojingg</groupId>
   <artifactId>springbootstudy-demo10-transactional</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>springbootstudy-demo10-transactional</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.9.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter</artifactId>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>

      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
         <version>5.1.21</version>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency>

   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>

</project>

通过定义User的name属性长度为5,这样通过创建时User实体的name属性超长就可以触发异常产生。

package com.xiaojingg.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

/**
 * 筱进GG
 */
@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false, length = 5)
    private String name;

    @Column(nullable = false)
    private Integer age;

    public User(){}

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

}
创建service实现UserRepository

package com.xiaojingg.domain;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

/**
 * 筱进GG
 */
public interface UserRepository extends JpaRepository<User, Long> {

    User findByName(String name);

    User findByNameAndAge(String name, Integer age);

    @Query("from User u where u.name=:name")
    User findUser(@Param("name") String name);
}

查看测试类:

此时查数据库中,创建了name从AAA到GGG的记录,没有HHHHHHHHHH、III、JJJ的记录。而若这是一个希望保证完整性操作的情况下,AAA到GGG的记录希望能在发生异常的时候被回退,这时候就可以使用事务让它实现回退,做法非常简单,我们只需要在test函数上添加@Transactional注解即可。

package com.xiaojingg;

import com.xiaojingg.domain.User;
import com.xiaojingg.domain.UserRepository;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootstudyDemo10TransactionalApplicationTests {

   @Autowired
   private UserRepository userRepository;

   @Test
   @Transactional
   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("HHHHHHHHHH", 80));
      userRepository.save(new User("III", 90));
      userRepository.save(new User("JJJ", 100));

      // 测试findAll, 查询所有记录
      Assert.assertEquals(10, userRepository.findAll().size());

      // 测试findByName, 查询姓名为FFF的User
      Assert.assertEquals(60, userRepository.findByName("FFF").getAge().longValue());

      // 测试findUser, 查询姓名为FFF的User
      Assert.assertEquals(60, userRepository.findUser("FFF").getAge().longValue());

      // 测试findByNameAndAge, 查询姓名为FFF并且年龄为60的User
      Assert.assertEquals("FFF", userRepository.findByNameAndAge("FFF", 60).getName());

      // 测试删除姓名为AAA的User
      userRepository.delete(userRepository.findByName("AAA"));

      // 测试findAll, 查询所有记录, 验证上面的删除是否成功
      Assert.assertEquals(9, userRepository.findAll().size());

   }

}
查看配置文件
spring.datasource.url=jdbc:mysql://localhost:3306/test1
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.properties.hibernate.hbm2ddl.auto=create

执行测试用例,可以看到控制台中抛出了如下异常,name字段超长:

org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not execute statement

再看数据库中,User表就没有AAA到GGG的用户数据了,成功实现了自动回滚。

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

package com.xiaojingg.service;

import com.xiaojingg.domain.User;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
/**
 * Created by Administrator on 2017-12-7 20:44:58
 */
public interface UserService {

    @Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED)
    User login(String name, String password);

}

事务详解

上面的例子中我们使用了默认的事务配置,可以满足一些基本的事务需求,但是当我们项目较大较复杂时(比如,有多个数据源等),这时候需要在声明事务时,指定不同的事务管理器。对于不同数据源的事务管理配置可以见《Spring Boot多数据源配置与使用》中的设置。在声明事务时,只需要通过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)

 

这里我们就结束了,更多的配置请看springboot官网配置哟!

Spring Boot之Spring Boot开启声明式事务

$%#%

Spring Boot之Spring Boot开启声明式事务

!!!!完成


打赏

已有1人打赏

已注销用户的gravatar头像
最近浏览
ᯤ⁵²¹ᴳ⁺  LV8 2021年10月11日
zlfsgg  LV3 2021年7月12日
胖胖来了  LV4 2021年7月2日
mrchen_yang94 2021年5月10日
暂无贡献等级
muyihuoye  LV1 2021年2月27日
a58807333  LV9 2021年1月4日
newhaijun  LV15 2020年12月28日
guixin  LV15 2020年10月20日
mikeyyy 2020年8月12日
暂无贡献等级
遇见,  LV36 2020年8月5日
顶部 客服 微信二维码 底部
>扫描二维码关注最代码为好友扫描二维码关注最代码为好友