mybatis 源码系列(七) Java基础之数据库事务隔离级别

2019/05/18 mybatis 浏览

正确设置数据库的事务访问级别,有助于我们的应用程序达到预期的效果

在mybatis中,提供了事务隔离级别的枚举类:org.apache.ibatis.session.TransactionIsolationLevel.java

来看具体代码:

/**
 * @author Clinton Begin
 */
public enum TransactionIsolationLevel {
  NONE(Connection.TRANSACTION_NONE),
  READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),
  READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),
  REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),
  SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);

  private final int level;

  private TransactionIsolationLevel(int level) {
    this.level = level;
  }

  public int getLevel() {
    return level;
  }
}

从代码中,我们看到,mybatis维护了一份Connection连接的事务隔离级别枚举类,作用仅仅是简化变量,方便程序调用.

那么,其中几个事务隔离级别具体代表什么意思呢?

英文名称 中文说明  
READ_COMMITTED 禁止脏读,允许不可重复读和幻读,此级别仅禁止事务读取具有未提交更改的行。 2
READ_UNCOMMITTED 允许脏读,不可重复读和幻读,此级别允许在提交该行中的任何更改(“脏读”)之前,由另一个事务读取由一个事务更改的行,如果回滚任何更改,则第二个事务将检索到无效行。 1
REPEATABLE_READ 禁止脏读和不可重复读,允许幻读,此级别禁止事务读取具有未提交更改的行,并且还禁止一个事务读取行,第二个事务更改行,第一个事务重新读取行,第二次获取不同值的情况( “不可重复读”)。 4
SERIALIZABLE 事务最高隔离级别,禁止脏读、幻读和不可重复读 8

看了Java中JDK的注释,我们首先需要明白何为脏读、不可重复读及幻读

何为事务

在理解事务隔离级别之前,我们需要知道事务是什么,有什么作用?

当应用程序被许多用户访问获取数据信息时,或者一个用户发出了多次请求时,为使用户获取的数据是完整的是非常重要的事情,而如何保证数据完整性在数据库中称为事务

为确保数据完整性,事务需要遵循四个条件:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),也就是我们通常所说的ACID

可以查阅mariadb的数据库理论文档,充分了解事务

题外话,最近在写博客温习这些知识的期间,有些不明白的还是会在网上查询资料,加深自己的理解,但我发现国内的很多篇幅都介绍的很片面,所以我建议大家都读英文文章,特别是官方文档,就算是一个单词一个单词的啃,对自己理解这个知识点会深刻许多,再结合自己工作中学到的,会事半功倍.

原子性(Atomicity)

我们都知道,原子是原子是元素中的最小单元

那么在事务中,我们把他理解为一个操作要么成功,要么失败,除了这两种,没有其他情况发生.

原子性意味着整个交易必须完成。如果不是这种情况,则中止整个事务。 这可确保数据库永远不会留下部分完成的事务,从而导致数据完整性不佳。

例如,如果您从一个银行帐户中删除资金,但第二个请求失败且系统无法将资金存入另一个银行,则两个请求都必须失败。 这笔钱不能简单地丢失,也不能从一个帐户中取出而不会进入另一个帐户。

一致性(Consistency)

一致性是指满足某些条件时数据所处的状态。

这个我认为需要结合应用程序来说,因为数据库中的数据状态的变更,都是由我们的应用程序来修改的,数据状态从一个状态变为另外一个状态,这其中的过程是不可见的

通过满足我们的业务需求条件,最终将数据的状态设置为我们的认为正确的状态,这就是数据一致性

一致性是目的(我们希望看到的数据状态),而AID是手段

隔离性(Isolation)

隔离意味着在第一个事务完成之前,另一个事务不能使用在处理一个事务期间使用的任何数据。

例如,如果两个人将100美元存入另一个账户,余额为900美元,则第一笔交易必须加100美元至900美元,第二笔交易必须加100美元至1000美元。 如果第二笔交易在第一笔交易完成前读取900美元,那么这两笔交易似乎都会成功,但100美元将会丢失。 第二个事务必须等到它一个人访问数据。

也就是事务之间是相互隔离的

通过上面的例子,我们也有所了解到隔离性也是确保我们的数据状态的一致

持久性(Durability)

持久性是指一旦事务中的数据被提交,即使系统出现故障,其影响也将保持不变。 当交易正在进行时,效果并不持久。 如果数据库崩溃,备份将始终在事务开始之前将其还原到一致状态。 交易没有什么能够改变这个事实。

我所理解的是事务持久性即事务一旦提交,那么所影响的记录行会持久化保存在我们的磁盘上,及时业务系统崩溃,也不会影响我们的数据(如果你说磁盘蹦了那我也只能漏出尴尬而不失礼貌的微笑了)

事务隔离级别

为了得到更详细的说明,我查看了mariadb的官方文档介绍

READ UNCOMMITTED(读取未提交)

SELECT语句以非锁定方式执行,但可能会使用行的早期版本。因此,使用此隔离级别,会导致非一致性.也叫”脏读”,就好像读取到了未提交的行一样.

READ COMMITTED(读取提交)

读取提交内容,关于一致性(非锁定)读取的类似Oracle的隔离级别:即使在同一事务中,每个一致性读取使之读取到的内容都是自己的新快照

对于锁定读取(SELECT FOR FOR UPDATE或LOCK IN SHARE MODE),InnoDB仅锁定索引记录,允许在锁定记录旁边自由插入新记录,对于UPDATE和DELETE语句,锁定取决于语句是使用具有唯一搜索条件的唯一索引(例如WHERE id = 100)还是范围类型搜索条件(例如WHERE id> 100)。对于具有唯一搜索条件的唯一索引,InnoDB仅锁定找到的索引记录,而不是之前的间隙。对于范围类型搜索,InnoDB使用间隙锁或下一键(间隙加索引记录)锁来锁定扫描的索引范围,以阻止其他会话插入范围所涵盖的间隙。这是必要的,因为必须阻止“幻像行”才能使MySQL复制和恢复正常工作

REPEATABLE READ(可重读)

这是InnoDB存储引擎的默认事务隔离级别,关于一致性读取,这和READ COMMITTED事务隔离级别有很大的不同,同一事务中的所有一致读取读取第一次读取建立的快照。此约定意味着如果在同一事务中发出多个普通(非锁定)SELECT语句,则这些SELECT语句也相互一致

使用锁读取的SELECT语句(FOR UPDATE或LOCK IN SHARE MODE ),UPDATE和DELETE语句,锁定取决于语句是否使用具有唯一搜索条件的唯一索引,或范围类型的搜索条件。对于具有唯一搜索条件的唯一索引,InnoDB仅锁定找到的索引记录,而不是之前的间隙。对于其他搜索条件,InnoDB使用间隙锁或下一键(间隙加索引记录)锁来锁定扫描的索引范围,以阻止其他会话插入范围所覆盖的间隙。

这对于并发操作数据库数据获取的数据一致性是有很大的帮助.

SERIALIZABLE(可串行化)

这个级别就像REPEATABLE READ,但InnoDB隐式地将所有普通SELECT语句转换为SELECT lock …如果禁用自动提交,则锁定共享模式。

如果启用了自动提交,则SELECT是其自己的事务。因此,已知它是只读的,并且如果作为一致(非锁定)读取执行则可以序列化,并且不需要阻止其他事务。(这意味着如果其他事务已修改所选行,则强制普通SELECT阻止,您应禁用自动提交。)

分布式XA事务始终应用是该隔离级别

数据库模拟

脏读

顾名思义,在一个事务中读取到了不该读到的数据,举例来说明:

目前我们有User信息表(id,age,name),假设当前有A、B两个事务对该User表进行操作

我们要模拟脏读的场景,首先就需要先设置我们当前数据库连接的事务隔离级别,设置为允许脏读,通过上面的说明,需要设置为READ_UNCOMMITED隔离级别

先来查看mariadb中的默认隔离级别,相关的命令可以查阅官方文档

mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.06 sec)

mysql>

mysql中默认事务隔离借呗为可重复读

先设置为READ_UNCOMMITTED级别

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.04 sec)

mysql> SELECT @@tx_isolation;
+------------------+
| @@tx_isolation   |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.06 sec)

mysql> 

先看A事务开启事务,查询User信息表:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
+----+-----+-------+
| id | age | name  |
+----+-----+-------+
|  1 |  33 | 23    |
|  2 |  33 | abc   |
|  3 |  12 | ab3ec |
|  4 |  12 | ab3ec |
|  5 |  12 | ab3ec |
+----+-----+-------+
5 rows in set (0.03 sec)

此时,开启B事务,修改User表中id=1的name值,但是并不提交当前事务

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update user set name='ccccccccccccc' where id=1;
Query OK, 1 row affected (0.19 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> select * from user;
+----+-----+---------------+
| id | age | name          |
+----+-----+---------------+
|  1 |  33 | ccccccccccccc |
|  2 |  33 | abc           |
|  3 |  12 | ab3ec         |
|  4 |  12 | ab3ec         |
|  5 |  12 | ab3ec         |
+----+-----+---------------+
5 rows in set (0.11 sec)

我们在B事务中修改后,在查询User信息,发现id=1的name值已发生变化,此时我们在回到A事务中查询User信息表

mysql> select * from user;
+----+-----+---------------+
| id | age | name          |
+----+-----+---------------+
|  1 |  33 | ccccccccccccc |
|  2 |  33 | abc           |
|  3 |  12 | ab3ec         |
|  4 |  12 | ab3ec         |
|  5 |  12 | ab3ec         |
+----+-----+---------------+
5 rows in set (0.11 sec)

A事务已经读取到了B事务对记录行的修改,但是B事务并未提交,这就是所谓的”脏读”了,此时我们回滚B事务

mysql> rollback;
Query OK, 0 rows affected (0.10 sec)

再A事务中再查询User信息记录行:

mysql> select * from user;
+----+-----+-------+
| id | age | name  |
+----+-----+-------+
|  1 |  33 | 23    |
|  2 |  33 | abc   |
|  3 |  12 | ab3ec |
|  4 |  12 | ab3ec |
|  5 |  12 | ab3ec |
+----+-----+-------+
5 rows in set (0.10 sec)

发现id=1的User表name信息已经跟随B事务一起回滚掉了.

由此我们应该想到,这不是明显不对嘛,我们都知道事务要么成功,要么失败,B事务还未提交的情况下,A事务已经能读取到B事务所做的修改操作,在显示中,加入银行也存在这种操作,那绝对是不允许的,所以在实际生产环境中,READ UNCOMMITTED这一事务隔离级别需要在特定的场合下使用,一般是不能使用的

不可重复读

在同一个事务之间,两次查询的结果不一致,这有可能是在两次查询之间,另外一个事务对结果记录行做了修改导致的.

同样,我们使用A、B两个事务来进行模拟

首先,将B事务设置为READ COMMITTED隔离级别:

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.04 sec)

A、B事务同时开启事务,此时,查询User信息表的数据都是一致的

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select *from user;
+----+-----+------------+
| id | age | name       |
+----+-----+------------+
|  1 |  33 | bbbbbbbbbb |
|  2 |  33 | abc        |
|  3 |  12 | ab3ec      |
|  4 |  12 | ab3ec      |
|  5 |  12 | ab3ec      |
+----+-----+------------+
5 rows in set (0.05 sec)

此时,A事务修改user表中id=1的name属性值,但并提交

mysql> update user set name='aaaaaaaaaaaaaaaaaa' where id=1;

我们在B事务中进行查询

mysql> select *from user;
+----+-----+------------+
| id | age | name       |
+----+-----+------------+
|  1 |  33 | bbbbbbbbbb |
|  2 |  33 | abc        |
|  3 |  12 | ab3ec      |
|  4 |  12 | ab3ec      |
|  5 |  12 | ab3ec      |
+----+-----+------------+
5 rows in set (0.05 sec)

发现数据并未产生变化,此时我们提交A事务

mysql> commit;
Query OK, 0 rows affected (0.11 sec)

再在B事务中查询User表信息

mysql> select *from user;
+----+-----+--------------------+
| id | age | name               |
+----+-----+--------------------+
|  1 |  33 | aaaaaaaaaaaaaaaaaa |
|  2 |  33 | abc                |
|  3 |  12 | ab3ec              |
|  4 |  12 | ab3ec              |
|  5 |  12 | ab3ec              |
+----+-----+--------------------+
5 rows in set (0.11 sec)

此时,B事务已经读取到A事务提交的影响记录,id=1的name值已更改

可重复读

在上面说明中我们知道,同一事务中的所有一致读取读取第一次读取建立的快照

同样是A、B两个事务

首先将B事务设置为REPEATABLE READ事务隔离级别

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.06 sec)

A\B开启事务,先查询user信息表

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
+----+-----+--------------------+
| id | age | name               |
+----+-----+--------------------+
|  1 |  33 | aaaaaaaaaaaaaaaaaa |
|  2 |  33 | abc                |
|  3 |  12 | ab3ec              |
|  4 |  12 | ab3ec              |
|  5 |  12 | ab3ec              |
+----+-----+--------------------+
5 rows in set (0.09 sec)

在B事务中,修改id=1的name值为123456,并提交B事务

mysql> update user set name='123456' where id=1;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> select * from user;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  33 | 123456 |
|  2 |  33 | abc    |
|  3 |  12 | ab3ec  |
|  4 |  12 | ab3ec  |
|  5 |  12 | ab3ec  |
+----+-----+--------+
5 rows in set (0.10 sec)

mysql> commit;
Query OK, 0 rows affected (0.16 sec)

B事务未提交,在B事务查询,User信息变更,此时,我们在A事务中查询user表信息

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
+----+-----+--------------------+
| id | age | name               |
+----+-----+--------------------+
|  1 |  33 | aaaaaaaaaaaaaaaaaa |
|  2 |  33 | abc                |
|  3 |  12 | ab3ec              |
|  4 |  12 | ab3ec              |
|  5 |  12 | ab3ec              |
+----+-----+--------------------+
5 rows in set (0.09 sec)

mysql> select * from user;
+----+-----+--------------------+
| id | age | name               |
+----+-----+--------------------+
|  1 |  33 | aaaaaaaaaaaaaaaaaa |
|  2 |  33 | abc                |
|  3 |  12 | ab3ec              |
|  4 |  12 | ab3ec              |
|  5 |  12 | ab3ec              |
+----+-----+--------------------+
5 rows in set (0.11 sec)

两次读取到的记录是一样的,并未产生任何变化,这也就是和官方说明保持一致,同一事务中的所有一致性查询,都是回去的第一次查询快照.这也就是可重读.

此时,我们在提交A事务,再查询User表,发现记录已变更

mysql> select * from user;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  33 | 123456 |
|  2 |  33 | abc    |
|  3 |  12 | ab3ec  |
|  4 |  12 | ab3ec  |
|  5 |  12 | ab3ec  |
+----+-----+--------+
5 rows in set (0.13 sec)

幻读

同一个事务两次查询的数据记录行不一致,导致产生的幻影

同样是基于REPEATABLE READ的事务隔离级别

开启A\B事务,查询我们的User表信息

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  33 | 123456 |
|  2 |  33 | abc    |
|  3 |  12 | ab3ec  |
|  4 |  12 | ab3ec  |
|  5 |  12 | ab3ec  |
+----+-----+--------+
5 rows in set (0.09 sec)

我们在B事务中新增一条数据,并提交

mysql> insert into user(id,age,name) values(6,44,'add');
Query OK, 1 row affected (0.06 sec)
mysql> commit;
Query OK, 0 rows affected (0.10 sec)

此时,A事务中查询User信息表

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  33 | 123456 |
|  2 |  33 | abc    |
|  3 |  12 | ab3ec  |
|  4 |  12 | ab3ec  |
|  5 |  12 | ab3ec  |
+----+-----+--------+
5 rows in set (0.09 sec)

我们发现A事务中并未读取到B事务提交的新记录行数,这就是幻读.

提交A事务,在进行读取

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  33 | 123456 |
|  2 |  33 | abc    |
|  3 |  12 | ab3ec  |
|  4 |  12 | ab3ec  |
|  5 |  12 | ab3ec  |
|  6 |  44 | add    |
+----+-----+--------+
6 rows in set (0.11 sec)

mysql> 

此时已经读取到新的记录行了.

可重复读隔离级别只允许读取已提交记录,而且在一个事务两次读取一个记录期间,其他事务部的更新该记录。但该事务不要求与其他事务可串行化

总结

或许根据事务隔离级别的字面意思,做一个简单的总结

READ UNCOMMITTED:意思是一个事务可以读取另外一个事务(未提交)所做的操作,那么此操作给开发者所造成的影响即有可能是脏读、不可重复读、和幻读

READ COMMITTED:意思是只能读取已提交的内容,这就避免的脏读的出现,但是在一个事务操作期间,另外一个事务对记录行产生了的变化,这就导致了可以不可重复读(A事务两次读取数据不一样)和幻读

REPEATABLE READ:意思是可重复读,此级别是和READ COMMITTED一致性读取有相似点,却也有不同点,首先是不允许脏读,读取的内容都是已提交的。其二从字面意思来看也能理解,允许重复读,所以它是禁止不可重复读的,当然,幻读允许存在

SERIALIZABLE:可串行化,我是这么理解的,串行化的操作既是一个操作连接着一个操作,就是事情总有先后,所以当一个事务正在操作的时候,其他事务必须等待,等操作的事务操作完成后,其他事务即可以进行操作,我们都知道事务是原子性的,所以该级别的隔离级别不允许脏读、不可重复读、和幻读.

我们在理解了以上的基础概念后,后面再来读mybatis的事务相关代码,会让我们更轻松.

最后

我不能保证我所说的都一定是正确的,但我会确保每一个词,每一个用意都是根据自身的理解结合官方文档所总结的,如果其中任然有纰漏,欢迎同行中的朋友加以指正,我会虚心接受学习.

站内搜索

    Table of Contents