事务

  • 在开发应用时,通常业务人员的一个操作实际上是对数据库读写的多步操作的结合。由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作的数据并不可靠,如果要让这个业务正确的执行下去,通常有实现方式:

    1. 记录失败的位置,问题修复之后,从上一次执行失败的位置开始继续执行后面要做的业务逻辑
    2. 在执行失败的时候,回退本次执行的所有过程,让操作恢复到原始状态,带问题修复之后,重新执行原来的业务逻辑
  • 事务就是针对上述方式2的实现。

Spring Boot中的事务管理

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

测试环境

  • 搭建一个web项目,使用h2数据库来测试事务,这里引用flyway的工程进行测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <dependencies>
    <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.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    </dependency>
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency>
    </dependencies>
  • 编写service类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    import java.util.List;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Service;
    import tk.fulsun.demo.entity.User;
    import tk.fulsun.demo.service.UserService;

    @Service
    public class UserServiceImpl implements UserService {

    private JdbcTemplate jdbcTemplate;

    UserServiceImpl(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public int create(String name, Integer age) {
    return jdbcTemplate.update("insert into USER(NAME, AGE) values(?, ?)", name, age);
    }

    @Override
    public List<User> getByName(String name) {
    List<User> users =
    jdbcTemplate.query(
    "select * from USER where NAME = ?",
    (resultSet, i) -> {
    User user = new User();
    user.setId(resultSet.getLong("ID"));
    user.setName(resultSet.getString("NAME"));
    user.setAge(resultSet.getInt("AGE"));
    return user;
    },
    name);
    return users;
    }

    @Override
    public int deleteByName(String name) {
    return jdbcTemplate.update("delete from USER where NAME = ?", name);
    }

    @Override
    public int getAllUsers() {
    return jdbcTemplate.queryForObject("select count(1) from USER", Integer.class);
    }

    @Override
    public int deleteAllUsers() {
    return jdbcTemplate.update("delete from USER");
    }
    }

手动制造异常

  • 修改create方法这样通过创建时User实体的age属性为0的时候就可以触发异常产生。

    1
    2
    3
    4
    5
    6
    7
    @Override
    public int create(String name, Integer age) {
    if (age <= 0) {
    throw new RuntimeException("年龄不能小于0");
    }
    return jdbcTemplate.update("insert into USER(NAME, AGE) values(?, ?)", name, age);
    }

测试类

  • 执行测试用例,可以看到控制台中抛出了如下异常,关于age字段的错误:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @SpringBootTest
    class UserServiceTest {
    @Autowired private UserService userService;

    @Test
    public void test1() {
    userService.create("张三", 18);
    userService.create("李四", 0);
    }
    }



    java.lang.RuntimeException: 年龄不能小于0

    at tk.fulsun.demo.service.impl.UserServiceImpl.create(UserServiceImpl.java:26)
    at tk.fulsun.demo.service.UserServiceTest.test1(UserServiceTest.java:18)
    ...
  • 内存数据库下,使用controller接口测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RestController
    @RequestMapping("/test")
    public class TransactiionTestController {
    @Autowired private UserService userService;

    @GetMapping("/create")
    public String test1() {
    try {
    userService.create("张三", 18);
    userService.create("李四", 0);
    } catch (Exception e) {
    return e.getMessage();
    }
    return "OK";
    }
    }

  • 查询数据库, 执行到一半之后因为异常中断了,张三的数据正确插入而李四没有成功插入

    1
    2
    3
    4
    SELECT * FROM USER;
    ID NAME AGE
    1 张三 18
    (1 row, 0 ms)
  • 如果这10条数据需要全部成功或者全部失败,那么这时候就可以使用事务来实现,做法非常简单,我们只需要在函数上添加@Transactional注解即可。

    • 不能使用try/catch ,catch“吃了异常”导致@Transactional失效

      1
      2
      3
      4
      5
      6
      7
      @GetMapping("/create")
      @Transactional
      public String test1() {
      userService.create("张三", 18);
      userService.create("李四", 0);
      return "OK";
      }
    • 访问 http://localhost:8080/test/create 后,可以看到异常信息,查询数据库数据为空,成功实现了自动回滚。

事务详解

  • 上面主要通过单元测试演示了如何使用@Transactional注解来声明一个函数需要被事务管理,通常我们单元测试为了保证每个测试之间的数据独立,会使用@Rollback注解让每个单元测试都能在结束时回滚。

  • 而真正在开发业务逻辑时,我们通常在service层接口中使用@Transactional来对各个业务逻辑进行事务管理的配置,例如:

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

@Transactional

  • 默认的事务配置,可以满足一些基本的事务需求
  • 但是当我们项目较大较复杂时(比如,有多个数据源等),这时候需要在声明事务时,指定不同的事务管理器。
  • 对于不同数据源的可以设置事务管理配置。在声明事务时,只需要通过value属性指定配置的事务管理器名即可,例如:@Transactional(value="transactionManagerPrimary")

隔离级别

  • 隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。

  • 我们可以看org.springframework.transaction.annotation.Isolation枚举类中定义了五个表示隔离级别的值:

    1
    2
    3
    4
    5
    6
    7
    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属性设置,例如:

    1
    @Transactional(isolation = Isolation.DEFAULT)

传播行为

  • 所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。

  • 我们可以看org.springframework.transaction.annotation.Propagation枚举类中定义了6个表示传播行为的枚举值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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属性设置,例如:

    1
    @Transactional(propagation = Propagation.REQUIRED)

应用场景

  • 传播行为有七种,不仅要知道这七种分别是什么意思,还要知道分别适合哪种场景使用。
  • 在service1(batchinset)中调用了service2(insert)方法,则service2可以定义以下不同事物传播行为
    • PROPAGATION_REQUIRED
      • 解释:如果当前存在事物,则加入该事物;如果不存在事物,则创建一个新的事物。该行为一般被设置为默认
      • 场景:多个事务操作嵌套,service2使用默认行为,出现异常会导致回滚
    • PROPAGATION_SUPPORTS
      • 解释:如果当前存在事物,则加入该事物;如果不存在事物,则直接执行
      • 场景:该模式是否支持事务,看调用它的方法是否有事务支持。
        • service1无事务,service2使用该行为,出现异常会不会导致回滚
        • service1有事务,service2使用该行为,出现异常会会导致回滚
    • PROPAGATION_MANDATORY (mandatory中文是强制性的意思)
      • 解释:强制要求当前存在一个事物,如果不存在则抛出异常
      • 场景:service2需要事物支持但自身又不管理事务提交或回滚,即强制要求前面的方法创建并管理一个事物给它用
        • service1无事务,service2使用该行为,会抛出了异常,提示没有存在的事务。
        • service1有事务,service2使用该行为,出现异常会会导致回滚
    • PROPAGATION_REQUIRES_NEW
      • 解释:不管当前是否存在事物,必定创建一个新事物,如果当前存在事物这个事物会被挂起。新事物在层级上与之前的事物是平级的,两者互不干扰
      • 场景:如service2事物的失败不想影响到service1事物的成功提交,可以使用这个行为,如订单和扣款是个事务操作,订单服务中调用付款服务,付款失败后不希望取消订单。
    • PROPAGATION_NOT_SUPPORTED
      • 解释:以非事务的方式运行,如果当前存在事务,暂停当前的事务(能不能挂起还得看对应的事务管理器是否支持挂起事物)
      • 场景:略
    • PROPAGATION_NEVER
      • 解释:永远不使用事物,如果存在事物,抛出异常
      • 场景:略
    • PROPAGATION_NESTED
      • 解释:如果不存在事物,则创建一个新的;如果存在事物,则在当前事务的一个嵌套事物中执行,嵌套事物的层级关系是外层事物的子事物,子事物执行的时候,外层事物不会被挂起,且子事物与外层事物共有事物状态(没有隔离)
      • 场景:将一个大事物分解为多个小事物执行,根据小事物的执行状态决定大事物的执行流程
      • 外围方法没有事务:这种情况跟REQUIRED是一样的,会新建一个事务。
      • 外围方法如果存在事务:这种情况就会嵌套事务。
        • 所谓嵌套事务,大意就是,
        • 外围事务回滚,内嵌事务一定回滚
        • 而内嵌事务可以单独回滚而不影响外围主事务和其他子事务。

多数据源的事务管理

  • 在一个Spring Boot项目中,连接多个数据源还是比较常见的。
  • 当我们采用多数据源的时候,同时也会出现一个这样的特殊场景:我们希望对A数据源的更新和B数据源的更新具备事务性。
  • 这样的例子很常见,比如:在订单库中创建一条订单记录,同时还需要在商品库中扣减商品库存。如果库存扣减失败,那么我们希望订单创建也能够回滚。
  • 如果这两条数据在一个数据库中,那么通过上面的事务管理就能轻松解决了。但是,当这两个操作位于不同的数据库中,那么就无法实现了。

什么是JTA

  • JTA,全称:Java Transaction API。JTA事务比JDBC事务更强大。一个JTA事务可以有多个参与者,而一个JDBC事务则被限定在一个单一的数据库连接。所以,当我们在同时操作多个数据库的时候,使用JTA事务就可以弥补JDBC事务的不足。

  • 在Spring Boot 2.x中,整合了这两个JTA的实现:

    • Atomikos:可以通过引入spring-boot-starter-jta-atomikos依赖来使用
    • Bitronix:可以通过引入spring-boot-starter-jta-bitronix依赖来使用
  • 由于Bitronix自Spring Boot 2.3.0开始不推荐使用,所以我们将使用Atomikos作为例子来介绍JTA的使用。

事务管理实现

场景设定

  • 假设我们有两个库,分别为:test1和test2
  • 这两个库中都有一张User表,我们希望这两张表中的数据是一致的
  • 假设这两张表中都已经有一条数据:name=ccc,age=20;因为这两张表中数据是一致的,所以要update的时候,就必须两个库中的User表更新时候,要么都成功,要么都失败。

传统方式配置多个数据源

  • 具体参考之前的多数据源配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    @Configuration
    public class DataSourceConfiguration {
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
    return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
    return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public JdbcTemplate primaryJdbcTemplate(
    @Qualifier("primaryDataSource") DataSource primaryDataSource) {
    return new JdbcTemplate(primaryDataSource);
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate(
    @Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
    return new JdbcTemplate(secondaryDataSource);
    }
    }
  • 测试接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @Service
    public class AtomikosTestService {
    private JdbcTemplate primaryJdbcTemplate;
    private JdbcTemplate secondaryJdbcTemplate;

    public AtomikosTestService(JdbcTemplate primaryJdbcTemplate, JdbcTemplate secondaryJdbcTemplate) {
    this.primaryJdbcTemplate = primaryJdbcTemplate;
    this.secondaryJdbcTemplate = secondaryJdbcTemplate;
    }
    @Transactional
    public void tx() {
    // 修改test1库中的数据
    primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "ccc");
    // 修改test2库中的数据
    secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "ccc");
    }

    @Transactional
    public void tx2() {
    // 修改test1库中的数据
    primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "ccc");
    // 模拟:修改test2库之前抛出异常
    throw new RuntimeException();
    }
    }

  • 当发生异常的时候,事务不起作用了

JTA方式

添加依赖

  1. pom.xml中加入JTA的实现Atomikos的Starter

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
    </dependency>
  2. application.properties配置文件中配置两个test1和test2数据源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    spring:
    jta:
    enabled: true
    atomikos:
    datasource:
    primary:
    xa-data-source-class-name: org.h2.jdbcx.JdbcDataSource
    xa-properties:
    url: jdbc:h2:mem:testdbsa
    user: sa
    password:
    unique-resource-name: test1
    max-pool-size: 25
    min-pool-size: 3
    max-lifetime: 20000
    borrow-connection-timeout: 10000
    secondary:
    xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
    xa-properties:
    url: jdbc:mysql://192.168.56.101:3306/test2
    user: test
    password: 123456
    unique-resource-name: test2
    max-pool-size: 25
    min-pool-size: 3
    max-lifetime: 20000
    borrow-connection-timeout: 10000
  3. 数据源配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package tk.fulsun.demo.config;

    import com.atomikos.jdbc.AtomikosDataSourceBean;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.jdbc.core.JdbcTemplate;

    /**
    * @author fulsun
    * @description: 数据源的配置信息
    * @date 6/11/2021 3:20 PM
    */
    @Configuration
    public class DataSourceConfiguration {
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.primary")
    public DataSource primaryDataSource() {
    return new AtomikosDataSourceBean();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.secondary")
    public DataSource secondaryDataSource() {
    return new AtomikosDataSourceBean();
    }

    @Bean
    public JdbcTemplate primaryJdbcTemplate(
    @Qualifier("primaryDataSource") DataSource primaryDataSource) {
    return new JdbcTemplate(primaryDataSource);
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate(
    @Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
    return new JdbcTemplate(secondaryDataSource);
    }
    }

  4. 重新测试异常发生后的结果,数据保持一致

参考