MySQL之MVCC
MySQL之MVCC
背景
在前面的MySQL锁文章中有提到过,
MyISAM
表的读操作与写操作,以及写操作之间是串行的。并且默认情况下写操作命令的执行优先于读操作执行,即使读请求早于写请求到达,写锁也会插队到读请求前面,因为
MySQL
认为写请求一般比读请求要重要。在
InnoDB
中,就是通过多版本并发控制(MVCC
)来解决读请求和写请求的并发问题,提高数据库的并发能力。并且在InnoDB
中是默认读不加锁,读写不冲突的。
两个概念
快照读
不加锁的
select
操作就是快照读,即不加锁的非阻塞读。当我们执行select xxx from table_name where xxx=yyy
语句时,就是快照读。
不是所有的隔离级别下都支持快照读。
- 读未提交: 不支持,未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行
- 读已提交:支持,每次
select
都生成一个快照读- 可重复读:支持,开启事务后执行第一个
select
语句才生成快照读,而不是一开启事务就生成快照读, 并且整个事务只生成一个快照读- 串行化:不支持,在此隔离级别下,读请求都是当前读。
当前读
读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。
可执行以下语句来进行当前读:
1 | select ... for update |
或者
1 | select ... lock in share mode |
除此之外,还有隐式的当前读,如执行insert
/update
/delete
语句时,这些语句会获取排他锁,自然保证了数据是最新的。
ps
: 这里获取的排他锁,其实就是InnoDB
中的next-key lock
(即record lock
+gap lock
)
实现
隐藏列
在
MySQL
表中除了我们创建的字段列以外,还有一些隐藏列字段,正是通过一些隐藏列字段来实现MVCC
。
DB_TRX_ID
最近修改(修改/插入)事务
id
(6字节):记录创建这条记录/最后一次修改该记录的事务id
DB_ROLL_PTR
回滚指针(7字节):指向这条记录的上一个版本(存储于
rollback segment
里)
DB_ROW_ID
隐藏主键(6字节):隐含的自增
id
,如果数据表没有主键,InnoDB
会自动以DB_ROW_ID
产生一个聚簇索引
Undo Log 版本链
undo log
版本链是基于 undo log
实现的。undo log
中主要保存了数据的基本信息,比如说日志开始的位置、结束的位置,主键的长度、表id
,日志编号、日志类型。
此外,undo log
还包含两个隐藏字段 trx_id
和 roll_pointer
。trx_id
表示当前这个事务的 id
,MySQL
会为每个事务分配一个 id
,这个 id
是递增的。roll_pointer
是一个指针,指向这个事务之前的 undo log
。
示例
执行:
1 | INSERT INTO student VALUES (1, '张三'); |
继续执行:
1 | UPDATE student SET name='李四' WHERE id=1; |
继续执行:
1 | UPDATE student SET name='王五' WHERE id=1; |
如此,数据行的每一次变化都会记录在undo log
中。为了保证事务并发操作时,在写各自的undo log
时不产生冲突,InnoDB
采用回滚段的方式来维护undo log
的并发写入和持久化。回滚段实际上是一种undo
文件组织方式。
ps
:select
操作不会记录到undo log
中,因为并没有改变数据行。
Read View(读视图)
由上面的Undo Log
版本链,我们知道在MySQL
中同一行数据会有多个版本,那在事务进行数据读取的时候,怎么判断数据具体应该读取哪一个版本呢?
在执行当前读时,明显我们是要读取数据行的最新版本,这个是确定的。
当执行快照读时,我们可以根据当前事务的事务id
来判断,具体该读取数据行的哪一个版本。
ps
:每个事务开启时,都会被分配一个id
,这个id
是默认递增的,所以事务越新,id
越大
在MySQL
执行快照读时,会生成数据库系统当前的一个快照,根据事务状态或者事务id
,大概有以下几类信息
- 已提交事务列表
- 未提交事务列表(活跃事务)
- 还未分配事务
id
(最大活跃事务id
+ 1)
当前事务
id
可见的数据,是根据事务id
来进行判断的。大于最大事务
id
,不可见
在活跃事务id
集合中的,不可见小于最小的活跃事务
id
的,可见
事务id
为本事务id
的,可见
总结:
Read View
就是协助MVCC
来实现并发读写时,保证事务能正确读取数据行信息。
可重复读的实现
在可重复读的隔离级别下,我们在事务中,多次读取数据,前后总是一致的。这个就是依赖MVCC
来实现的。
在可重复读的隔离级别下,事务中第一个执行
select
语句的时刻,数据库才会生成一个Read View
,事务后续的快照读都会使用这个Read View
来读取数据。
MVCC解决幻读了吗
幻读:
事务A执行多次读取操作过程中,由于在事务提交之前,事务B(
insert
/delete
/update
)写入了一些符合事务A的查询条件的记录,导致事务A在之后的查询结果与之前的结果不一致,这种情况称之为幻读。
在可重复读的隔离级别下
快照读时,事务读取数据都是通过
Read View
来读取的,并且事务中第一个执行select
语句的时刻,数据库才会生成一个Read View
,事务后续的快照读都会使用这个Read View
来读取数据。这样并不会读取到其他事务提交的数据行,保证了多次读取数据的结果是一致的,所以解决了幻读的问题。
当前读时,事务总是会读取数据行的最新版本,在不做其他限制的情况下,
MVCC
并不会阻止其他事务进行数据行的插入,所以不能解决幻读的问题。
结论
MVCC
在快照读的情况下可以解决幻读问题,但是在当前读的情况下不能解决,需要配合间隙锁才能解决
可重复读隔离级别一定不会发生幻读吗?
不一定,在事务中交叉使用 当前读 和 快照读 时,仍然会发生幻读问题。
情况一:
- 事务A先查询,此时数据库没有记录a
- 事务B插入记录a,并提交事务
- 事务A更新不存在的记录a
- 然后事务A就可以看到事务B提交的记录a了,即出现了幻读
情况二:
- 事务A执行快照读,查询记录行数
- 事务B插入一条数据,并提交事务
- 事务A执行当前读,查询记录行数,行数不一致,即出现了幻读