MVCC
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,可以实现对数据库的并发访问。
MVCC带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
MVCC的实现原理
MVCC的实现原理主要是依赖记录中的 4个隐式字段,undo日志 ,Read View
来实现的。所以我们先来看看这个四个隐藏字段
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
- DB_TRX_ID
6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID - DB_ROLL_PTR
7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里) - DB_ROW_ID
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引 - flag
实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
undo日志
redo log:重做日志,就是每次mysql在执行写入数据前先把要写的信息保存在重写日志中,但出现断电,奔溃,重启等等导致数据不能正常写入期望数据时,服务器可以通过redo_log中的信息重新写入数据。
undo log:撤销日志,与redo log恰恰相反,当一些更改在执行一半时,发生意外,而无法完成,则可以根据撤消日志恢复到更改之前的壮态。
undo log主要分为两种:
- insert undo log
事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃 - update undo log
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
Read View(读视图)
说read view之前,要先知道当前读
当前读
读取数据的最新版本。常见的 update/insert/delete、还有 select ... for update、select ... lock in share mode
都是当前读。
什么是Read View?
Read View就是在该事务进行的快照读的那一刻,会生成数据库系统当前的一个快照,这个快照就是read view。
下面就说一下怎么基于MVCC生成Read View?
在此之前,先说几个ReadView的概念
- m_ids:活跃事务id列表,表示在生成ReadView时系统中活跃的读写事务的事务id列表。
- min_trx_id:活跃事务id列表中最小事务ID表示在生成ReadView时当前活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
- max_trx_id:活跃事务id列表中最大事务ID表示生成ReadView时系统中应该分配给下一个事务(这是解决情况1的核心点)的id值。
- creator_trx_id:当前事务ID表示生成该ReadView的事务的事务id。
判断逻辑:
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
MVCC 解决了幻读了没有?
MVCC 解决了部分幻读,但并没有完全解决幻读。
对于快照读,MVCC 因为因为从 ReadView 读取,所以必然不会看到新插入的行,所以天然就解决了幻读的问题。
而对于当前读的幻读,MVCC 是无法解决的。需要使用 Gap Lock 或 Next-Key Lock(Gap Lock + Record Lock)来解决。
Repeatable Read 解决了幻读是什么情况?
SQL 标准中规定的 RR 并不能消除幻读,但是 MySQL InnoDB 的 RR 可以,靠的就是 Gap 锁。在 RR 级别下,Gap 锁是默认开启的,而在 RC 级别下,Gap 锁是关闭的。
深入思考
只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
q:如果默认select产生的事务id为0的话,那么min_trx_id永远为0,这不是扯犊子吗?
A:m_ids为读写事务列表,而只有select查询的情况下,为只读事务,不会参与计算,并且其实只读事务会产生一个随机事务id,并不是默认为0的事务
场景举例
CREATE TABLE `test` (
`id` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
RR级别
情况1:
事务A | 事务B |
begin; | |
begin; | |
Select * from test; | |
insert into test(id,name) values(2,‘323’); | |
commit; | |
Select * from test; | |
此时无法查询到B事务插入的数据 |
情况2:
事务A | 事务B |
begin; | |
begin; | |
insert into test(id,name) values(2,‘323’); | |
commit; | |
Select * from test; | |
可以查询到B事务插入的数据 |
情况3:
事务A | 事务B |
begin; | |
begin; | |
Update test set name = ‘2’ where id = 4 | |
insert into test(id,name) values(2,‘323’); | |
commit; | |
Select * from test; | |
可以查询到B事务插入的数据 |
情况4:
事务A | 事务B |
begin; | |
begin; | |
Select * from test; | |
insert into test(id,name) values(2,‘323’); | |
commit; | |
Select * from test; | |
Update test set name = ‘2’ where id = 2 | |
Select * from test; | |
此时无法查询到B事务插入的数据 |