🐱个人主页:阿Q说代码
🙋♂️作者简介:公众号阿Q说代码作者(期待你的关注)、infoQ签约作者、CSDN后端领域新星创作者
💫技术方向:专注于后端技术栈分享:JVM、数据库、中间件、微服务、Spring全家桶
我们从上文中了解到InnoDB
默认的事务隔离级别是repeatable read
(后文中用简称RR),它为了解决该隔离级别下的幻读的并发问题,提出了LBCC
和MVCC
两种方案。其中LBCC
解决的是当前读情况下的幻读,MVCC
解决的是普通读(快照读)的幻读。至于什么是当前读,什么是快照读,将在文中给出答案。
LBCC
LBCC
是Lock-Based Concurrent Control
的简称,意思是基于锁的并发控制,此文主要内容是MVCC
,所以LBCC
暂时不展开。
当前读
当前读(Locking Read
)也称锁定读,读取当前数据的最新版本,而且读取到这个数据之后会对这个数据加锁,防止别的事务更改即通过next-key
锁(行锁+gap锁)来解决当前读的问题。
在进行写操作的时候就需要进行“当前读”,读取数据记录的最新版本,包含以下SQL
类型:select ... lock in share mode
、select ... for update
、update
、delete
、insert
。
因为锁的粒度过大,会导致性能的下降,因此提出了比LBCC
性能更优越的方法MVCC
。
MVCC
MVCC
是Multi-Version Concurremt Control
的简称,意思是基于多版本的并发控制协议,通过版本号,避免同一数据在不同事务间的竞争,只存在于InnoDB
引擎下。它主要是为了提高数据库的并发读写性能,不用加锁就能让多个事务并发读写。
MVCC
的实现依赖于:三个隐藏字段、Undo log
和Read View
,其核心思想就是:只能查找事务id小于等于当前事务ID的行;只能查找删除时间大于等于当前事务ID的行,或未删除的行。
接下来让我们从源码级别来分析下MVCC
。
隐藏列
MySQL
中会为每一行记录生成隐藏列,接下来就让我们了解一下这几个隐藏列吧。
(1)DB_TRX_ID:事务ID,是根据事务产生时间顺序自动递增的,是独一无二的。如果某个事务执行过程中对该记录执行了增、删、改操作,那么InnoDB
存储引擎就会记录下该条事务的id。
(2)DB_ROLL_PTR:回滚指针,本质上就是一个指向记录对应的undo log
的一个指针,大小为 7 个字节,InnoDB
便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在undo log
中都通过链表的形式组织。
(3)DB_ROW_ID:行标识(隐藏单调自增 ID
),如果表没有主键,InnoDB 会自动生成一个隐藏主键,大小为 6 字节。如果数据表没有设置主键,会以它产生聚簇索引。
(4)实际还有一个删除flag隐藏字段,既记录被更新或删除并不代表真的删除,而是删除flag变了。
undo log
每当我们要对一条记录做改动时(这里的改动可以指INSERT、DELETE、UPDATE),都需要把回滚时所需的东西记录下来, 比如:
- Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
- Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
- Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
InnoDB
把这些为了回滚而记录的这些东西称之为undo log
。这里需要注意的一点是,由于查询操作(SELECT
)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log
。
每次对记录进行改动都会记录一条undo日志,每条undo日志也都有一个DB_ROLL_PTR
属性,可以将这些undo日志都连起来,串成一个链表,形成版本链。版本链的头节点就是当前记录最新的值。
例
先插入一条记录,假设该记录的事务id为80,那么此刻该条记录的示意图如下所示
实际上insert undo
只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的Undo Log Segment
也会被系统回收。接着继续执行sql操作
其版本链如下
很多人以为
undo log
用于将数据库物理的恢复到执行语句或者事务之前的样子,其实并非如此,undo log
是逻辑日志,只是将数据库逻辑的恢复到原来的样子。因为在多并发系统中,你把一个页中的数据物理的恢复到原来的样子,可能会影响其他的事务。
Read View
在可重复读隔离级别下,我们可以把每一次普通的select
查询(不加for update
语句)当作一次快照读,而快照便是进行select
的那一刻,生成的当前数据库系统中所有未提交的事务id数组(数组里最小的id
为min_id
)和已经创建的最大事务id
(max_id
)的集合,即我们所说的一致性视图readview
。在进行快照读的过程中要根据一定的规则将版本链中每个版本的事务id
与readview
进行匹配查询我们需要的结果。
快照读是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。快照读的实现是基于多版本并发控制,即MVCC
,可以认为MVCC
是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。MVCC
只在 READ COMMITTED
和 REPEATABLE READ
两个隔离级别下工作,其他两个隔离级别不和MVCC
不兼容。因为READ UNCOMMITTED
总是读取最新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE
则会对所有读取的行都加锁。事务的快照时间点(即下文中说到的Read View
的生成时间)是以第一个select
来确认的。所以即便事务先开始,但是select
在后面的事务的update
之类的语句后进行,那么它是可以获取前面的事务的对应的数据。
RC和RR隔离级别下的快照读和当前读:RC隔离级别下,快照读和当前读结果一样,都是读取已提交的最新;RR隔离级别下,当前读结果是其他事务已经提交的最新结果,快照读是读当前事务之前读到的结果。RR下创建快照读的时机决定了读到的版本。
对于使用RC和RR隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。为此,InnoDB
提出了一个Read View
的概念。
Read View
就是事务进行快照读(普通select
查询)操作的时候生产的一致性读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,它由执行查询时所有未提交的事务id数组(数组里最小的id为min_id
)和已经创建的最大事务id(max_id
)组成,查询的数据结果需要跟read view
做对比从而得到快照结果。
版本链比对规则: