Spring事务

目录

什么是事务?

为什么需要事务?

Spring中事务的实现

数据准备

创建数据表

model

mapper

service

controller

Spring 编程式事务

Spring 声明式事务

@Transactional

rollbackFor

事务隔离级别

事务传播机制


什么是事务?

事务:指作为单个逻辑工作单元执行的一系列操作,是一组操作的集合,是一个不可分割的操作。在数据库管理系统中,事务通常用于确保数据库操作的原子性、一致性、隔离性和持久性

事务会把所有的操作作为一个整体,一起向数据库提交或是撤销操作的请求,这组操作要么同时成功,要么同时失败

为什么需要事务?

事务的存在是为了保证数据库操作的一致性、完整性和可靠性

例如:

在进行转账操作时(A账户向B账户转账):

第一步:A 账户 -1000 元

第二步:B账户 +1000 元

若没有事务,第一步执行成功,但第二步执行失败了,此时 A 账户中的 1000 元就平白无故消失了。此时使用事务,让 A-1000 B+1000 作为一组操作,这组操作要么一起成功,要么一起失败

事务的操作

事务的操作主要有三步

1. 开启事务 start transaction/ begin(一组操作前开启事务)

2. 提交事务 commit(这组操作全部成功,提交事务)

3. 回滚事务 rollback(这组操作中间任意一个操作出现异常,回滚事务)

Spring中事务的实现

Spring 中的事务操作分为两类

1. 编程式事务(手动写代码操作事务)

2. 声明式事务(利用注解自动开启和提交事务)

接下来,我们通过用户注册的例子来进一步理解Spring中事务的实现

数据准备

创建数据表

要实现用户注册,我们首先需要准备两张表:user_info(用户表)log_info(操作日志表)

-- 创建数据库 
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
use trans_test;
-- 创建用户表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
 `id` INT NOT NULL AUTO_INCREMENT,
 `user_name` VARCHAR (128) NOT NULL,
 `password` VARCHAR (128) NOT NULL,
 `create_time` DATETIME DEFAULT now(),
 `update_time` DATETIME DEFAULT now() ON UPDATE now(),
 PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARACTER 
SET = utf8mb4 COMMENT = '⽤⼾表';
-- 创建操作日志表 
DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info (
 `id` INT PRIMARY KEY auto_increment,
 `user_name` VARCHAR ( 128 ) NOT NULL,
 `op` VARCHAR ( 256 ) NOT NULL,
 `create_time` DATETIME DEFAULT now(),
 `update_time` DATETIME DEFAULT now() ON UPDATE now() 
) DEFAULT charset 'utf8mb4';

接下来,我们引入依赖、创建项目、连接数据库...

创建实体类

model

UserInfo:

import lombok.Data;

import java.util.Date;

@Data
public class UserInfo {
    private Integer id;
    private String userName;
    private String password;
    private Date createTime;
    private Date updateTime;
}

LogInfo:

import lombok.Data;

import java.util.Date;

@Data
public class LogInfo {
    private Integer id;
    private String userName;
    private String op;
    private Date createTime;
    private Date updateTime;
}

在进行用户注册时,我们需要向用户表user_info 和 日志表log_info 中插入数据:

mapper

UserMapper:

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    @Insert("insert into user_info (user_name, password) values (#{userName}, #{password})")
    Integer insertUser(String userName, String password);
}

LogMapper:

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface LogMapper {
    @Insert("insert into log_info (user_name, op) values (#{userName}, #{op})")
    Integer insertLog(String userName, String op);
}

service

UserService:

import com.example.springtrans.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service

public class UserService {
    @Autowired
    private UserMapper userMapper;

    public Integer insertUser(String userName, String password) {
        return userMapper.insertUser(userName, password);
    }
}

LogService:

import com.example.springtrans.mapper.LogMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service

public class LogService {
    @Autowired
    private LogMapper logMapper;
    public Integer insertLog(String userName, String op){
        return logMapper.insertLog(userName, op);
    }
}

controller

userController:

import com.example.springtrans.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")

public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password){
        //注册
        Integer result = userService.insertUser(userName, password);
        return true;

    }
}

Spring 编程式事务

Spring手动操作事务有3个重要操作步骤:

开启事务

提交事务

回滚事务

import com.example.springtrans.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")

public class UserController {
    //JDBC 事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    // 定义事务属性
    @Autowired
    private TransactionDefinition transactionDefinition;

    @Autowired
    private UserService userService;
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password){
        //开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        //注册
        Integer result = userService.insertUser(userName, password);
        //提交事务
        dataSourceTransactionManager.commit(transactionStatus);
        return true;
    }
}

SpringBoot 内置了两个对象:

DataSourceTransactionManager :事务管理器,用来开启、提交或回滚事务

TransactionDefinition:事务的属性,在开启事务的时候需要将 TransactonDefinition 传递进取从而获取一个事务 TransactionStatus

我们运行程序,观察事务的提交:

http://127.0.0.1:8080/user/registry?userName=zhangsan&password=zhangsan

观察数据库,数据插入成功:

 接下来,我们观察事务回滚:

import com.example.springtrans.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")

public class UserController {
    //JDBC 事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    // 定义事务属性
    @Autowired
    private TransactionDefinition transactionDefinition;

    @Autowired
    private UserService userService;
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password){
        //开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer result = userService.insertUser(userName, password);
        //回滚事务
        dataSourceTransactionManager.rollback(transactionStatus);
//        //提交事务
//        dataSourceTransactionManager.commit(transactionStatus);
        return true;
    }
}

再次运行,访问:

http://127.0.0.1:8080/user/registry?userName=zhangsan&password=zhangsan

此时再观察数据库:

数据库中并未新增数据

我们对比两次打印的日志:

从日志可以看到两次都执行了插入操作,且插入成功,但由于第二次事务进行了回滚,因此数据库中没有新增数据

Spring 声明式事务

使用声明式事务只需要在需要事务的方法上添加 @Transactional 注解就可以实现了,无需手动开启事务和提交事务,在进入方法时会自动开启事务,而当方法执行完会自动提交事务,若中途发生了未处理的异常,就会自动回滚事务

    @Transactional
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password){
        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer result = userService.insertUser(userName, password);
        return true;
    }

再次运行程序,观察数据库:

从自增 id 值我们也可以看出:在之前进行回滚事务时,数据插入到用户表中,但由于事务回滚,因此数据库没有新增数据

若在执行过程中出现异常:

    @Transactional
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password){
        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer result = userService.insertUser(userName, password);
        int a = 1 / 0;
        return true;
    }

重新运行程序,观察数据库和打印的日志:

数据插入成功,但数据库没有新增数据,事务进行了回滚

事务的控制一般是在业务逻辑层中的,这是因为在业务逻辑层当中,一个业务功能可能会包含多个数据访问的操作,在业务逻辑层来控制事务,就可以将多个数据访问操作控制在一个事务范围内

在这里,为了方便最开始的学习,因此我们将其写在 controller 

@Transactional

@Transactional 可以用来修饰方法或类

修饰方法:只有修饰 public 方法时才会生效(修饰其他方法时不会报错,但也不会生效)

修饰类: @Transactional 修饰的类中所有的 public 方法生效

当 类/方法被 @Transactional 修饰时,在目标方法执行开始前,会自动开启事务,方法执行结束后,会自动提交事务

若在方法执行的过程中,出现了异常,且异常未被捕获,就会进行事务回滚

若异常被程序捕获,方法就会被认为是成功执行,仍然会提交事务

我们对上述异常进行捕获:

    @Transactional
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password){
        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer result = userService.insertUser(userName, password);
        try {
            int a = 1 / 0;
        }catch (Exception e){
            e.printStackTrace();
        }
        return true;
    }

重新运行程序,观察数据库,数据插入成功:

 此时虽然程序出错了,但由于异常被捕获了,所以事务依然得到了提交

若需要事务进行回滚,有两种方式:

1. 重新抛出异常

  @Transactional
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password){
        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer result = userService.insertUser(userName, password);
        try {
            int a = 1 / 0;
        }catch (Exception e){
            throw e;
        }
        return true;
    }

此时重新运行事务,观察数据库:

此时在运行过程中出现了异常,且异常被捕获,但由于异常又被抛出,因此事务进行了回滚

2. 手动回滚事务


    @Transactional
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password){
        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer result = userService.insertUser(userName, password);
        try {
            int a = 1 / 0;
        }catch (Exception e){
            //手动回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return true;
    }

再次运行:

在运行过程中出现了异常,且异常被捕获,但由于设置了事务回滚,因此事务进行了回滚

接下来,我们来了解 @Transactional 注解中的三个常见属性

1. rollbackFor:异常回滚属性,指定能够触发事务回滚的异常类型可以指定多个异常类型

2. Isolaction:事务的隔离级别 默认值为 Isolaction.DEFAULT

3. propagation:事务的传播机制 默认值为 Propagation.REQUIRED

rollbackFor

我们来看一个例子:

    @Transactional
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password) throws IOException {
        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer result = userService.insertUser(userName, password);
        log.info("数据插入, {}", result);
        // 数据插入成功,抛出异常
        if(result > 0){
            throw new IOException();
        }
        return true;
    }

此时运行代码,观察打印的日志:

此时虽然抛出了异常,但是事务依然进行了提交

此时虽然发生了未处理的异常,但事务依然进行了提交,未进行回滚,这是为什么呢?

这是因为 @Transactional 默认只有在遇到 运行时异常 ERROR 时才会回滚,非运行时异常时不会回滚,即 Exception的子类中,除了 RuntimeException及其子类

若需要回滚指定类型的异常,可以通过 rollbackFor 属性来指定

 @Transactional(rollbackFor = Exception.class)
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password) throws IOException {
        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer result = userService.insertUser(userName, password);
        log.info("数据插入, {}", result);
        // 数据插入成功,抛出异常
        if(result > 0){
            throw new IOException();
        }
        return true;
    }

此时再运行程序:

此时事务进行了回滚

事务隔离级别

事务的隔离级别解决的是多个事务同时调用一个数据库的问题

我们首先来回顾MySQL的事务隔离级别

SQL标准定义了四种隔离级别,MySQL全都支持,这四种隔离级别分别是:

(1)读未提交(Read Uncommitted):事务中的修改可以被其他事务读取,即一个事务可以读到另一个事务未提交的更新数据。此级别会产生脏读、不可重复读、幻读等问题。

其他事务未提交的数据可能会发生回滚,但该隔离级别可以读到,该级别读到的数据称为脏数据,这个现象叫做脏读

(2)读已提交(Read Committed):一个事务只能读到另一个事务已经提交的数据,避免了脏读,但是可能会出现不可重复读幻读

在事务执行中可以读到其他事务提交的结果,在不同的时间,相同的SQL查询结果可能不同,这种现象叫做不可重复读

(3)可重复读(Repeatable Read):保证了在同一事务中多次读取同一数据时,结果始终相同,避免了不可重复读。但是仍然可能出现幻读

当此级别的事务正在执行时,另一个事务成功插入了某条数据,但因为它每次查询的结果是相同的,此时就会导致查询不到这条数据,自己重复插入时失败(唯一约束的原因),在事务中查询不到这条信息,但自己就是插入不进去,这个现象叫做幻读

(4)序列化(Serializable):最高的隔离级别,强制事务串行执行,避免了脏读、不可重复读、幻读等问题。但是也带来了性能上的损失。

隔离级别越高,数据库的并发性能就越差。因此,在选择隔离级别时,需要根据实际情况进行权衡。

事务隔离级别脏读不可重复读幻读
读未提交(Read Uncommitted)
读已提交(Read Committed)×
可重复读(Repeatable Read)××
序列化(Serializable)×××

Spring 事务隔离级别:

Spring 中事务隔离级别有5种

Isolation.DEFAULT:以连接的数据库的事务隔离级别为主
Isolation.READ_UNCOMMITTED:读未提交,对应SQL标准中的 Read Uncommitted
Isolation.READ_COMMITTED:读已提交,对应SQL标准中的 Read Committed
Isolation.REPEATABLE_READ:可重复读,对应SQL标准中的 Repeatable Read
Isolation.SERIALIZABLE:串行化,对应SQL标准中的 Serializable

 Spring 中事务隔离级别可以通过  @Transactional 中的 isolation 属性进行设置

@Transactional( isolation = Isolation.READ_COMMITTED)

事务传播机制

什么是事务传播机制?

事务传播机制:多个事务方法存在调用关系时,事务是如何在这些方法之间进行传播的

例如:

存在方法A和B都被 @Transactional 修饰,A方法调用B方法

A方法运行时,会开启一个事务,而当A调用B时,B方法本身也有事务,此时B方法运行时,是加入A的事务,还是创建一个新的事务呢?

这就涉及到事务的传播机制了,事务的传播机制解决的是一个事务在多个节点(方法)中传递的问题

事务的传播机制有:

Spring 事务传播机制有以下7种:

Propagation.REQUIRED:默认的事务传播级别,若当前存在事务,则加入该事务;若当前没有事务,则创建一个新的事务

若 A 存在事务,调用B方法时,B方法使用 A的事务;若A不存在事务,则B方法创建一个新的事务

Propagation.SUPPORTS:若当前存在事务,则加入该事务;若当前没有事务,则以非事务的方式继续运行

若 A 存在事务,调用B方法时,B方法使用 A的事务;若A不存在事务,则B方法以非事务的方式运行

Propagation.REQUIRES_NEW:无论当前是否存在事务,都会创建一个新的事务,且开启的事务互相独立,互不干扰

若 A 存在事务,调用B方法时,B方法将A的事务挂起(不用),且开启新的事务;若A不存在事务,B方法开启新的事务

Propagation.NOT_SUPPORTED:以非事务的方式运行,若当前存在事务,则将当前事务挂起

若 A 存在事务,调用B方法时,B方法将A的事务挂起(不用),以非事务的方式运行;若A不存在事务,B方法以非事务的方式运行

Propagation.MANDATORY:若当前存在事务,则加入该事务,若当前没有事务,则抛出异常

若 A 存在事务,调用B方法时,B方法使用 A的事务;若A不存在事务,则抛出异常

Propagation.NEVER:以非事务的方式运行,若当前存在事务,则抛出异常

若 A 存在事务,调用B方法时,则抛出异常;若A不存在事务,B以非事务的方式运行

Propagation.NESTED:若当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,若当前没有事务,则创建一个新的事务

若 A 存在事务,调用B方法时,B方法创建一个事务作为当前事务的嵌套事务来运行;若A不存在事务,则B方法创建一个新的事务

@Transactional 注解支持事务传播的设置,通过 propagation 属性来指定传播行为

我们还是通过用户注册来进一步理解Spring的事务传播机制

当进行用户注册时,需要向用户表插入用户数据,向操作日志表插入用户操作记录

我们首先来看 REQUIRED:

UserController:

public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private LogService logService;
    @Transactional( propagation = Propagation.REQUIRED)
    @RequestMapping("/registry")
    public Boolean registry(String userName, String password) throws IOException {
        //参数校验
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        //注册
        Integer userResult = userService.insertUser(userName, password);
        log.info("数据插入 user_info:{}", userResult);
        Integer logResult = logService.insertLog(userName, "用户注册");
        log.info("数据插入 log_info: {}", logResult);
        return true;
    }
}

UserService:

public class UserService {
    @Autowired
    private UserMapper userMapper;
    @Transactional(propagation = Propagation.REQUIRED)
    public Integer insertUser(String userName, String password) {
        return userMapper.insertUser(userName, password);
    }
}

LogService:

public class LogService {
    @Autowired
    private LogMapper logMapper;
    @Transactional(propagation = Propagation.REQUIRED)
    public Integer insertLog(String userName, String op){
        int result = logMapper.insertLog(userName, op);
        int a = 1 / 0;
        return result;
    }
}

运行程序,观察数据库,此时没有插入任何数据

(1)registry 方法开启事务

(2)接着调用 userService.insertUser 方法,向user_info 插入一条数据(执行成功),(与registry 使用同一个事务)

(3)然后registry调用 logService.insertLog 方法,向 log_info 插入一条数据(插入成功),但由于出现了异常,因此执行失败(与registry 使用同一个事务)

(4)由于(3)出现了异常,因此事务回滚,(2)与(3)使用的同一个事务,因此(2)的数据也回滚了

REQUIRES_NEW

将上述userService.insertUser 和 logService.insertLog 的事务传播机制改为 Propagation.REQUIRES_NEW

运行程序,此时用户表数据插入成功了,日志表数据插入失败

(1)registry方法开启事务

(2)接着调用 userService.insertUser 方法,向user_info 插入一条数据(执行成功),(使用新开启的事务)

(3)然后registry调用 logService.insertLog 方法,向 log_info 插入一条数据(插入成功),但由于出现了异常,因此执行失败(使用新开启的事务)

(4)由于(3)出现了异常,因此事务回滚,(2)与(3)使用的是不同事务,互不干涉,因此(2)的数据不会进行回滚

其他的事务传播机制在这里就不再进行分析了,大家可以自行测试和分析

接下来,我们来看 NESTED (嵌套事务)

将userService.insertUser 和 logService.insertLog 的事务传播机制改为 Propagation.NESTED

再次运行程序,发现没有数据插入:

(1)registry方法开启事务

(2)接着调用 userService.insertUser 方法,向user_info 插入一条数据(执行成功),(嵌套registry的事务)

(3)然后registry调用 logService.insertLog 方法,向 log_info 插入一条数据(插入成功),但由于出现了异常,因此执行失败(嵌套registry的事务)

(4)由于(3)出现了异常,因此事务回滚,使用的是嵌套事务,因此 logService.insertLog往上找调用它的方法和事务,因此 userService.insertUser 也失败了,所以(2)的数据也回滚了

 registry 事务可以认为是父事务,嵌套事务是子事务,父事务出现异常,子事务也会回滚,子事务出现异常如果不进行处理,也会导致父事务回滚

当事务执行成功时,NESTEDREQUIRED 的结果是相同的,而上述执行失败时,此时结果也与 REQUIRED 结果相同,那么 NESTED 与 REQUIRED 有什么不同呢?

NESTED 和 REQUIRED 的区别

我们修改 LogService

public class LogService {
    @Autowired
    private LogMapper logMapper;
    @Transactional(propagation = Propagation.NESTED)
    public Integer insertLog(String userName, String op){
        int result = logMapper.insertLog(userName, op);
        try {
            int a = 1 / 0;
        }catch (Exception e){
            // 回滚当前事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }

        return result;
    }
}

运行代码,此时用户表数据插入成功了,日志表数据插入失败

而当我们将 logService.insertLog 事务的传播机制修改为 REQUIRED:

此时再次运行程序,用户表和日志表的数据都添加失败

我们可以看到:

当事务全部执行成功时,NESTEDREQUIRED 的结果是相同的

当事务部分执行成功时,REQUIRED 会导致整个事务全部回滚,而 NESTED 嵌套事务可以实现局部回滚(不影响上一个方法中的执行结果)

嵌套事务之所以能够实现部分事务的回滚,是因为事务中有一个保存点(savepoint),嵌套事务进入后相当于新建了一个保存点,而回滚时只是回滚到当前保存点

REQUIRED 是加入到当前事务中的,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚

  • 60
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 47
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 47
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

楠枬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值