隔离级别

在具体介绍 Spring Boot 的事务管理机制前,有必要了解一下数据库在事务上是如何控制的。这是隔离级别的概念,主要应对在高并发情况下,如何保证数据的一致性的问题。然后介绍 Spring Boot 配置中的重要属性的隔离级别,以及如何使用数据库的隔离级别。

数据库的隔离级别

首先,介绍数据库的四大特性,然后对隔离级别做详细的介绍。

ACID

如果数据库支持事务,则必定存在 ACID,就是数据库的四大特性。

  • 原子性(Atomicity):事务中包含的所有业务操作被看成业务单元,要么全部执行成功,要么全部执行失败。

  • 一致性(Consistency):事务操作完成后,必须使得数据库从一种一致状态变为另一种一致状态。通俗的说法就是,两个用户互相转账,在多次交易之后,他们之间的钱加起来不变,主要是保证数据的完整性。

  • 隔离性(Isolation):在高并发的情况下,多个用户操作同一张表,数据库会为每一个用户开启一个事务,多个事务要互相隔离,不被其他用户干扰,这就是隔离性,在后面也会重点介绍。

  • 持久性(Durability):事务在结束后,即事务提交后,数据库中的数据依旧存在,是永久性的。

隔离级别

在数据库中可以对数据完全隔离,保证数据的完整性。但是在互联网中,在保证数据完整性的同时,我们还要考虑系统性能,因此数据库的隔离级别出现了四类。

如果隔离级别过高,在高并发时,系统中就会存在大量的锁,导致线程被挂起,一个时刻只能允许一个线程访问数据,只有锁释放,下一个线程才能进行访问。这造成整个系统的运行速度特别慢,用户体验变差。

这时我们就需要考虑数据库的隔离级别,四类级别分别是未提交读、读写提交、可重复读、串行化。级别越高,可靠性越好,但是并发量就会变小。四类级别如下。

1) 未提交读(READ UNCOMMITTED)。

这是隔离级别最低的级别,在多个事务中,如果对其中一个事务进行了修改,即使没有提交该事务,在其他的事务中依旧可以读取到已经修改的值。

这种级别会存在脏读,也就是指在一个事务处理过程里读取了另一个未提交的事务中的数据。如表7.1所示。

image 2024 03 31 21 16 50 171
Figure 1. 表7.1 未提交读的情况

举例说明,假设买票,剩下两张票,这时卖出去一张票,按道理应该回滚到 1 才正确,但这里回滚为 0。如果只看事务 1,应该回滚到 2,这样也不对。因此这种级别在实际中使用得不多,如果对数据的一致性要求不高,但是对并发性的要求高,可以采用这种级别。

2) 读写提交(READ COMMITTED)。

对于未提交读写,这里的事务只能读取另一个事务已经提交的数据,没有提交的数据不能被读取。这个级别虽然克服了脏读的情况,但也会存在问题,就是读取旧数据的问题,造成不可重读的现象。如表7.2所示。

image 2024 03 31 21 18 39 776
Figure 2. 表7.2 读写提交的情况

举例说明,同样是买票,假设剩下一张票,直接看事务 2,我们查询数据表发现有数据,但在减少一张进行卖票时,发现没票了。对于事务 2 来说,数据表数据是一个在变化的值,造成不能重复读的现象。

3) 可重复读(REPEATABLE READ)。

针对读写提交的情况,这个级别克服了不可重复读的问题。举例如表7.3所示。

image 2024 03 31 21 19 34 049
Figure 3. 表7.3 可重复读的情况

从表中可以看到,在提交事务 1 之后,事务 2 才能读取,这里使用了阻塞。但也存在问题,不是针对 update 与 delete,而是针对 insert。这里查询不再是同一个数据,而是同一批数据的个数。举例说明,在事务 2 中,先查询时只有 50 条数据,然后过一会再查询时,发现事务 1 提交了 5 条数据,导致此时有 55 条数据。刚才的数据就是幻读。

4) 串行化(Serializable)。

这个级别是数据库中级别最高的,所有的 SQL 都会按照顺序进行执行,可以避免出现脏读、不可重复读、幻读等问题。

Spring Boot中的隔离级别

上面已经说明了隔离级别,但具体的数据库支持不同,例如 MySQL 全部支持,而 Oracle 只支持其中的读写提交与串行化。Spring Boot 的事务支持的隔离级别,可通过一段代码来看,代码如下所示。

package org.springframework.transaction.annotation;

public enum Isolation {
   DEFAULT(-1),
   READ_UNCOMMITTED(1),
   READ_COMMITTED(2),
   REPEATABLE_READ(4),
   SERIALIZABLE(8);

   private final int value;

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

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

需要特别说明的是,DEFAULT 是默认值的意思,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这个值是 READ_COMMITTED,这里是默认值,不是第五种隔离级别,所以不会与其他四种隔离级别冲突。

关于它的使用有两种方式,注解会在后面进行说明。使用方式一,在注解上添加属性配置项,如下所示。

//插入user
@Transactional(isolation = Isolation.READ_COMMITTED)
public User insertUser(User user){
   userMapper.insertUser(user);
   return user;
}

在注解 @Transactional 上添加配置项。而 @Transaction 这个注解在7.2.1节进行说明。使用方式二,在 application.properties 中添加配置项,当然这里会全部被指定。

spring.datasource.dbcp2.default-transaction-isolation=2

上面的代码可以使用数字代表隔离级别,所以在 application.properties 中可以使用数字编写。