上一篇文章,介绍了隔离级别,MySQL默认是使用可重复读,但是在可重复读的级别下,可能会出现幻读,也就是读取到另一个session添加的数据,那么除了配合使用间隙锁的方式,还使用了MVCC机制解决,保证在可重复读的场景下,同一个session读取的数据一致性。
mvcc机制
MVCC(Multi-Version Concurrency Control) 多版本并发控制机制,对同一行数据的读和写操作默认不会加锁互斥保证隔离型,提高性能,而串行化隔离级别为了保证较高的隔离型是将所有操作通过互斥来实现的。
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。
原理
其实undo日志链是指一行数据被多个事务依次修改过后,每个事务修改完后,mysql都会保留修改前的数据undo 回滚日志,并且添加两个隐藏字段trx_id
和 roll_pointer
将undo日志链串联形成一个历史记录版本链。 通过数据快照的方式。关键核心是undo日志和readView
什么时候会生产trx-id ?
在begin transaction的时候并不会新建,在执行到他们之后的第一个修改操作InnoDB表的语句的时候,事务才真正启动,向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
一个案例
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
最终结果事务A读取的是1,而事务B读取的是3。为什么是这样,我们来分析一下。
假设在事务A开始的时候只有一个transaciton id = 1, 那么事务A就是2,事务B就是3,事务C就是4.
事务A的试图数组就是[1,2] 事务B视图数组[1,2,3] , 事务C视图数组[1,2,3,4];
当事务C进行修改k=k+1 ,就将id=1的k 设置为2。但是接着事务B也加1操作,此时事务B的+1操作其实是当前读,也就是获取最新的数据,k=2, 在k=2的基础上+1 操作,那么k=3。所以事务B的K值事3。但是事务A的视图数组[1,2] 查询undo日志链,发现 【3,4】都查看不了,所以k=1;
对比规则
-
如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
-
如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
-
如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见
一个简易的版本就是
- 版本未提交,不可见;
- 版本已提交,但是是在视图创建后提交的,不可见;
- 版本已提交,而且是在视图创建前提交的,可见。
MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取 同一条数据在版本链上的不同版本数据。
不同的读操作
select * from table where ?;
select * from table where ? lock in share mode; # 加读锁 select * from table where ? for update;# 加写锁
insert into table values (...);# 加写锁
update table set ? where ?;# 加写锁
delete from table where ?;# 加写锁
# 所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发 事务不能修改当前记录,对读取记录加锁。
# 其中,除了第一条语句,对读取记录加读锁外,其他的操作都加的是写锁。
那么思考一个逻辑。如果一个事务A 对id=1更新操作的时候,还没有提交,那么事务B也对id=2更新操作,会出现什么情况?
答案就是会阻塞事务B,必须等事务A执行完毕。
bufferpool缓存
在我们更新一条SQL数据的时候,大概流程如下
1.构建连接、查询缓存、分析器、优化器、执行器
2.在执行器的时候
- 如果buffer pool有对应的页数据,直接获取,否则从磁盘加载对应的id=1的数据 name=zhuge。
- 将name=‘zhuge’ 进行写入undo日志文件中,(主要方式如果事务进行回滚的话,可以直接恢复数据)
- 更新内存中的buffer pool的数据 name=‘zhuge 666’
- 写入redo log日志。准备阶段。 (系统宕机,用于恢复数据 重做)
- 写入bin log日志,然后提交事务。
我们来思考下,为什么需要设计一套这么复杂的,因为主要是对于磁盘的操作是随机IO性能不高,可以通过写入LOG文件,提升性能。先更新到BufferPool中,然后顺序写日志文件。也可以保证各种异常情况下数据的一致性。
几个小问题?
1.脏页刷盘的时机?(大概四种 a.redolog满了 binnodl buffer满了 c:myg!正常关闭 d.mysql空闲)
2.如果数据库突然奔溃了,没刷盘的数据是不是就丟了?(不会,redolog防崩溃)
3.如果redo.log没写入磁盘,这时候这部分事务是不是数据就丢了(redolog buffer 里的数据丢了怎么办,redolog buffer记录的是 事务prepare阶段数据(未提交 丢了无所谓))
4.如果redolog在刷盘的时候断电呢。
总结
MySQL的事务是如何保证的,我们用了两篇文章进行详细描述,通过ACID,其中AID是为了保证C。
(隔离性):MVCC原理、(原子性):innodb 事务二阶段提交、D(持久性):事务提交后的数据落盘。以及通过相关的锁机制,来保证。