MVCC出现背景
事务的4个隔离级别以及对应的三种异常
读未提交(Read uncommitted)
读已提交(Read committed):脏读
可重复读(Repeatable read):不可重复读
串行化(Serializable):幻读
- 脏读:一个事务读取到了另外一个事务没有提交的数据;
- 不可重复读:在同一个事务中,两次读取同一数据,得到内容不同;
- 幻读:同一事务中,用同样的操作读取两次,得到的记录数不同。
在MySQL 中,默认的隔离级别是可重复读,可以解决脏读和不可重复读的问题,但不能解决幻读的问题。如果我们需要解决幻读的问题,就需要采用串行化的方式,也就是将隔离级别提升到最高,但这样依赖就会大幅降低数据库的事务并发能力。
而MVCC就是通过乐观锁的方式来解决不可重复读和幻读的问题,它可以在大多数情况下替代行级锁,降低系统的开销。
MySQL并发事务会引起更新丢失问题,解决办法是锁,主要分两类:
- 乐观锁:
其实就如它的名字一样,非常乐观,总是假设比较好的情况。
每次取数据的时候都认为他人不会对其修改,所以不会上锁,但是会在更新的时候进行判断,看看在此期间内有没有人去更新这个数 据,可以使用版本号机制和CAS算法实现。
- 悲观锁:
与乐观锁完全相反,非常悲观,总是假设非常糟糕的情况。
每次取数据的时候都认为他人会对其进行修改,所以每次拿数据的时候都会加上锁,这样别人想拿数据的话就会阻塞,除非它拿到 锁。
什么是MVCC
Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。
多版本并发控制(MVCC) 是通过保存数据在某个时间点的快照来实现并发控制的,也就是说,不管事务执行多长时间,事务内部看到的数据是不受其它事务影响的,根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
简单来说,多版本并发控制的思想就是保存数据的历史版本,通过对数据行的多个版本管理来实现数据库的并发控制。这样我们就可以通过比较版本号决定数据是否显示出来,读取数据的时候不需要加锁也可以保证事务的隔离级别。
可以认为多版本并发控制是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销耕地。虽然实现机制有所不同,但大都实现类非阻塞的读操作,写操作也只锁定必要的行。
MySQL 的大多数事务性存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制 。不仅是MySQL ,包括Oracle 、PostgreSQL 等其它数据库系统也都实现了MVCC ,但各自的实现机制不尽相同,因为MVCC 没有一个统一的实现标准,典型的有乐观并发控制 和悲观并发控制。
MVCC解决了什么问题
解决了在REPEATABLE READ和READ COMMITTED两个隔离级别下读同一行和写同一行的两个事务的并发。
-
读写直接阻塞的问题:通过 MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
提高并发的演进思路:
- 普通锁,只能串行执行;
- 读写锁,可以实现读读并发;
- 数据多版本并发控制,可以实现读写并发。
-
降低了死锁的概率:因为 InnoDB 的 MVCC采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
-
解决一致性读的问题:一致性读也被称为快照读,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。
举个简单的例子:
- 一个事务A (txnId =100),修改了数据X ,使得X =1,并且commit了。
- 另外一个事务B (txnId =101)开始尝试读取X ,但是还X =1。但B没有提交。
- 第三个事务C (txnId =102)修改了数据X ,使得X=2,并且提交了。
- 事务B 又一次读取了X。这时
- 如果事务B 是Read Committed ,那么就读取X 的最新commit 的版本,也就是X=2。
- 如果事务B是Repeatable Read 。那么读取的就是当前事务(txnId =101)之前X 的最新版本,也就是X 被txnId =100提交的版本,即X=1。
注意,这里B 不论是Read Committed ,还是Repeatable Read ,都不会被锁,都能立刻拿到结果。这也就是MVCC存在的意义。
快照读与当前读
快照读 (SnapShot Read )是一种一致性不加锁的读 ,是InnoDB并发如此之高的核心原因之一。
这里的一致性 是指,事务读取到的数据,要么是事务开始前就已经存在的数据 ,要么是事务自身插入或者修改过的数据。
不加锁的简单SELECT属于快照读,例如:
sql
select * from users where id = '1';
与快照读 相对应的则是当前读 ,当前读 就是读取最新数据,而不是历史版本的数据。加锁的SELECT就属于当前读,例如:
sql
select * from users where id = '1' lock in share mode;
select * from users where id = '1' for update;
MVCC 就是为了实现读-写冲突不加锁 ,而这个读指的就是快照读 ,而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现
InnoDB的MVCC是如何工作的
当查询一条记录的时候,执行流程如下:
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 Read View;
- 查询得到的数据,然后与 Read View 中的事务版本号进行比较;
- 如果不符合 Read View 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
InnoDB是如何储存记录多个版本的
事务版本号:
每开启一个事务,我们都会从数据库中获取一个事务ID (也就是事务版本号),这个事务ID 是自增长的,通过ID大小,我们就可以判断事务的时间顺序。
行记录的隐藏列:
InnoDB 的叶子段储存了数据页,数据页中保存了行记录,而在行记录中有一些重要的隐藏字段:
- DB_ROW_ID :6-byte ,隐藏的行ID ,用来生成默认聚簇索引。如果我们创建数据表的时候没有指定聚簇索引,这时InnoDB 就会用这个隐藏ID来创建聚簇索引。采用聚簇索引的方式可以提升数据的查找效率。
- DR_TRX_ID :6byte ,操作这个数据的事务ID ,也就是最后一个对该数据进行插入或更新的事务ID。
- DB_ROLL_PTR :7byte ,回滚指针,也就是指向这个记录的Undo log信息。
Undo Log
InnoDB 将行记录保存在了Undo Log,我们就可以在回滚段中找到它们,如下图所示:
从图中能看到回滚指针将数据行的所有快照记录都通过链表的结构串联了起来,每个快照的记录都保存了当时的db_trx_id ,页就是那个时间点操作这个数据的事务ID。这样如果我们想找历史数据快照,就可以通过遍历回滚指针的方式进行查找。
Read View(读视图)
什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
所以我们知道Read View 主要是用来做可见性判断的,即当我们某个事务只想快照读的时候,对该记录创建一个Read View 读视图,把它比作条件用来判断当前事务是否能够看到哪个版本的数据,即可能是当前最新的数据,也有可能是该行记录的undo log里面的的某个版本数据。
- trx_list 未提交事务ID列表,用来维护Read View生成时刻系统正活跃的事务ID
- up_limit_id 记录trx_list列表中事务ID最小的ID
- low_limit_id ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
- 首先比较DB_TRX_ID < up_limit_id , 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
- 接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
- 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains (DB_TRX_ID ),如果在,则代表我Read View 生成时刻,你这个事务还在活跃,还没有Commit ,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View 生成之前就已经Commit了,你修改的结果,我当前事务是能看见的
在可重复读(REPEATABLE READ )隔离级别下,InnoDB 的MVCC是如何工作的
查询
InnoDB会根据以下两个条件检查每行记录:
- InnoDB 只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样确保事务读取的行,要么实在事务开始前已经存在的,要么是事务自身插入或者修改过的
- 行的删除版本要么未定义,要么大于当前事务版本号。这样可以确保事务读取到的行,在事务开始之前未被删除。
插入
InnoDB未新插入的每一行保存当前系统号作为行版本号。
删除
InnoDB 为删除的每一行保存当前系统版本号作为行删除标识。
删除在内部被视为更新,行中的一个特殊位会被设置为已删除。
更新
InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。