03mysql事务隔离级别及验证

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务最经典也经常被拿出来说例子就是转账了。需要保证转出与转入都会成功。

1、常用术语

  • 事务(transaction)指一组SQL语句
  • 回退(rollback)撤销指定SQL语句的过程
  • 提交(commit)将未存储的SQL语句结果写入数据库中
  • 保留点(savepoint)指事务处理中设置的临时占位符,你可以对他发布回退。

2、控制事务处理

  • STRAT TRANSACTION
  • ROLLBACK
  • COMMIT
  • SAVEPOINT delete_user_1
  • ROLLBACK TO delete_user_1

3、更改默认的提交行为

MySQL默认采用自动提交(AUTOCOMMIT)模式。也就是说,如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作。

AUTOCOMMIT变量:在当前连接中,可以通过设置AUTOCOMMIT变量来启用或者禁用自动提交模式
1表示启用,0表示禁用:此模式下,所有的查询都是在一个事务中,直到显式地执行COMMIT提交或者ROLLBACK回滚,该事务结束,同时又开始了另一个新事务。

show variables like 'autocommit';

set autocommit=0;

4、关系性数据库需要遵循ACID规则,具体内容如下:

  • 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  • 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

5、什么是脏读?幻读?不可重复读?

  • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

6、什么是事务的隔离级别?MySQL的默认隔离级别是什么?

为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
    • 串行化下事物加的是行锁而不是表锁
    • 通常情况下,写会阻塞读,但是在autocommit=1的情况下,单条select语句并不会阻塞,而在begin或者start transaction中就会阻塞。(这里单条select语句没有阻塞,说明读事物不一定会被写阻塞。而在事物语句中的select语句产生了阻塞,说明start transaction的查询事物跟单条select事务还是有区别)

这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别

7、测试MySQL的隔离级别

首先需要知道几个关于事务的SQL语句

select @@session.transaction_isolation;
select @@transaction_isolation;
SET SESSION TRANSACTION ISOLATION LEVEL read uncommitted;  
SET SESSION TRANSACTION ISOLATION LEVEL read committed;  
SET SESSION TRANSACTION ISOLATION LEVEL repeatable read;  
SET SESSION TRANSACTION ISOLATION LEVEL serializable;
DROP TABLE IF EXISTS `tb_account`;
CREATE TABLE `tb_account` (
  `account_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '账户ID',
  `account_num` int(11) NOT NULL DEFAULT '0' COMMENT '账户金额',
  PRIMARY KEY (`account_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='账户表';
--准备数据
insert into tb_account(account_id,account_num)values
(null,100);

7.1 将隔离级别设置为read uncommitted(读未提交)

A:启动mysql窗口A,设置事务为读未提交

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

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

mysql> select * from tb_account;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+

B:启动mysql窗口B,启动事务,更新数据,但不提交

mysql> select * from tb_account;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+
1 row in set (0.00 sec)

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

mysql> update tb_account set account_num = 200 where account_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         200 |
+------------+-------------+

A:再次读取数据,发现数据已经被修改了。当B窗口把事务回滚,出现脏读

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         200 |
+------------+-------------+

B:回滚事务

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

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+

A:再次读数据,发现数据变回初始状态

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+

经过上面的实验可以得出结论,事务B更新了一条记录,但是没有提交,此时事务A可以查询出未提交记录。造成脏读现象。未提交读是最低的隔离级别。

7.2将隔离级别设置为read committed(读已提交)验证不可重复度

此事务隔离级别可以解决脏读问题,但是无法解决不可重复度与幻读问题。

A:启动mysql窗口A,设置事务为读已提交

mysql> SET SESSION TRANSACTION ISOLATION LEVEL read committed;
Query OK, 0 rows affected (0.00 sec)

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

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

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+

B:启动事务,更新数据,但不提交

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

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+
1 row in set (0.00 sec)

mysql> update tb_account set account_num = 200 where account_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         200 |
+------------+-------------+

A:再次读数据,发现数据未被修改

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+

B:提交事务

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

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         200 |
+------------+-------------+

A:再次读取数据,发现数据已发生变化,说明B提交的修改被事务中的A读到了。但是同一事务中两次读取的结果不一样,这就是所谓的“不可重复读”

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         200 |
+------------+-------------+

经过上面的实验可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。已提交读只允许读取已提交的记录,但不要求可重复读。

7.3将隔离级别设置为repeatable read(可重复读)

A:启动mysql窗口A,设置事务为可重复读

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

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

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         200 |
+------------+-------------+

B:启动事务,更新数据并提交数据

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

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         200 |
+------------+-------------+
1 row in set (0.00 sec)

mysql> update tb_account set account_num = 100 where account_id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+
1 row in set (0.00 sec)

mysql> commit;

A:再次读取数据,发现数据依然未发生变化,这说明这次可以重复读了

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         200 |
+------------+-------------+

A:提交本次事务,再次读取数据,发现读取正常了

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

mysql> select * from tb_account where account_id = 1;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
+------------+-------------+

在一个事务中两次读取一个记录期间,两次读取的数据是一样的。即使其他事务对该数据进行了更新。

7.4幻读操作实例

A:开启事务,查询当前存在的数据,一共4条数据

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

mysql> select * from tb_account;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
|          2 |         200 |
|          3 |         300 |
|          4 |         400 |
+------------+-------------+

B:开启事务,查询当前存在的数据,一共4条数据。插入一条新的数据。并提交记录。

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

mysql> select * from tb_account;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
|          2 |         200 |
|          3 |         300 |
|          4 |         400 |
+------------+-------------+
4 rows in set (0.00 sec)

mysql> insert into tb_account(account_id,account_num)values(5,500);
Query OK, 1 row affected (0.00 sec)

mysql> select * from tb_account;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
|          2 |         200 |
|          3 |         300 |
|          4 |         400 |
|          5 |         500 |
+------------+-------------+
5 rows in set (0.00 sec)

mysql> commit;

A:查询发现没有记录5,现在开始进行插入5。此时插入报错,没有的记录提示我已经存在。此时存在幻读的情况。

mysql> insert into tb_account(account_id,account_num)values(5,500);
ERROR 1062 (23000): Duplicate entry '5' for key 'PRIMARY'
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
---事务提交后,查询是发现确实已经存在。
mysql> select * from tb_account;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
|          2 |         200 |
|          3 |         300 |
|          4 |         400 |
|          5 |         500 |
+------------+-------------+

7.5将隔离级别设置为可串行化(Serializable)

A:设置事务级别,启动事务

mysql> SET SESSION TRANSACTION ISOLATION LEVEL serializable;
Query OK, 0 rows affected (0.00 sec)

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

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

mysql> select * from tb_account;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
|          2 |         200 |
|          3 |         300 |
|          4 |         400 |
|          5 |         500 |
+------------+-------------+

B:开启事务,插入一条数据。发现B此时进入了等待状态,原因是因为A的事务尚未提交,只能等待(此时,B可能会发生等待超时)

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

mysql> select * from tb_account;
+------------+-------------+
| account_id | account_num |
+------------+-------------+
|          1 |         100 |
|          2 |         200 |
|          3 |         300 |
|          4 |         400 |
|          5 |         500 |
+------------+-------------+
5 rows in set (0.00 sec)

mysql> insert into tb_account(account_id,account_num)values(6,600);

A:提交事务

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

B:数据插入成功,注意所用的时间

mysql> insert into tb_account(account_id,account_num)values(6,600);
Query OK, 1 row affected (37.87 sec)

serializable完全锁定字段,若一个事务来查询同一份数据就必须等待,直到前一个事务完成并解除锁定为止。是完整的隔离级别,会锁定对应的数据表格,因而会有效率的问题。

你可能感兴趣的:(03mysql事务隔离级别及验证)