MySQL的隔离级别

MySQL中的事务

1、简单介绍

百度百科介绍:事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transactionend transaction语句(或函数调用)来界定。

事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。

大白话总结:事务中可能存在多个操作,要保证这些操作要么全部执行成功,要么全部执行失败。不允许存在其中某一个操作执行成功,另外几个执行失败的情况。

举个例子:

A操作:插入数据;

B操作:查询数据;

C操作:更改数据;

在事务情况下:需要保证A、B、C操作全部执行完成。

列举个场景:在事务开启前提下,A操作执行完成了,但是BC操作执行失败了。那么这种操作是不符合事务原则的。

2、事务操作本质

因为事务操作只会针对于DML操作语句,也就是增删改操作才会生效,不会对查询操作生效。

这里再次来列举一个之前写的例子:

业务场景下操作如下所示:需要先进行一条Insert语句,然后进行一条update语句,最后执行一条delete语句

  • 第一步:开启事务;
  • 第二步:进行insert语句,这条语句执行成功之后,并不是立即就修改掉硬盘中的数据,而是保存中数据库的操作历史中去;
  • 第三步:执行update语句,这条语句执行成功之后,并不是立即就修改掉硬盘中的数据,而是保存中数据库的操作历史中去;
  • 第四步:执行delete语句,这条语句执行成功之后,并不是立即就修改掉硬盘中的数据,而是保存到数据库的操作历史中去;
  • 第五步:只有上面的三条DML语句都成功的时候,这个时候提交事务之后,才会真正的修改掉硬盘中的数据,并将操作历史中的文件进行删除;

注意:如果要是有一条DML语句没有成功,那么将会回滚。最终的结果就是,不会去修改掉磁盘中的数据,同时也会将这些操作历史进行删除。不管是commit还是rollback,都会将事务进行关闭;而commit比rollback多做了一步,那就是修改硬盘中的数据。

那么这里的操作历史又是什么?其实这里就是一个日志记录而已,后续学习MySQL中会来继续来进行介绍。

可以在事务中进行设置保存点,savePoint,在进行rollback的时候,可以设置回到哪个保存点,但是保存点这块很少使用。

3、MySQL事务

MySQL事务又被称之为本地事务。

3.1、事务的四大特性

MySQL的四大特性ACID

  • A:Atomicity,原子性。事务是最小的工作单元,不可再进行分离。一组SQL组合起来,要么全部成功,要么全部失败;
  • C:Consistency,一致性。事务要么是成功的(commit),要么是失败的(rollback)。在DML语句中,会导致数据的状态从一个一致性状态转换到另外一个一致性状态上去。一致性是和原子性是密切相关的。
  • I:Isolation:隔离性。事务A和事务B具有隔离性,也是为了保证数据的安全。A干自己的事情,B干自己的事情。不同的事务各自执行各自的操作,可能会造成什么影响?事务A的操作被事务B的操作给覆盖掉,虽然最终一致性是有的,但是相对来说,对于事务A和B来说,是不符合一致性的。加入说事务A和事务B是同时读到一个一致性状态,那么事务A操作完成之后,达到了一个新的一致性状态,那么对于事务B来说,应该是从这个新的状态开始的,然后达到另外一个新的状态。所以在隔离性这块会有四种情况,在下面会来进行介绍。
  • D:Durability:持久性。数据最终必须持久化到硬盘文件中去。保证数据是不会丢失的。

3.2、事务的隔离级别

隔离级别 不同数据库默认 说明 存在问题 备注
读未提交 对方事务还没有提交,当前事务可以读取到对方未提交的数据 脏读
读已提交 Oracle 到了其它事务提交到的数据。对方已提交,读取的就是新的数据,但是无法保证当前读取的数据是最新的。因为在不同的时间节点中,读取数据库中的记录条数是不固定的,一会儿是10条,一会儿是8条,一会儿是100条。也就是说在多个事务之下,对于当前事务来说,其他事务操作完成之后的结果,当前操作中都可以读取到最新的。对于当前事务来说,读取操作的不是固定不变的,而是动态变化的。 不可重复读
可重复读 MySQL 在进行select的时候,对于同一个SQL语句来说,要求读取到的结果必须是相同的。那么也就意味着对于当前事务的操作中,感知不到其他事务对数据的操作,那么也就是意味着即使其他事务删除或者是更新了数据库中的记录来说,当前事务是不可见的。所以给当前事务操作造成了一种假象,好像数据库中是没有变化的。 幻读 RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),因此不存在幻读现象。 但是标准的RR只能保证在同一事务中多次读取同样记录的结果是一致的,而无法解决幻读问题。InnoDB的幻读解决是依靠MVCC的实现机制做到的。
串读 事务操作一个一个的执行,事务排队执行 没问题,但是效率低

所以对于上面的隔离级别来说,读未提交--->读已提交--->可重复读--->串读,逐层递进,下一级解决了上一级出现的问题,但是随着而来的又是新的问题。

下面来比较一下:

隔离级别 隔离级别 比较
读未提交 读已提交 脏读是某一事物读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据
读已提交 可重复读 幻读和不可重复读都是读取了另一条已经提交的事务(脏读不是这样),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)

那么这里思考一下为什么MySQL中或者是Spring中使用的都是默认的操作方式?

在我看来,在事务的操作中,在可重复读的隔离级别下,增删改操作可能会对数据来造成影响。比如说删除了一条数据库记录条数来说,其他的事务准备要来执行修改操作?那么这个时候将会造成当前事务操作不成功的情况。

4、事务演示

MySQL中的事务默认是开启且自动提交的,也就是执行任意一条DML语句,那么自动进行提交,所以要进行演示的时候,需要手动的进行关闭。这里需要使用到TCL语句:commit和rollback

4.1、手动开启事务

所以在演示之前,首先需要做到的操作是:手动开启事务,提交事务的时候要手动提交事务

start transaction;

或者使用:

begin;

使用start transaction或者是begin来关闭自动提交事务的机制,标志着一个事务开启了。

4.2、创建数据库表

drop table if exists t_user;
create table t_user(
	id int PRIMARY key auto_increment,
	username varchar(200)
);

4.3、测试事务

开始事务

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

插入数据

mysql> insert into t_user (name) values ('guang');
ERROR 1054 (42S22): Unknown column 'name' in 'field list'
mysql> insert into t_user (username) values ('guang');
Query OK, 1 row affected (0.00 sec)

在当前事务中来查询

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  1 |   guang  |
+----+----------+
1 row in set (0.00 sec)

然后回滚事务后再次来进行查询

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

mysql> select * from t_user;
Empty set (0.00 sec)

这里也就是说使用回滚操作之后,将原来插入在日志中的记录给删除了,而不是真正的将操作的数据写入到磁盘中来。然后再次从磁盘中来进行查询的时候,发现磁盘中是不存在记录的,所以显示为空。

如果是自动提交的情况下,那么会将插入到日志中的记录直接同步到磁盘中来。

5、MySQL事务隔离级别命令行演示

演示这种操作的时候需要首先来设置隔离级别,因为MySQL中默认的隔离级别是可重复读

不妨来查看一下此时当前数据库的隔离级别:

mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+
1 row in set (0.00 sec)

那么为了来演示对应的不同的隔离级别,需要来设置当前数据库的隔离级别:

当然设置隔离级别之后,表示的是针对于当前这条数据库连接的隔离级别是设置的隔离级别,而并不代表着其他的数据库隔离级别也是相同的隔离级别。

注意:所以如果使用了数据库连接池的情况下,对于某一条数据库连接来说,是设置了隔离级别的,那么在将连接进行归还的时候,还需要将当前的数据库连接的隔离级别进行恢复。

5.1、读未提交

首先设置一下读未提交的隔离级别:

mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

然后关闭掉黑窗口之后再打开来查询一下是否开启读未提交的隔离级别是否成功:

mysql> select @@global.transaction_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| READ-UNCOMMITTED      |
+-----------------------+
1 row in set (0.00 sec)

开启两个窗口,代表的是两个数据库连接,表示开启了两个事务。

窗口一:开启事务A

mysql> start tranction;
mysql> insert into t_user (username) values ('meng');
Query OK, 1 row affected (0.00 sec)

窗口二:开启事务B

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

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  2 | meng     |
+----+----------+
1 row in set (0.00 sec)

在事务A中开启事务后,然后插入一条对应的数据,然后开启事务B,执行查询操作,可以看到查询出来的值为事务A插入进去的值;

5.2、读已提交

首先设置一下读未提交的隔离级别:

mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

然后关闭掉黑窗口之后再打开来查询一下是否开启读未提交的隔离级别是否成功:

mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-COMMITTED                 |
+--------------------------------+
1 row in set (0.00 sec)

开启两个窗口,代表的是两个数据库连接,表示开启了两个事务。

窗口一:开启事务A

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

mysql> insert into t_user (username) values ('meng');
Query OK, 1 row affected (0.00 sec)

窗口二:开启事务B

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

mysql> select * from t_user;
Empty set (0.00 sec)

首先从这里看到,这里至少是已经将脏读的问题解决了。但是随之而来的是不可重复读,因为当前事务可以读取得到其他事务已经提交的数据。

那么在窗口二,事务B的窗口中执行插入操作之后提交事务:

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

mysql> select * from t_user;
Empty set (0.00 sec)

mysql> insert into t_user (username) values ('xiang');
Query OK, 1 row affected (0.00 sec)

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

然后在窗口一,事务A的窗口中执行来执行查询操作:

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

mysql> insert into t_user (username) values ('meng');
Query OK, 1 row affected (0.00 sec)

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  3 | meng     |
|  4 | xiang    |
+----+----------+
2 rows in set (0.00 sec)

可以发现事务A读取得到了其他事务已经提交的结果。那么如果再有其他的事务来提交事务,那么如果事务A再次进行查询的时候将会有不同的结果产生。

5.3、可重复读

首先设置一下读未提交的隔离级别:

mysql> set global transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

然后关闭掉黑窗口之后再打开来查询一下是否开启读未提交的隔离级别是否成功:

mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-COMMITTED                 |
+--------------------------------+
1 row in set (0.00 sec)

其实这里也可以不需要来进行设置隔离级别,因为这里默认的就是读未提交。

开启两个窗口,代表的是两个数据库连接,表示开启了两个事务。

窗口一:开启事务A

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  5 | lig      |
|  6 | lim      |
|  7 | lij      |
+----+----------+
3 rows in set (0.00 sec)

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

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  5 | lig      |
|  6 | lim      |
|  7 | lij      |
+----+----------+
3 rows in set (0.00 sec)

mysql> delete from t_user where id = 7;
Query OK, 1 row affected (0.00 sec)

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  5 | lig      |
|  6 | lim      |
+----+----------+
2 rows in set (0.00 sec)

在窗口一中,开启事务后来进行查询,然后将数据库中的数据id=7的给删除掉,此时如果没有来进行提交的话,在窗口B中来进行操作

窗口二:开启事务B

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  5 | lig      |
|  6 | lim      |
|  7 | lij      |
+----+----------+
3 rows in set (0.00 sec)

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

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  5 | lig      |
|  6 | lim      |
|  7 | lij      |
+----+----------+
3 rows in set (0.00 sec)

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  5 | lig      |
|  6 | lim      |
|  7 | lij      |
+----+----------+
3 rows in set (0.00 sec)

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  5 | lig      |
|  6 | lim      |
|  7 | lij      |
+----+----------+
3 rows in set (0.00 sec)

在开启了事务提交下,对于事务A来说,尽管已经删除了id=7的用户,但是对于开启了事务的B来说,是没有感知到已经将数据给删除了的。

但是只要事务A将事务给提交了之后,那么事务B将会感知到变化了,那么再次来进行查询的时候,会得到最新的数据。不是说好的幻读吗?

那么在窗口二,事务B的窗口中执行插入操作之后提交事务:

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

mysql> select * from t_user;
Empty set (0.00 sec)

mysql> insert into t_user (username) values ('xiang');
Query OK, 1 row affected (0.00 sec)

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

然后在窗口一,事务A的窗口中执行来执行查询操作:

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

mysql> insert into t_user (username) values ('meng');
Query OK, 1 row affected (0.00 sec)

mysql> select * from t_user;
+----+----------+
| id | username |
+----+----------+
|  3 | meng     |
|  4 | xiang    |
+----+----------+
2 rows in set (0.00 sec)

可以发现事务A读取得到了其他事务已经提交的结果。那么如果再有其他的事务来提交事务,那么如果事务A再次进行查询的时候将会有不同的结果产生。这就让使用者产生了一种幻觉,但是这条数据在当前事务中是不应该出现的。

5.4、串读

首先设置一下读未提交的隔离级别:

mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

然后关闭掉黑窗口之后再打开来查询一下是否开启读未提交的隔离级别是否成功:

mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| SERIALIZABLE                   |
+--------------------------------+
1 row in set (0.00 sec)

开启两个窗口,代表的是两个数据库连接,表示开启了两个事务。

窗口一:开启事务A

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

mysql> insert into t_user (username) values ('zj');
Query OK, 1 row affected (0.00 sec)

窗口二:开启事务B

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

mysql> select * from t_user;

这里可以看到只要事务A中不进行提交或者是回滚,那么事务B将会一直处于等待状态,直到等待事务A提交回滚或者是提交。

6、Java操作事务

在MySQL阶段,可以使用命令行来进行操作,而在JDBC中,java中接口Connection也提供了开启事务和关闭事务的API

Connection中与事务有关的方法 说明
setAutoCommit(boolean autoCommit) 参数是true或false 如果设置为false,表示关闭自动提交,相当于开启事务; 类似sql里面的 start transaction;
void commit() 提交事务; 类似sql里面的 commit;
void rollback() 回滚事务; 类似sql里面的 rollback;

MySQL中也提供了变量autocommit来进行操作。

对于MySQL中使用InnoDB执行引擎条件下,事务的开启和提交模型无非以下两种情况:

如果设置了autocommit=0,那么就相当于手动在命令行窗口中执行了start transaction命令,表示的开始事务。在操作完成之后,执行了commit命令之后,本次操作从设置autocommit=0开始到commit这一阶段是一个完整的事务周期;

如果设置了autocommit=1(系统默认值),事务的开启和提交又分为了两种状态:

1、手动提交:autocommit=0,表示开启事务,如果此时没有执行commit命令,系统默认事务回滚;

2、

6.1、手动提交

如果设置了autocommit=0,那么就相当于手动在命令行窗口中执行了start transaction命令,表示的开始事务。在操作完成之后,执行了commit命令之后,本次操作从设置autocommit=0开始到commit这一阶段是一个完整的事务周期;若不执行commit命令,系统则默认事务回滚。

6.2、自动提交

自动提交:如果设置了autocommit=1(数据库默认情况下),如果用户在当前情况下(参数autocommit=1)未执行start transaction命令而对数据库进行了操作,系统则默认用户对数据库的每一个操作为一个孤立的事务,也就是说用户每进行一次操作系都会即时提交或者即时回滚。这种情况下用户的每一个操作都是一个完整的事务周期。

而我们通常操作数据库的时候,使用的就是第二种方式。因为默认值是自动提交,然后手动执行start transaction命令开启了事务操作,那么对数据库中的每个操作就不再是独立的事务操作了,直到最终进行commit或者是rollback操作。

6.3、设置参数的值

设置autocommit开启和关闭的方法:用户可以将自动提交功能强制置为OFF。这样用户执行SQL语句后将不会被提交了,而执行COMMIT命令才提交,执行ROLLBACK命令回滚。

查看当前自动提交功能状况:

show variables like 'autocommit';  
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.06 sec)

临时生效设置方法:
下面是将自动提交功能置为ON以及置为OFF的具体语法。
将自动提交功能置为ON

SET AUTOCOMMIT=1;

将自动提交功能置为OFF

SET AUTOCOMMIT=0;

永久生效设置方法:
通过修改配置文件my.cnf文件,通过vim编辑my.cnf文件,在[mysqld](服务器选项下)添加:

autocommit=0

参考博客:https://www.cnblogs.com/deverz/p/6547866.html

posted @ 2021-12-20 01:25  写的代码很烂  阅读(75)  评论(0编辑  收藏  举报