事务特性
大家都知道关系型数据库事务的特性:ACID
不同的隔离级别,存在着不同问题
MySQL默认的隔离级别是RR(在生产中我们一般采用RC的隔离级别),可以解决脏读和不可重复读,但是不能解决幻读的问题,如果要解决幻读的问题,就要采用串行化的方式,这样对数据库性能会有很大影响。
今天咱们来唠唠隔离性中,MySQL InnoDB实现RC和RR的原理——MVCC
啥是MVCC?
MVCC看起来很熟悉,难道是MVC的表兄弟?开个玩笑,MVCC的英文全称是Multi Version Concurrency Control
,中文是多版本并发控制
。从名字看来,InnoDB是通过数据行的多个版本,来管理数据库的并发。这个数据行的版本,有点类似于我们开发代码时的多次commit版本。
通过 MVCC 我们可以解决以下几个问题:
- 读写之间阻塞的问题,通过 MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
- 降低了死锁的概率。这是因为 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
- 解决一致性读的问题。一致性读也被称为快照读,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。
快照读和当前读
快照读
快照读顾名思义,就是读取的快照数据,有可能读到历史版本数据。简单的select都是快照读,比如:
select * from user_info ;
当前读
当前读就是读取最新数据,而非历史版本,加锁的select或者增、删、改,都属于当前读:
select * from user_info for update;
update user_info set name = 'zhangsan' where id = 2;
delete from user_info where id = 2;
insert into user_info values ('zhangsan');
RC和RR是如何读取数据的呢?
在读已提交和可重复读隔离级别中,多事务并发,查询的并不是当前行最新数据,而是数据库快照。
读已提交隔离级别下,会在每次查询前,读取快照,所以它是不可重复度。
可重复读隔离级别下,只会在事务开始时,查询数据库快照,所以它是可重复读。
那么问题来了,数据库快照是什么?数据库成百上千G的数据,如何能在瞬间生成快照呢?我们继续剖析。
InnoDB中MVCC如何实现的?
在深入MVCC前,我们先做一些知识准备。
事务号
InnoDB会为每个事务分配一个版本号——trx_id
,这个版本号是递增的,可以以此判断事务开启的时间。
细心的同学可能会问,如果trx_id用完了怎么办,这里其实大可放心,trx_id的设计可以使用到各位开发者安享晚年,它是采用6字节无符号数据存储,最大值为2的48次方=281474976710656,如果按照每秒10wTPS,可以安全使用89年,既是用到最后一位后,便会从0开始,这样有可能会产生一些脏读,不过基本上不会发生。
行记录的隐藏字段
每个人的身上(数据行)是都有毛毛(隐藏字段)
我来给你唱(分析下)毛毛(隐藏字段)
到底我们身上都有些什么毛(隐藏字段)
我来唱(分析)给你们知道
InnoDB的叶子节点上存储的是数据页,数据页上存储着数据行,行里面存着我们定义的字段,除了我们定义的字段外,还有一些隐藏字段,用于InnoDB系统计算。
行数据
Undo Log
InnoDB将快照数据,记录在了Undo Log中,我们可以在回滚段中找到相关数据,如图:
根据图来看,roll_ptr指针,将undo log用链表结构组合在一起,每个undo log都记录了当时的trx_id,这样我们可以很方便的找到历史快照。
Read View
有了这些前置条件,我们来探讨下,在RC和RR隔离级别下,InnoDB是如何通过MVCC实现一致性读的。
InnoDB中,多个事务对同一行数据更新,会产生多个历史版本(就像git
commit记录一样),这些历史版本就记录在Undo Log中,如果某个事务想要查询某行数据,那它需要读取哪行数据呢?这时候Read View
登场了。
有了这个read view,我们需要找哪个快照版本的数据,就一目了然了,那到底怎么找,我们看图,一图胜千言:
假设当前活跃事务是trx_id_8、trx_id_10、trx_id_12、trx_id_20
,那么套用变量:
- up_limit_id = trx_id_8
- low_limit_id = trx_id_20
- trx_ids = [trx_id_8, trx_id_10, trx_id_12, trx_id_20]
假设我们在事务中需要查询的数据的trx_id为x,那么:
- x < 8,表示该数据快照是当前活跃事务提交前的版本,是已经完成的事务提交的,安全可以使用。
- x > 20,表示该数据快照版本是当前活跃事务之后事务提交的,是不可以使用的。
- 若:8 <= x <= 20
3.1 trx_ids.contain(x),该行数据未提交,不可见。
3.2 !trx_ids.contain(x),该行数据已提交,可见。
好了,那么通过这种方式,InnoDB实现了全库数据快照,并保证了高并发下数据的准确性和一致性。设计思路不得不佩服。
总结
MVCC
的核心可以拆分为MV
+ VC
,MV
通过Undo Log
来实现多版本的管理,VC
通过Read View
来实现事务并发下,数据是否可见,针对不同的隔离级别,产生Read View的时机也不一样,这样也就实现了不同的隔离级别RC和RR。
MVCC在不通的数据库下,实现的原理不一样,主要是理解设计思想。