MySQL多版本并发控制MVCC
# MySQL多版本并发控制MVCC
MySQL 多版本并发控制 MVCC 是如何解决并发读写问题的呢?
# 1 非锁定读(快照读)
如果读取的行正在执行 delete 或 update 操作,这时读取数据不会等待锁的释放,而会去读取行的快照数据。对于这种读取历史数据的方法,我们叫做一致性非锁定读,或快照读(snapshot read)。
一致性非锁定读(Consistent Nonlocking Reads,CNR)的实现,通常做法就是加一个版本号活锁时间戳字段。在更新数据的同时版本号 +1 或更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见。
在 Read Committed 和 Repeatable Read 两个隔离级别下,如果是执行普通的 select 语句则会使用一致性非锁定读(快照读)。之所以称为非锁定读,是因为不需要等待访问数据行上的独占锁的释放,快照数据是指该行之前版本的数据,通过 undo 日志来实现。
# 2 锁定读(当前读)
执行下面的语句,就是锁定读(Locking Reads)
select ... lock in share mode
:对记录加共享锁,其他事务也可加共享锁,如果加独占锁则会阻塞。select ... for update
、insert
、update
、delete
:对记录加独占锁锁,其他事务不能加任何锁。
# 3 多版本并发控制MVCC
Multi-Version Concurrency Control,多版本并发控制,主要是为了提高数据库的并发性能。
数据库并发场景:
读-读:不存在任何问题,不需要并发控制
读-写:有线程安全问题,可能造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。(MVCC解决该问题)
写-写:有线程安全问题,可能会存在丢失更新问题。
平时,同一行数据发生读写请求时,会上锁阻塞住。但 mvcc 用更好的方式去处理读-写请求,做到发生读-写请求冲突时不上锁。可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。解决脏读、不可重复读、幻读等事务隔离问题。
这里的读指的是快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。
MVCC 通过【数据可见性算法】分析当前数据是否可见,如果可见就读取当前数据,如果不可见就读取历史数据。【数据可见性算法】通过【隐藏字段】和【Read View】实现。如果当前数据不可见时,通过【Undo Log】读取历史数据。
# 4 隐藏字段
在内部,InnoDB 存储引擎为每行数据添加了三个隐藏字段:
- DB_TRX_ID:最后一次修改该行的事务id。插入、更新、删除都算修改。(transaction id)
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本。如果改行未被更新,则为空。(rollback pointer)
- DB_ROW_ID:若没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引。
# 5 Read View
主要用来做可见性判断,里面保存了「当前对本事务不可见的其他活跃事务」,有以下字段:
- m_creator_trx_id:创建 Read View 的事务 ID
- m_ids:Read View 创建时其他未提交的活跃事务 ID 列表。在创建 Read View 时,将当前未提交事务ID记录下来,后续即使他们修改了记录行的值,对于当前事务也是不可见的。
m_ids
不包括当前事务自己和已提交的事务(正在内存中) - m_low_limit_id:创建当前 read view 时,当前系统最大事务 ID+1 (注意:low是最大),大于这个id的数据版本不可见。
- m_up_limit_id:创建当前 read view 时,系统正处于活跃事务的最小版本号(注意:up是最小),系统正处于活跃事务的 ID 如果
m_ids
为空,则m_up_limit_id
=m_low_limit_id
,小于这个数据版本均可见。
小于最小事务id可见,大于最大事务id不可见,存在于中间需要后续判断。
# 6 undo log回滚日志
有两个作用:
- 当事务回滚是用于将数据恢复到修改前的样子
- MVCC,当读取记录时,若该记录被其他事务占用或者当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,一次实现非锁定读。
每一次对数据库记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性,可以将这些 undo 日志都连起来,串成一个链表:
# 7 数据可见性算法
在 InnoDB 存储引擎中,创建一个新事务后,执行每个 select 语句前的,都会创建一个快照 Read View,快照中保持了当前数据库系统中正处于活跃(没有commit)的事务的 ID 号。简单来说,就是保存了当前不应该被本事务看到的其他事务 ID 列表(即m_ids)。当用户要读取某个记录行的时候,InnoDB 会将该记录的 DB_TRX_ID 与R ead View 中的一些变量即当前事务 ID 进行比较,判断是否满足可见性。
一定要记住!!
DB_TRX_ID:最后一次修改该行数据的事务 id
m_ids:正在活跃的事务
该条数据是否可见,主要在于 DB_TRX_ID 事务是否已经提交,若提交则可见,若未提交则不可见。
- 如果 DB_TRX_ID < m_up_limit_id(最小版本号) || DB_TRX_ID = m_creater_trx_id,表明最新修改该行的事务在当前事务创建快照之前就提交了,或者修改该行的事务创建了当前快照,所以该记录行的值对当前事务可见。
- 如果 DB_TRX_ID >=m_low_limit_id(最大版本号),表明最新修改该行的事务在当前事务创建快照之后才修改改行,所以该记录行的值对当前事务不可见。跳到步骤5
- m_ids为空,不存在活跃的事务,则表明在当前事务创建快照之前,修改改行的事务就已经提交了,所以该记录行的值对当前事务是可见的。
- 如果m_up_limit_id <= DB_TRX_ID < m_low_limit_id,表明最新修改改行的事务在当前事务创建快照的时候可能处于“活跃状态”或“已提交状态”;所以要对活跃事务列表m_ids进行查找。
- 如果活跃事务列表中能找到 DB_TRX_ID,表明该行记录的修改未提交,所以记录行对当前事务不可见。
- 如果活跃事务列表中能找不到 DB_TRX_ID,表明该行记录的修改已提交,所以记录行对当前事务可见。
- 在记录行的 DB_ROLL_PTR 指针诊所指向的 undo log 取出快照记录,用快照记录的 DB_TRX_ID 跳到步骤1重新开始判断,直到找到满足的快照版本或返回空。
# 8 RC和RR隔离级别下的MVCC的差异
- 在 RC(Read Commit)隔离级别下,每次 select 查询前都会生成一个Read View。
- 在 RR(Repeatable Read)隔离级别下,只在事务开始后第一次 select 数据前生成一个Read View。
# 9 MVCC 与 Next-key-Lock 防止幻读
InnoDB 存储引擎在 PR 级别下通过 MVCC 和 Next-key Lock 来解决幻读问题。
在执行普通 select,此时会以 MVCC 快照读的方式读取数据(不加锁)
在快照读的情况下,PR 隔离级别只会在事务开启后的第一次查询生成 Read View,并使用至事务提交。所以在生成 Read View 之后其他事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的“幻读”。
执行 select...for update/lock in share mode、insert、update、delete 等当前读(加锁)
在当前读下,读取的都是最新的数据,如果其他事务有插入新的记录,并且刚好在当前事务查询的范围内,就会产生幻读。InnoDB 使用 Next-key Lock 来防止这种情况。当执行当前读时,会锁定读取到的记录,锁定它们的间隙,防止其他事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读。