目录
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
MySQL有4个隔离级别:
用一个例子说明这几种隔离级别。假设数据表 T 中只有一列,其中一行的值为 1,下面是按照时间顺序执行两个事务的行为。
- mysql> create table T(c int) engine=InnoDB;
- insert into T(c) values(1);
在不同的隔离级别中,事务 A 会有不同的返回结果,也就是图里面 V1、V2、V3 的返回值会不同。
在实现上,数据库里面会创建一个视图(read-view),访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
这里需要先讲解下当前读和快照读 和 事务的启动时机和读视图生成的时刻
MySQL读取数据实际上有两种模式,分别是当前读和快照读。
快照读:普通的select语句(即是不加锁的select操作) 都是采用 快照读的模式。
当前读:数据修改的操作(update、insert、delete) 和select ... lock in share mode; select ... for update;
都是采用 当前读的模式,对读取到的数据(索引记录)加锁来保证数据一致性,是读到最新的数据。
在 MySQL 有两种开启事务的命令,分别是:
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。
第一种启动方式,一致性视图是在执行第一个快照读语句时创建的;
第二种启动方式,一致性视图是在执行start transaction with consistent snapshot 时创建的。
说到视图(read-view),那就会引出MVCC。而事务隔离就是通过MVCC来实现的。
更加准确来说,实现事务隔离的方法是有两种:
所以,事务隔离是通过MVCC 和 加锁 实现的。
那么,MVCC 用来实现哪几个隔离级别?
多版本并发控制(Multi-Version Concurrency Control)是一种用来解决读-写冲突的无锁并发控制,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。
最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了 InnoDB 的并发度。
实现原理主要是依赖记录中的 三个隐藏字段,undo日志 ,Read View 来实现的。
innodb引擎保存的行数据是有三个隐藏字段的。
具体的内容可以查看该文章MySQL的一行数据是如何存储的?
innodb引擎表 的聚簇索引保存的数据就是完整的行数据(即是上图的数据)。这里就主要是使用TRX_ID和ROLL_PTR。
DB_TRX_ID | 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。 |
DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,即是指向指向 undo log 的指针。用于配合Undo Log,指向上一个版本。 |
每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中。DB_ROLL_PTR是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
INSERT
操作的时候,产生的回滚日志在事务提交后可被立即删除
。而UPDATE
和DELETE
操作的时候,产生的Undo Log日志不仅在进行数据回滚时需要,在进行快照读时也需要,所以不会立即被删除
。undo log 版本链
用一个表做例子 ,建表语句如下。
- mysql> CREATE TABLE `t` (
- `id` int NOT NULL,
- `age` int DEFAULT NULL,
- `name` varchar(10) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB;
上图中的(1):
从图中可以得知此时插入的事务ID是1,此时插入会同时生成一条 undo log ,并且行记录上的 roll_pointer 会指向这条 undo log ,而这条 undo log的类型是TRX_UNDO_INSERT_REC
,代表是 insert 生成的,里面还存储了主键值(还有其他值,这里就不做过多介绍)。
所以 InnoDB 可以根据 undo log 里的主键的值,找到这条记录,然后删除该主键对应的行数据来实现回滚的效果。因此可以简单地理解 undolog 里面存储的就是当前操作的反向操作,认为里面存了个delete 30即可
。
上图中的(2):
此时事务1提交,然后另一个事务ID为 2的事务执行 update t set age=3 where id=30
,此时的行记录和 undolog 就如上图所示的(2)。
之前 insert 产生的 undo log没了,insert 的事务提交了之后对应的 undolog 就被回收了,为什么呢?
因为不可能有别的事务会访问比这还要早的版本了,访问插入之前的版本?插入之前的版本都没有这行数据,要如何访问??没得访问的。所以insert事务提交后对应的undo log就回收了。
(看到很多文章写的是insert对其他事务不可见,只对本事务可见,所以提交后就可删除,感觉这理由是不妥的)
update 产生的 undolog,其类型为 TRX_UNDO_UPD_EXIST_REC,
并且记录上一版本的trx_id和数据。
上图中的(3):
此时事务 2提交,然后另一个 ID 为 3 的事务执行update t set name='a3' where id=30
,此时的记录和 undolog 就如上图所示中的(3)。
update 产生的 undolog 不会马上删除,因为可能有别的事务需要访问之前的版本,所以不能删。这样就串成了一个版本链,可以看到该记录本身加上两条 undo log,这条 id 为 30的记录就共有三个版本。
不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的记录,链表尾部是最早的记录。我们把这个链表称之为 版本链。
Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻(select ....),会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
已经弄清楚版本链后,而 readView 就是用来判断哪个版本对当前事务可见的。
readView中有4个概念:
知道版本链和读视图后,那如何通过读视图来判断哪个版本对当前事务是可见的呢?
代码中判断的逻辑如下:
从最新版本开始沿着版本链逐渐寻找老的版本,如果遇到符合任一条件的版本就返回。
注意:在不同的隔离级别下快照读生成的ReadView规则不同:
而我们写sql语句后进行分析可见版本,是看不到min_trx_id和max_trx_id这些数据的。那我们用另一种方式来判断。
不知min_trx_id等数据的分析规则:
一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
这种通过 版本链 来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
用一个表做例子 ,建表和初始化语句如下。
- mysql> CREATE TABLE `t` (
- `id` int NOT NULL,
- `age` int DEFAULT NULL,
- `name` varchar(10) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB;
- insert into t values(30,30,'a30');
从上图时间顺序,事务2先启动,跟着是事务3,4,5依次启动。事务2对应的事务id是2,依次类推。
因为是RC隔离级别,所以每次select都会生成新的快照。
下面是每次提交事务生成的版本链&第一次快照读的ReadView
只分析事务5中的select,从最新版本开始沿着版本链逐渐寻找老的版本。
第一次select:
查看上图的并发执行过程,对比事务5,发现事务2是已提交,因此此刻可以读取事务2提交过的数据。
第二次select也是这样分析,但注意的是在RC隔离级别下,是生成新的读视图的,这里还是按照上面的逻辑分析的,这里就不具体写了,留给读者分析。
用不知min_trx_id等数据的分析规则进行分析:
第一次select:
第二次select:
这里的事务执行顺序和RC隔离级别的是一样的。
在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该 ReadView,那么既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么最终快照读返 回的结果也是一样的。而且都是和RC隔离级别第一次select中的结果一样的。分析过程和RC级别的一样。
用不知min_trx_id等数据的分析规则进行分析:
第一次select:和RC隔离级别的第一次select是一样的,事务id是2的版本可见。
第二次select:
这样分析第一次select和第一次select读取的数据是一致的。
如果没有MVCC读写操作之间会有冲突。
假设一个场景:
事务A在执行中,此时事务B修改了记录1,还没提交;而此时事务A想要读取记录1。事务B还没提交,所以事务A无法提取到最新的记录1,不然就是脏读了。
那么事务A就是应该读取被事务B修改前的记录。但是记录1已被事务B修改了,那就只能用锁,用锁阻塞等到事务B的提交。这种实现就是基于锁的并发控制 ,Lock Based Concurrency Control(LBCC)。
这时,如果有多版本就好了,保存事务B修改记录1之前的版本数据。此时事务A就可以读取之前版本的数据,这样读写操作就不会阻塞,也不用加锁。所以说 MVCC 提高了事务的并发度,提升数据库的性能。
这个多版本说法只是为了便于理解或者说展现出来像多版本的样子而已。
InnoDB 不会真的存储多个版本的数据,只是借助 undo log 记录每次写操作的反向操作,所以索引上对应的记录只会有一个版本(即最新版本)。只不过可以根据 undo log 中的记录反向操作得到数据的历史版本,所以看起来是多个版本。
事务是在 MySQL 引擎层实现的,默认的 InnoDB 引擎支持事务。
MySQL InnoDB 引擎的默认隔离级别是 可重复读(RR),但不建议将隔离级别升级为串行化,因为这会导致数据库并发时性能很差。RR隔离级别是可以很大程度避免幻读现象(并不是完全解决),解决的方案有两种:
所以,事务隔离是通过MVCC 和 加锁 实现的。