Spring Boot 事务管理

什么是事务?

事务实际上是指一系列操作,程序中的事务大多时候是指数据库事务,那么这一系列操作就是数据库操作,通常包含多个 SQL 语句。

为了保证完整性,事务需要满足 ACID 原则:
(1) 原子性(Atomic)
一个事务无论包含多少操作,要么全部执行,要么全部不执行,若执行期间某个操作失败,则在其之前执行的操作都要回滚到事务执行前状态。
(2) 一致性(Consistency)
一个事务使系统从一个一致状态转换到另一个一致状态。
(3) 隔离性(Isolation)
事务执行过程中的数据变化只存在于该事务中,对外界不产生影响,只有该事务正常执行完毕后,其它事务才能获取到这些变化的数据。
(4) 持久性(Durability)
事务正常执行完毕后对数据的改变是永久性的。

本文重点在于 Spring Boot 中事务的使用,不再赘述事务相关的技术细节。

本文示例基于之前已介绍过的代码,如有不清楚还请参看:
Spring Boot 集成 MyBatis
Spring Boot 集成阿里巴巴 Druid 数据库连接池

Spring 在之前版本中早已提供事务管理的能力,Spring Boot 诞生后进一步简化了事务配置工作。如果添加了 spring-boot-starter-jdbc 依赖,框架会默认自动注入 DataSourceTransactionManager ;如果添加了 spring-boot-starter-data-jpa 依赖,框架会默认自动注入 JpaTransactionManager。无需其它额外配置,直接在需要添加事务处理的方法上使用 @Transactional 注解。

1 定义 Service 接口

package demo.spring.boot.transaction.service;

import demo.spring.boot.transaction.domain.User;

public interface UserService {
    
    void addUser(User user);
}

2 定义 Service 接口实现类

package demo.spring.boot.transaction.service.impl;

import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    public void addUser(User user) {
        userDao.insert(user);
    }
}

3 编写单元测试

package demo.spring.boot.transaction;

import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
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 java.time.LocalDate;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTests {
    
    @Autowired
    private UserService userService;
    
    @Test
    public void testAddUser() {
        User user = new User();
        user.setAccount("test_account");
        user.setName("Test Name");
        user.setBirth(LocalDate.now());
        userService.addUser(user);
    }
}

执行单元测试,测试通过,查询数据库可以看到刚插入的数据

mysql> select * from user \G;
*************************** 1. row ***************************
     id: 24
account: test_account
   name: Test Name
  birth: 2018-08-03
1 row in set (0.00 sec)

4 对 Service 接口实现类的 addUser 方法稍作调整,在插入数据后执行一条肯定会抛出异常的语句,如下

@Override
public void addUser(User user) {
    userDao.insert(user);
    int x = 1 / 0;
}

删除数据库 user 表中记录(因为 account 字段添加了唯一性索引),再次执行单元测试,测试失败,失败原因是被测试方法抛出了异常 java.lang.ArithmeticException: / by zero
但是因为抛出异常在 DAO 执行插入操作之后,所以数据库中还是成功插入了数据,数据库查询结果如下:

mysql> select * from user \G;
*************************** 1. row ***************************
     id: 25
account: test_account
   name: Test Name
  birth: 2018-08-03
1 row in set (0.00 sec)

5 在 Service 接口实现类的 addUser 方法中添加 @Transactional 注解实现事务管理

package demo.spring.boot.transaction.service.impl;

import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    @Transactional
    public void addUser(User user) {
        userDao.insert(user);
        int x = 1 / 0;
    }
}

执行和第 4 步相同步骤的单元测试,注意还是需要删除之前插入 user 表中记录,依旧因为异常原因导致单元测试失败,但是查询数据库 user 表无数据记录,说明异常抛出前 DAO 插入的数据已被成功回滚(省略数据库查询结果)。

注意:@Transactional 默认只回滚 Unchecked Exception,即 RuntimeException,所有 Checked Exception 默认是不会滚的

示例:
首先,修改 addUser 方法,将 int x = 1 / 0; 替换成抛出 Checked Exception

package demo.spring.boot.transaction.service.impl;

import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    @Transactional
    public void addUser(User user)
        throws IOException {
        userDao.insert(user);
        throw new IOException();
    }
}

其次,修改单元测试方法

@Test
public void testAddUser()
    throws IOException {
    User user = new User();
    user.setAccount("test_account");
    user.setName("Test Name");
    user.setBirth(LocalDate.now());
    userService.addUser(user);
}

最后,运行单元测试,测试失败,但是查询数据库仍看到抛出异常前插入的数据

mysql> select * from user \G;
*************************** 1. row ***************************
     id: 28
account: test_account
   name: Test Name
  birth: 2018-08-03
1 row in set (0.00 sec)

@Transactional 指定异常回滚

查看 @Transactional 源码,rollbackFor 属性可以指定针对某些异常回滚(其它类似功能属性请参考 API 文档

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.METHOD, ElementType.TYPE})
@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 {};
}

修改 addUser 方法,添加 @Transactional 注解属性 rollbackFor,指定当抛出 java.io.IOException 异常时回滚

package demo.spring.boot.transaction.service.impl;

import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    @Transactional(rollbackFor = {IOException.class})
    public void addUser(User user)
        throws IOException {
        userDao.insert(user);
        throw new IOException();
    }
}

运行单元测试,测试失败,但是查询数据库 user 表无数据记录,说明异常抛出前 DAO 插入的数据已被成功回滚(省略数据库查询结果)。

如果赋予 rollbackFor 属性其它异常类型,既不是 java.io.IOException 又不是其父类,则运行单元测试后尽管测试失败,但是异常抛出前的数据也会被插入数据库 user 表中,请自行测试。

项目工程目录


POM文件

<?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>demo.spring.boot</groupId>
    <artifactId>demo-spring-boot-transaction</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo-spring-boot-transaction</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.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-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

</project>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容