MySQL事务
- 事物的基本概念
- 事物的ACID属性
- 事务的使用
- 事务隔离级别
- MVCC&ReadView
- MySQL是否还存在幻读
事物的基本概念
Transaction作为关系型数据库的核心组成,在数据安全方面有着非常重要的作用,本文会一步步解析事务的核心特性,以获得对事务更深的理解。
什么是事物?博主的理解了是事物是一次和数据库连接会话当中所有的sql要么全部成功要么全部失败。
事物的ACID属性
- 原子性(Atomicity): 一个事物当中所有的操作要么全部成功要么全部失败,不会结束在某个中间环节,如果事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样
- 一致性:指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态。这种状态是语义上 的而不是语法上的,跟具体的业务有关
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间对其他并发事务是隔离的。
- 持久性:一个事务结束,它对数据所做的修改应该是永久的即便系统出现了故障也不会丢失。
在InnoDB存储引擎当中如何实现这三个特性了?
- 原子性:通过undo日志(回滚日志)来实现
- 一致性:通过原子性、持久性、隔离性来实现
- 隔离性:通过mvcc(多版本并发控制)+锁(next-key lock)
- 持久性:通过redo日志来实现。
事务的使用
事务有两种方式,分别为 显式事务 和 隐式事务。下面我们一个一个的来看这两个事务如何使用。
显示的事务我们可以使用 START TRANSACTION 或者 BEGIN ,作用是显式开启一个事务。
1.显示的事务
mysql> BEGIN;
#或者
mysql> START TRANSACTION;
但是本人一般使用BEGIN来开启事务,因为他最简单哈哈哈哈哈!
START TRANSACTION 语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符 :
- READ ONLY :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
- READ WRITE :标识当前事务是一个 读写事务 ,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
- WITH CONSISTENT SNAPSHOT :启动一致性读。
提交或者终止事务
# 提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;
终止事务
# 回滚事务。即撤销正在进行的所有没有提交的修改
mysql> ROLLBACK;
# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]
2.隐式的事务
在Innodb存储引擎下,会将没一条SQL封装成一个事务并且是默认是提交的。这个默认提交在MySQL中有一个系统变量 autocommit来控制 :
当然,如果我们想关闭这种 自动提交 的功能,可以使用下边两种方法之一:
- 显式的的使用 START TRANSACTION 或者 BEGIN 语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。
- 把系统变量 autocommit 的值设置为 OFF ,就像这样:
SET autocommit = OFF;
#或
SET autocommit = 0;
此时如果我们将这个变量设置为0我们对数据库进行写操作然后我们在让数据库崩溃,此时这条数据就不在了。
下面我们来演示一下这个事务的回滚,提交等操作的效果是什么样子的:
CREATE TABLE user(name varchar(20), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;
BEGIN;
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;
SELECT * FROM user;
下面我们在看情况二:
CREATE TABLE user (name varchar(20), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;
运行结果:
mysql> SELECT * FROM user;
+--------+
| name |
+--------+
| 张三 |
| 李四 |
+--------+
2 行于数据集 (0.01 秒)
情况三:
CREATE TABLE user(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
SET @@completion_type = 1;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;
SELECT * FROM user;
运行结果:
mysql> SELECT * FROM user;
+--------+
| name |
+--------+
| 张三 |
+--------+
1 行于数据集 (0.01 秒)
总结:
当我们设置 autocommit=0 时,不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事务,都需要用 COMMIT 进行提交,让事务生效,使用 ROLLBACK 对事务进行回滚。当我们设置 autocommit=1 时,每条 SQL 语句都会自动进行提交。 不过这时,如果你采用STARTTRANSACTION 或者 BEGIN 的方式来显式地开启事务,那么这个事务只有在 COMMIT 时才会生效,在 ROLLBACK 时才会回滚。
事务隔离级别
MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。那么在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。
SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:
- 读未提交:一个事务所做的修改还没有提交其他事务就能够看到。
- 读提交:一个事务所做的变更,只有提交之后其他事务才能看到。
- 可重复读: 一个事务执行过程当中看到的数据和一直跟这个事务启动时看到的数据是一致的这也是mysql的默认隔离基本
- 串行化: 会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
在这些隔离级别下,由于隔离基本的不同可能会出现脏读,不可重复读、幻读。下面解释一下脏读,不可重复读和幻读。
- 脏读: 如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
- 不可重复读: 在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
- 幻读:在一个事务内部多次查询符合条件的记录条数,前后查询得到的记录条数不一致。
下面我们来演示一下这个脏读,不可重复读和幻读。
由于这个MySQL的默认隔离级别是这个可重复读,所以我们在演示这个脏读的时候。我们需要将这个隔离级别设置为读未提交。
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.00 sec)
在这里我们需要修改一下这个隔离级别,将其设置为这个读未提交.
SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'
#其中,隔离级别格式:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE
或者
SELECT @@transaction_isolation;
注意我们设置隔离级别还分这个会话还是全局的。
使用 GLOBAL 关键字(在全局范围影响):
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
#或
SET GLOBAL TRANSACTION_ISOLATION = 'SERIALIZABLE';
则:
当前已经存在的会话无效,只对执行完该语句之后产生的会话起作用。
使用 SESSION 关键字(在会话范围影响):
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
#或
SET SESSION TRANSACTION_ISOLATION = 'SERIALIZABLE';
则:
对当前会话的所有后续的事务有效如果在事务之间执行,则对后续的事务有效该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务。在这里我们选择这个全局的来进行设置。
mysql> SET TRANSACTION_ISOLATION = 'READ-UNCOMMITTED';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
首先我们需要准备一张表来进行实验,在这里博主直接给出一张表:
CREATE TABLE student (
studentno INT,
name VARCHAR(20),
class varchar(20),
PRIMARY KEY (studentno)
) Engine=InnoDB CHARSET=utf8;
然后我们来演示一下这个脏读:
然后我们在将这个隔离级别设置为这个读提交,在演示一下这个不可重复读。
首先第一步我们将这个隔离性设置为这个读提交。
这就发生了这个不可重复读,查询同一数据前后两次结果不一致。
下面我们在测试一下这个幻读,我们看看在mysql在可重复读的隔离级别下是否会出现这个幻读的问题。
我们发现在inndb存储引擎下mysql并没有出现这个幻读的问题。其实是通过这个mvcc+锁来解决这个问题的。但是在inndb存储引擎下并没有解决所有的幻读问题依然是存在幻读问题。
总结一下:
- 在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象;
- 在「读提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
- 在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象;
- 「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生。
MVCC&ReadView
MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能。
在mysql innodb存储引擎下,采用两种方式来处理幻读:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
下面我们重点说一下这个mvcc是什么
MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
在undo日志的版本链,对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必
要的隐藏列。
- trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给trx_id 隐藏列
- roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
注意:
insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的Undo Log Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放).
假设之后两个事务id分别为 10 、 20 的事务对这条记录进行 UPDATE 操作,操作流程如下:
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个 roll_pointer 属性( INSERT 操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志都连起来,串成一个链表.
对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 ,版本链的头节点就是当前记录最新的值。每个版本中还包含生成该版本时对应的 事务id.
当然mvcc的实现原来还需要一个ReadView视图,那这个ReadView是什么东西了?这个ReadView中主要包含4个比较重要的内容,分别如下:
- creator_trx_id :创建这个 Read View 的事务 ID。
- trx_ids :表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表 。
- up_limit_id :活跃的事务中最小的事务 ID。
- low_limit_id :表示生成ReadView时系统中应该分配给下一个事务的 id 值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,
trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。
当然这个ReadView也有,有他的规则下面我们来学习一下这个ReadView的规则:
- 如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的 up_limit_id 值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于或等于ReadView中的 low_limit_id 值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的 up_limit_id 和 low_limit_id 之间,那就需要判
断一下trx_id属性值是不是在 trx_ids 列表中。如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
所以这个mvcc的流程就是:
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 ReadView;
- 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View。
注意:此时同样的查询语句都会重新获取一次 Read View,这时如果 Read View 不同,就可能产生不可重复读或者幻读的情况。
当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View,如下表所示
下面我们来看看这个MySQL在innodb存储引擎下如何解决幻读
假设现在表 student 中只有一条数据,数据内容中,主键 id=1,隐藏的 trx_id=10,它的 undo log 如下图所示
假设现在有事务 A 和事务 B 并发执行, 事务 A 的事务 id 为 20 , 事务 B 的事务 id 为 30 。
步骤1:事务 A 开始第一次查询数据,查询的 SQL 语句如下
select * from student where id >= 1;
在开始查询之前,MySQL 会为事务 A 产生一个 ReadView,此时 ReadView 的内容如下: trx_ids=[20,30] , up_limit_id=20 , low_limit_id=31 , creator_trx_id=20 。
由于此时表 student 中只有一条数据,且符合 where id>=1 条件,因此会查询出来。然后根据 ReadView机制,发现该行数据的trx_id=10,小于事务 A 的 ReadView 里 up_limit_id,这表示这条数据是事务 A 开启之前,其他事务就已经提交了的数据,因此事务 A 可以读取到。
结论:事务 A 的第一次查询,能读取到一条数据,id=1。
步骤2:接着事务 B(trx_id=30),往表 student 中新插入两条数据,并提交事务。
insert into student(id,name) values(2,'李四');
insert into student(id,name) values(3,'王五');
此时表student 中就有三条数据了,对应的 undo 如下图所示:
步骤3:接着事务 A 开启第二次查询,根据可重复读隔离级别的规则,此时事务 A 并不会再重新生成
ReadView。此时表 student 中的 3 条数据都满足 where id>=1 的条件,因此会先查出来。然后根据ReadView 机制,判断每条数据是不是都可以被事务 A 看到。
-
首先 id=1 的这条数据,前面已经说过了,可以被事务 A 看到。
-
然后是 id=2 的数据,它的 trx_id=30,此时事务 A 发现,这个值处于 up_limit_id 和 low_limit_id 之间,因此还需要再判断 30 是否处于 trx_ids 数组内。由于事务 A 的 trx_ids=[20,30],因此在数组内,这表示 id=2 的这条数据是与事务 A 在同一时刻启动的其他事务提交的,所以这条数据不能让事务 A 看到。
-
同理,id=3 的这条数据,trx_id 也为 30,因此也不能被事务 A 看见。
最终事务 A 的第二次查询,只能查询出 id=1 的这条数据。这和事务 A 的第一次查询的结果是一样的,因此没有出现幻读现象,所以说在 MySQL 的可重复读隔离级别下,不存在幻读问题。
总结:
MVCC 在 READ COMMITTD 、 REPEATABLE READ 这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。核心点在于 ReadView 的原理, READ COMMITTD 、 REPEATABLE READ 这两个隔离级别的一个很大不同就是生成ReadView的时机不同: -
READ COMMITTD 在每一次进行普通SELECT操作前都会生成一个ReadView
-
REPEATABLE READ 只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
MySQL是否还存在幻读
在这里博主先说一下,MySQL并没有解决这个所有的幻读。依然存在这个幻读的可能性。下面我们来进行实验看是否存在幻读了。
我们先建一张表,博主建的表如图下图所示:
然后我们这张表里面插入数据
我们发现这样就出现了幻读。下面我们在来一中情况我们这个快照读和当前读进行混用。
我们发现这样也出现了这个幻读。所以要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。