Spring Boot中处理简单的事务

说到事务,我们第一影响应该是数据库管理系统的一个重要概念。

事务(Transaction)是数据库管理系统(DBMS)中的一个概念,用于管理对数据库的一组操作,这些操作要么全部成功执行,要么全部回滚(撤销)。

事务通常由一系列数据库操作组成,例如插入、更新、删除等。这些操作被视为一个逻辑上的单元,要么全部执行成功,要么全部不执行。事务具有以下四个特性,通常被称为 ACID 特性:

  • 原子性(Atomicity) :事务被视为一个不可分割的原子操作,要么全部执行成功,要么全部回滚。如果事务中的任何一个操作失败,整个事务将被回滚到初始状态,数据库不会受到部分操作的影响。
  • 一致性(Consistency) :事务在执行前和执行后,数据库的完整性约束没有被破坏。这意味着事务必须确保数据库从一个一致的状态转换到另一个一致的状态。
  • 隔离性(Isolation) :并发执行的多个事务之间应该相互隔离,每个事务的操作应该与其他事务的操作相互独立。隔离性确保了每个事务在并发执行时不会相互干扰,避免了数据的不一致性。
  • 持久性(Durability) :一旦事务提交成功,其所做的修改将永久保存在数据库中,即使系统发生故障或重启,修改的数据也不会丢失。

事务的使用可以确保数据库操作的一致性和可靠性,尤其在并发访问数据库的环境中,事务的隔离性能够避免数据冲突和并发问题。

那么我们在Spring Boot中如果实现对简单事务的处理呢?

那就随我一同来通过Spring Boot来简单实现一个事务吧:

引入相关依赖:

xml 复制代码
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

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

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

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

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
		</dependency>

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

在Spring Boot的相关配置文件中配置数据库:

properties 复制代码
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.datasource.primary.username=root
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=create
java 复制代码
@Entity
//@Data
//@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    @Max(50)
    private Integer age;

    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;
    }

    public User() {
    }
}

创建一个承载SQL的接口:

java 复制代码
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);

}

最后创建一个测试类:

java 复制代码
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @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("HHH", 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());

    }

}

因为我们在实体类中通过@Max注解为User的age设置了最大值为50,这样就可以通过创建User实体的age属性超过50的时候就可以触发异常,也就是我在上边测试类的执行年龄大于50岁的数据都会被终止:

java 复制代码
HHH000346: Error during managed flush [Validation failed for classes [com.miaow.demo.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
	ConstraintViolationImpl{interpolatedMessage='最大不能超过50', propertyPath=age, rootBeanClass=class com.miaow.demo.User, messageTemplate='{javax.validation.constraints.Max.message}'}
]]

如果我们去查数据库表会发现只存在包含50岁以下的所有数据,其他数据就添加失败了。

这个时候我们就需要进行事务处理了,因为他插入了50岁以前的,但是50岁以后的都没了,这样就不满足事务的原子性了,要么都做要么都不做,我们可以通过@Transactional来实现,之后我们在瞅瞅数据库表中数据,我们发现在没加@Transactional的时候,他会执行一部分,另一部分不执行,也就是不满足我们事务中的原子性,在我们加了@Transactional当我们执行收到阻碍的时候,或者被异常终止的时候,会使用@Rollback注解让我们的相关类都可以在结束的时候得到回滚,也就是,这件事情,要么做,要做就要做成功,要么不做。

事务的隔离级别

说到了事务,那么我们需要提及的是隔离级别,事务的隔离级别是我们的数据库管理系统中用来控制并访问数据的一种机制,他定义了事务在同时访问数据库的时候,对其他事务的影响程度和可以见性的一种规则。

我们常见的隔离级别:

  • 读未提交(Read Uncommitted) :最低的隔离级别,允许一个事务读取另一个事务未提交的数据。可能会导致脏读(Dirty Read)问题。
  • 读已提交(Read Committed) :保证一个事务只能读取到已经提交的数据。避免了脏读问题,但可能会导致不可重复读(Non-repeatable Read)问题。
  • 可重复读(Repeatable Read)保证一个事务在执行期间多次读取同样的数据时,能够得到一致的结果避免了不可重复读问题,但可能会导致幻读(Phantom Read)问题
  • 串行化(Serializable)最高的隔离级别 ,通过强制事务串行执行来避免并发问题。可以避免脏读、不可重复读和幻读问题,但会降低并发性能
java 复制代码
package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

/**
 * Enumeration that represents transaction isolation levels for use
 * with the {@link Transactional} annotation, corresponding to the
 * {@link TransactionDefinition} interface.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 */
public enum Isolation {

	/**
	 * Use the default isolation level of the underlying datastore.
	 * All other levels correspond to the JDBC isolation levels.
	 * @see java.sql.Connection
	 */
	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

	/**
	 * A constant indicating that dirty reads, non-repeatable reads and phantom reads
	 * can occur. This level allows a row changed by one transaction to be read by
	 * another transaction before any changes in that row have been committed
	 * (a "dirty read"). If any of the changes are rolled back, the second
	 * transaction will have retrieved an invalid row.
	 * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED
	 */
	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

	/**
	 * A constant indicating that dirty reads are prevented; non-repeatable reads
	 * and phantom reads can occur. This level only prohibits a transaction
	 * from reading a row with uncommitted changes in it.
	 * @see java.sql.Connection#TRANSACTION_READ_COMMITTED
	 */
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

	/**
	 * A constant indicating that dirty reads and non-repeatable reads are
	 * prevented; phantom reads can occur. This level prohibits a transaction
	 * from reading a row with uncommitted changes in it, and it also prohibits
	 * the situation where one transaction reads a row, a second transaction
	 * alters the row, and the first transaction rereads the row, getting
	 * different values the second time (a "non-repeatable read").
	 * @see java.sql.Connection#TRANSACTION_REPEATABLE_READ
	 */
	REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

	/**
	 * A constant indicating that dirty reads, non-repeatable reads and phantom
	 * reads are prevented. This level includes the prohibitions in
	 * {@code ISOLATION_REPEATABLE_READ} and further prohibits the situation
	 * where one transaction reads all rows that satisfy a {@code WHERE}
	 * condition, a second transaction inserts a row that satisfies that
	 * {@code WHERE} condition, and the first transaction rereads for the
	 * same condition, retrieving the additional "phantom" row in the second read.
	 * @see java.sql.Connection#TRANSACTION_SERIALIZABLE
	 */
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);


	private final int value;


	Isolation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}
}

针对这个枚举类我们来看一下,我们的Spring Boot的Transaction 中定义的5个表示隔离级别值:

java 复制代码
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:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

传播行为

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

java 复制代码
package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

/**
 * Enumeration that represents transaction propagation behaviors for use
 * with the {@link Transactional} annotation, corresponding to the
 * {@link TransactionDefinition} interface.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 */
public enum Propagation {

	/**
	 * Support a current transaction, create a new one if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>This is the default setting of a transaction annotation.
	 */
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

	/**
	 * Support a current transaction, execute non-transactionally if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>Note: For transaction managers with transaction synchronization,
	 * PROPAGATION_SUPPORTS is slightly different from no transaction at all,
	 * as it defines a transaction scope that synchronization will apply for.
	 * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
	 * will be shared for the entire specified scope. Note that this depends on
	 * the actual synchronization configuration of the transaction manager.
	 * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
	 */
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

	/**
	 * Support a current transaction, throw an exception if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

	/**
	 * Create a new transaction, and suspend the current transaction if one exists.
	 * Analogous to the EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 * on all transaction managers. This in particular applies to
	 * {@link org.springframework.transaction.jta.JtaTransactionManager},
	 * which requires the {@code javax.transaction.TransactionManager} to be
	 * made available to it (which is server-specific in standard Java EE).
	 * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
	 */
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

	/**
	 * Execute non-transactionally, suspend the current transaction if one exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 * on all transaction managers. This in particular applies to
	 * {@link org.springframework.transaction.jta.JtaTransactionManager},
	 * which requires the {@code javax.transaction.TransactionManager} to be
	 * made available to it (which is server-specific in standard Java EE).
	 * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
	 */
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

	/**
	 * Execute non-transactionally, throw an exception if a transaction exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */
	NEVER(TransactionDefinition.PROPAGATION_NEVER),

	/**
	 * Execute within a nested transaction if a current transaction exists,
	 * behave like PROPAGATION_REQUIRED else. There is no analogous feature in EJB.
	 * <p>Note: Actual creation of a nested transaction will only work on specific
	 * transaction managers. Out of the box, this only applies to the JDBC
	 * DataSourceTransactionManager when working on a JDBC 3.0 driver.
	 * Some JTA providers might support nested transactions as well.
	 * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
	 */
	NESTED(TransactionDefinition.PROPAGATION_NESTED);


	private final int value;


	Propagation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}

}

事务传播行为是指在多个事务之间进行操作时,事务的行为方式。常见的事务传播行为包括:

  • REQUIRED:如果当前存在事务,则加入该事务,如果没有事务,则创建一个新的事务。这是默认的传播行为。
  • REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将其挂起。
  • SUPPORTS:如果当前存在事务,则加入该事务,如果没有事务,则以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则将其挂起。
  • MANDATORY:如果当前存在事务,则加入该事务,如果没有事务,则抛出异常。
  • NEVER:以非事务方式执行操作,如果当前存在事务,则抛出异常。
  • NESTED:如果当前存在事务,则在嵌套事务中执行。嵌套事务是外部事务的一部分,可以独立提交或回滚,但是如果外部事务回滚,则嵌套事务也会回滚。

事务传播行为可以根据具体的业务需求来选择,以确保事务的一致性和可靠性。不同的传播行为可以在多个事务之间提供灵活的控制和管理。

指定方法:通过使用propagation属性设置,例如:

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
相关推荐
程序员小凯1 小时前
Spring Boot性能优化详解
spring boot·后端·性能优化
tuine1 小时前
SpringBoot使用LocalDate接收参数解析问题
java·spring boot·后端
番茄Salad2 小时前
Spring Boot项目中Maven引入依赖常见报错问题解决
spring boot·后端·maven
摇滚侠3 小时前
Spring Boot 3零基础教程,yml配置文件,笔记13
spring boot·redis·笔记
!if3 小时前
springboot mybatisplus 配置SQL日志,但是没有日志输出
spring boot·sql·mybatis
阿挥的编程日记4 小时前
基于SpringBoot的影评管理系统
java·spring boot·后端
java坤坤4 小时前
Spring Boot 集成 SpringDoc OpenAPI(Swagger)实战:从配置到接口文档落地
java·spring boot·后端
摇滚侠5 小时前
Spring Boot 3零基础教程,整合Redis,笔记12
spring boot·redis·笔记
荣淘淘5 小时前
互联网大厂Java求职面试全景实战解析(涵盖Spring Boot、微服务及云原生技术)
java·spring boot·redis·jwt·cloud native·microservices·interview
吃饭最爱5 小时前
spring高级知识概览
spring boot