SpringBoot事务控制
in JavaDevelop with 0 comment

SpringBoot事务控制

in JavaDevelop with 0 comment

步骤

首先来了解一下什么是数据库事务。

由于 maven 下载包需要联网,这里先将 maven 包下载下来,放到本地仓库便可以解决联网的问题

wget http://labfile.oss.aliyuncs.com/courses/1152/m2.zip unzip m2.zip mv .m2 /home/shiyanlou/

项目文件结构

image-1655350865772

什么是数据库事务

数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的 ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由 DBMS 中的事务管理子系统负责事务的处理。 ----来自百度百科

数据库事务的特性

事务的隔离级别

在数据库事务的 ACID 四个属性中,隔离性是一个最常放松的一个。为了获取更高的隔离等级,数据库系统的锁机制或者多版本并发控制机制都会影响并发。 应用软件也需要额外的逻辑来使其正常工作。 很多数据库管理系统定义了不同的“事务隔离等级”来控制锁的程度。在很多数据库系统中,多数的数据库事务都避免高等级的隔离等级(如可序列化)从而减少对系统的锁定开销。程序员需要小心的分析数据库访问部分的代码来保证隔离级别的降低不会造成难以发现的代码 bug。相反的,更高的隔离级别会增加死锁发生的几率,同样需要编程过程中去避免。 ANSI/ISO SQL 定义的标准隔离级别如下:

可串行化

最高的隔离级别。 在基于锁机制并发控制的 DBMS 实现可串行化,要求在选定对象上的读锁和写锁保持直到事务结束后才能释放。在 SELECT 的查询中使用一个“WHERE”子句来描述一个范围时应该获得一个“范围锁”(range-locks)。这种机制可以避免“幻影读”(phantom reads)现象(详见下文)。 当采用不基于锁的并发控制时不用获取锁。但当系统探测到几个并发事务有“写冲突”的时候,只有其中一个是允许提交的。这种机制的详细描述见“快照隔离”

可重复读

在可重复读(REPEATABLE READS)隔离级别中,基于锁机制并发控制的 DBMS 需要对选定对象的读锁(read locks)和写锁(write locks)一直保持到事务结束,但不要求“范围锁”,因此可能会发生“幻影读”。

提交读

在提交读(READ COMMITTED)级别中,基于锁机制并发控制的 DBMS 需要对选定对象的写锁一直保持到事务结束,但是读锁在 SELECT 操作完成后马上释放(因此“不可重复读”现象可能会发生,见下面描述)。和前一种隔离级别一样,也不要求“范围锁”。

未提交读

未提交读(READ UNCOMMITTED)是最低的隔离级别。允许“脏读”(dirty reads),事务可以看到其他事务“尚未提交”的修改。 -----来自 wiki 百科

不可重复读和幻影读的区别主要在于不可重复读是由于更新和删除操作造成的,而幻影读是由于插入操作造成的。

事务控制原理

Spring 事务管理是基于接口代理(JDK)或动态字节码(CGLIB)技术,然后通过 AOP 实 施事务增强的。当我们执行添加了事务特性的目标方式时,系统会通过目标对象的代理对 象调用 DataSourceTransactionManager 对象,在事务开始的时,执行 doBegin 方法, 事务结束时执行 doCommit 或 doRollback 方法。如图所示:
在这里插入图片描述

Sprng Boot 事务控制

在 Spring 中,要进行事务管理有两种方式,一种是编程式事务,一种是声明式事务,但是都需要配置事务管理器,想要了解的同学可以去查看 Spring 的事务管理,而 SpringBoot 的事务管理十分简单,只需要一个@Transactional 注解就可以了,当然必须是使用主流 ORM 框架。下面看看如何使用 SpringBoot 进行事务控制,这里采用 Spring-Date-JPA 来演示,Spring-Data-JPA 比较简单,学过 Hibernate 的同学都可以很快的上手,没有学过的同学也没有关系,这里的应用比较简单。

初始化数据库

先建立一个测试数据库,首先打开终端,然后需要启动 mysql 服务

sudo service mysql start

接着进入数据库 创建数据库test,并且查看数据库是否创建成功。 切换到数据库test,创建数据表user;

mysql -u root
create database test;
show databases;

use test;
CREATE TABLE user
(
    id int PRIMARY KEY AUTO_INCREMENT,
    username varchar(50),
    password varchar(50)
);

image-1655350881007

创建 SpringBoot 项目

首先需要创建一个 SpringBoot 项目springboot,接着创建包com.shiyanlou.springboot,最后形成下面的目录结构:

image-1655350887396

接着修改 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.shiyanlou</groupId>
    <artifactId>springboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot</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/>
    </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-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            </dependency>
    </dependencies>

    <build>
        <plugins>
        <!--spirng Boot maven插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

User.java

创建实体类 User.java

package com.shiyanlou.springboot;

import javax.persistence.*;

/**
 * 设置表名为user,并且标记该类为实体类
 */
@Table(name = "user")
@Entity
public class User {

    /**
     * 设置主键生成策略
     */
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    public int id;

    @Column
    private String username;

    @Column
    private String password;

    public int getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

UserRepository.java

UserRepository.java 作为 dao 层

package com.shiyanlou.springboot;

import org.springframework.data.repository.CrudRepository;

/**
 * 继承CrudRepository
 */
public interface UserRepository extends CrudRepository<User, Integer> {

}

UserService.java

UserService.java 作为 service 层 UserService 层的作用是保存传递过来的 User 对象,接着将保存后的 User 对象的密码修改成123456

package com.shiyanlou.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public User save(User user) {
//        保存实体类
        userRepository.save(user);
        //修改密码
        user.setPassword("123456");
        //重新保存,更新记录
        return userRepository.save(user);
    }


}

SpringbootApplication.java

用于启动 Spring Boot 程序

package com.shiyanlou.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootApplication.class, args);
    }
}

ServerTest.java

package com.shiyanlou.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

/**
 * ApplicationRunner接口可以让在SpringBoot启动后马上执行想要执行的方法
 */
@Component
public class ServerTest implements ApplicationRunner {

    /**
     * 注入userService服务
     */
    @Autowired
    public UserService userService;

    /**
     * 该方法再SpringBoot启动完成后立即执行
     *
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) {
//        新建一个实体类
        User user = new User();
        user.setPassword("springboot");
        user.setUsername("shiyanlou");
//        调用包存实体类的service
        userService.save(user);
    }
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

测试 SpringBoot 事务管理

启动 SpringBoot 项目

mvn spring-boot:run

等待项目运行完成,看下数据库记录

select * from user;

成功保存记录并且修改密码为 123456,现在在 service 中人为抛出一个异常,修改 UserService.java。

package com.shiyanlou.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public User save(User user) {
//        保存实体类
        userRepository.save(user);
//        人为抛出异常
        int shiyanlou = 1 / 0;
        //修改密码
        user.setPassword("123456");
        //重新保存,更新记录
        return userRepository.save(user);
    }


}

再查看一下数据库记录,可以看到数据库中的数据并不是想要的,因为异常的发生,导致数据库数据出现了错误,最后修改密码的方法没有运行,密码依旧为 spingboot,将异常处理加上看看还会不会出现这种情况。

在 UserService.java 的 save 方法上加上@Transactional 注解

@Transactional 属性如下:

属性 类型 描述
value String 设置事务管理器(可选)
transactionManager String 设置事务管理器(可选)
propagation enum: Propagation 事务传播行为 (可选)
isolation enum: Isolation 事务隔离级别 (可选), 默认值采用 DEFAULT。当多个事务并发执行时,可能会 出现脏读,不可重复读,幻读等现象时,但假如不希望出现这些现象可考虑修改事务 的隔离级别(但隔离级别越高并发就会越小,性能就会越差)
timeout int 事务超时时间 (可选), 默认值为-1,表示没有超时显示。如果配置了具体时间,则 超过该时间限制但事务还没有完成,则自动回滚事务。这个时间的记录方式是在事务 开启以后到 sql 语句执行之前。
readOnly boolean 是否只读事务,默认 false,即为读写事务 (可选), 为了忽略那些不需要事务的 方法,比如读取数据,可以设置 read-only 为 true。对添加,修改,删除业务 readonly 的值应该为 false。
rollbackFor Class 对象数组,继承自 Throwable 导致事务回滚的异常类数组 (可选), 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指 定,各类型之间可以通过逗号分隔。
rollbackForClassName 类名 数组,继承自 Throwable 的类名 导致事务回滚的异常类名字数组 (可选)
noRollbackFor Class 对象数组,继承自 Throwable 不会导致事务回滚的异常类数组 (可选)
noRollbackForClassName 类名 数组,继承自 Throwable 的类名 不会导致事务回滚的异常类名字数组(可选)

修改 userService.java

package com.shiyanlou.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    /**
     * 添加事务管理
     */
    @Transactional(rollbackFor = Exception.class)
    public User save(User user) {
//        保存实体类
        userRepository.save(user);
//        人为抛出异常
        int shiyanlou = 1 / 0;
        //修改密码
        user.setPassword("123456");
        //重新保存,更新记录
        return userRepository.save(user);
    }


}

再次启动程序,查看数据库记录,可以看到数据库并没有插入数据,是因为添加了事务管理,在发生异常后进行了回滚,新的记录被撤销了。