Transaction作为关系型数据库的核心组成,在数据安全方面有着非常重要的作用,本文会一步步解析事务的核心特性,以获得对事务更深的理解。
什么是事物?博主的理解了是事物是一次和数据库连接会话当中所有的sql要么全部成功要么全部失败。
在InnoDB存储引擎当中如何实现这三个特性了?
事务有两种方式,分别为 显式事务 和 隐式事务。下面我们一个一个的来看这两个事务如何使用。
显示的事务我们可以使用 START TRANSACTION 或者 BEGIN ,作用是显式开启一个事务。
1.显示的事务
mysql> BEGIN;
#或者
mysql> START TRANSACTION;
但是本人一般使用BEGIN来开启事务,因为他最简单哈哈哈哈哈!
START TRANSACTION 语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符 :
提交或者终止事务
# 提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;
终止事务
# 回滚事务。即撤销正在进行的所有没有提交的修改
mysql> ROLLBACK;
# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]
2.隐式的事务
在Innodb存储引擎下,会将没一条SQL封装成一个事务并且是默认是提交的。这个默认提交在MySQL中有一个系统变量 autocommit来控制 :
当然,如果我们想关闭这种 自动提交 的功能,可以使用下边两种方法之一:
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> 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存储引擎下并没有解决所有的幻读问题依然是存在幻读问题。
总结一下:
MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能。
在mysql innodb存储引擎下,采用两种方式来处理幻读:
下面我们重点说一下这个mvcc是什么
MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
在undo日志的版本链,对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必
要的隐藏列。
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个比较重要的内容,分别如下:
注意: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的规则:
所以这个mvcc的流程就是:
在隔离级别为读已提交(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并没有解决这个所有的幻读。依然存在这个幻读的可能性。下面我们来进行实验看是否存在幻读了。
我们先建一张表,博主建的表如图下图所示:
然后我们这张表里面插入数据
我们发现这样就出现了幻读。下面我们在来一中情况我们这个快照读和当前读进行混用。
我们发现这样也出现了这个幻读。所以要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。